diff --git a/assets/src/bundles/admin/origin-save.js b/assets/src/bundles/admin/origin-save.js index 4ae62417..87c1dff5 100644 --- a/assets/src/bundles/admin/origin-save.js +++ b/assets/src/bundles/admin/origin-save.js @@ -1,363 +1,353 @@ /** * 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 */ import {handleFetchError, csrfPost, htmlAlert} 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); let columnsData = [ { data: 'id', name: 'id', visible: false, searchable: false }, { data: 'save_request_date', name: 'request_date', render: (data, type, row) => { if (type === 'display') { let date = new Date(data); return date.toLocaleString(); } return data; } }, { data: '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 += `${sanitizedURL}`; } else { html += sanitizedURL; } html += ` `; return html; } return data; } } ]; pendingSaveRequestsTable = $('#swh-origin-save-pending-requests').DataTable({ serverSide: true, processing: true, language: { processing: `` }, 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); rejectedSaveRequestsTable = $('#swh-origin-save-rejected-requests').DataTable({ serverSide: true, processing: true, language: { processing: `` }, 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.push({ data: 'save_task_status', name: 'save_task_status' }); columnsData.push({ name: 'info', render: (data, type, row) => { if (row.save_task_status === 'succeeded' || row.save_task_status === 'failed') { return '`; } else { return ''; } } }); acceptedSaveRequestsTable = $('#swh-origin-save-accepted-requests').DataTable({ serverSide: true, processing: true, language: { processing: `` }, 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) { - event.stopPropagation(); + e.stopPropagation(); } else if ($(e.target).parents('.swh-save-request-info').length === 0) { $('.swh-save-request-info').popover('dispose'); } }); }); } -export function addAuthorizedOriginUrl() { - let originUrl = $('#swh-authorized-url-prefix').val(); - let addOriginUrl = Urls.admin_origin_save_add_authorized_url(originUrl); - csrfPost(addOriginUrl) - .then(handleFetchError) - .then(() => { - 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(response => { - $('.swh-add-authorized-origin-status').html( - htmlAlert('warning', 'The provided origin url prefix is already registered in the authorized list.', true) - ); - }); +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 function removeAuthorizedOriginUrl() { - let originUrl = $('#swh-authorized-origin-urls tr.selected').text(); +export async function removeAuthorizedOriginUrl() { + const originUrl = $('#swh-authorized-origin-urls tr.selected').text(); if (originUrl) { - let removeOriginUrl = Urls.admin_origin_save_remove_authorized_url(originUrl); - csrfPost(removeOriginUrl) - .then(handleFetchError) - .then(() => { - authorizedOriginTable.row('.selected').remove().draw(); - }) - .catch(() => {}); + 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 function addUnauthorizedOriginUrl() { - let originUrl = $('#swh-unauthorized-url-prefix').val(); - let addOriginUrl = Urls.admin_origin_save_add_unauthorized_url(originUrl); - csrfPost(addOriginUrl) - .then(handleFetchError) - .then(() => { - 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 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 function removeUnauthorizedOriginUrl() { - let originUrl = $('#swh-unauthorized-origin-urls tr.selected').text(); +export async function removeUnauthorizedOriginUrl() { + const originUrl = $('#swh-unauthorized-origin-urls tr.selected').text(); if (originUrl) { - let removeOriginUrl = Urls.admin_origin_save_remove_unauthorized_url(originUrl); - csrfPost(removeOriginUrl) - .then(handleFetchError) - .then(() => { - unauthorizedOriginTable.row('.selected').remove().draw(); - }) - .catch(() => {}); + 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() { - let selectedRow = pendingSaveRequestsTable.row('.selected'); + const selectedRow = pendingSaveRequestsTable.row('.selected'); if (selectedRow.length) { - let acceptOriginSaveRequestCallback = () => { - let rowData = selectedRow.data(); - let acceptSaveRequestUrl = Urls.admin_origin_save_request_accept(rowData['visit_type'], rowData['origin_url']); - csrfPost(acceptSaveRequestUrl) - .then(() => { - pendingSaveRequestsTable.ajax.reload(null, false); - }); + 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); } } export function rejectOriginSaveRequest() { - let selectedRow = pendingSaveRequestsTable.row('.selected'); + const selectedRow = pendingSaveRequestsTable.row('.selected'); if (selectedRow.length) { - let rejectOriginSaveRequestCallback = () => { - let rowData = selectedRow.data(); - let rejectSaveRequestUrl = Urls.admin_origin_save_request_reject(rowData['visit_type'], rowData['origin_url']); - csrfPost(rejectSaveRequestUrl) - .then(() => { - pendingSaveRequestsTable.ajax.reload(null, false); - }); + let rejectOriginSaveRequestCallback = async() => { + const rowData = selectedRow.data(); + const rejectSaveRequestUrl = Urls.admin_origin_save_request_reject(rowData['visit_type'], rowData['origin_url']); + await csrfPost(rejectSaveRequestUrl); + pendingSaveRequestsTable.ajax.reload(null, false); }; swh.webapp.showModalConfirm( 'Reject origin save request ?', 'Are you sure to reject this origin save request ?', rejectOriginSaveRequestCallback); } } function removeOriginSaveRequest(requestTable) { let selectedRow = requestTable.row('.selected'); if (selectedRow.length) { let requestId = selectedRow.data()['id']; - let removeOriginSaveRequestCallback = () => { - let removeSaveRequestUrl = Urls.admin_origin_save_request_remove(requestId); - csrfPost(removeSaveRequestUrl) - .then(() => { - requestTable.ajax.reload(null, false); - }); + let 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/auth/index.js b/assets/src/bundles/auth/index.js index 652736b1..a560efb4 100644 --- a/assets/src/bundles/auth/index.js +++ b/assets/src/bundles/auth/index.js @@ -1,193 +1,190 @@ /** * Copyright (C) 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 */ import {handleFetchError, csrfPost, removeUrlFragment} from 'utils/functions'; import './auth.css'; let apiTokensTable; function tokenForm(infoText, buttonText) { const form = `

${infoText}

`; return form; } function errorMessage(message) { return `

${message}

`; } function successMessage(message) { return `

${message}

`; } function disableSubmitButton() { $('#swh-token-form-submit').prop('disabled', true); } function generateToken() { window.location = Urls.oidc_generate_bearer_token(); } -function displayToken(tokenId) { +async function displayToken(tokenId) { const postData = { token_id: tokenId }; - csrfPost(Urls.oidc_get_bearer_token(), {}, JSON.stringify(postData)) - .then(handleFetchError) - .then(response => response.text()) - .then(token => { - const tokenHtml = - `

Below is your token.

-
${token}
`; - swh.webapp.showModalHtml('Display bearer token', tokenHtml); - }) - .catch(response => { - response.text().then(responseText => { - let errorMsg = 'Internal server error.'; - if (response.status === 400) { - errorMsg = responseText; - } - swh.webapp.showModalHtml('Display bearer token', errorMessage(errorMsg)); - }); - }); + try { + const response = await csrfPost(Urls.oidc_get_bearer_token(), {}, JSON.stringify(postData)); + handleFetchError(response); + const token = await response.text(); + const tokenHtml = + `

Below is your token.

+
${token}
`; + swh.webapp.showModalHtml('Display bearer token', tokenHtml); + } catch (response) { + const responseText = await response.text(); + let errorMsg = 'Internal server error.'; + if (response.status === 400) { + errorMsg = responseText; + } + swh.webapp.showModalHtml('Display bearer token', errorMessage(errorMsg)); + } } -function revokeTokens(tokenIds) { +async function revokeTokens(tokenIds) { const postData = { token_ids: tokenIds }; - csrfPost(Urls.oidc_revoke_bearer_tokens(), {}, JSON.stringify(postData)) - .then(handleFetchError) - .then(() => { - disableSubmitButton(); - $('#swh-token-form-message').html( - successMessage(`Bearer token${tokenIds.length > 1 ? 's' : ''} successfully revoked.`)); - apiTokensTable.draw(); - }) - .catch(() => { - $('#swh-token-form-message').html(errorMessage('Internal server error.')); - }); + try { + const response = await csrfPost(Urls.oidc_revoke_bearer_tokens(), {}, JSON.stringify(postData)); + handleFetchError(response); + disableSubmitButton(); + $('#swh-token-form-message').html( + successMessage(`Bearer token${tokenIds.length > 1 ? 's' : ''} successfully revoked.`)); + apiTokensTable.draw(); + } catch (_) { + $('#swh-token-form-message').html(errorMessage('Internal server error.')); + } } function revokeToken(tokenId) { revokeTokens([tokenId]); } function revokeAllTokens() { const tokenIds = []; const rowsData = apiTokensTable.rows().data(); for (let i = 0; i < rowsData.length; ++i) { tokenIds.push(rowsData[i].id); } revokeTokens(tokenIds); } export function applyTokenAction(action, tokenId) { const actionData = { display: { submitCallback: displayToken }, generate: { modalTitle: 'Bearer token generation', infoText: 'Click on the button to generate the token. You will be redirected to ' + 'Software Heritage Authentication Service and might be asked to enter ' + 'your password again.', buttonText: 'Generate token', submitCallback: generateToken }, revoke: { modalTitle: 'Revoke bearer token', infoText: 'Click on the button to revoke the token.', buttonText: 'Revoke token', submitCallback: revokeToken }, revokeAll: { modalTitle: 'Revoke all bearer tokens', infoText: 'Click on the button to revoke all tokens.', buttonText: 'Revoke tokens', submitCallback: revokeAllTokens } }; if (!actionData[action]) { return; } if (action !== 'display') { const tokenFormHtml = tokenForm( actionData[action].infoText, actionData[action].buttonText); swh.webapp.showModalHtml(actionData[action].modalTitle, tokenFormHtml); $(`#swh-token-form`).submit(event => { event.preventDefault(); event.stopPropagation(); actionData[action].submitCallback(tokenId); }); } else { actionData[action].submitCallback(tokenId); } } export function initProfilePage() { $(document).ready(() => { apiTokensTable = $('#swh-bearer-tokens-table') .on('error.dt', (e, settings, techNote, message) => { $('#swh-origin-save-request-list-error').text( 'An error occurred while retrieving the tokens list'); console.log(message); }) .DataTable({ serverSide: true, ajax: Urls.oidc_list_bearer_tokens(), columns: [ { data: 'creation_date', name: 'creation_date', render: (data, type, row) => { if (type === 'display') { let date = new Date(data); return date.toLocaleString(); } return data; } }, { render: (data, type, row) => { const html = ` `; return html; } } ], ordering: false, searching: false, scrollY: '50vh', scrollCollapse: true }); $('#swh-oidc-profile-tokens-tab').on('shown.bs.tab', () => { apiTokensTable.draw(); window.location.hash = '#tokens'; }); $('#swh-oidc-profile-account-tab').on('shown.bs.tab', () => { removeUrlFragment(); }); if (window.location.hash === '#tokens') { $('.nav-tabs a[href="#swh-oidc-profile-tokens"]').tab('show'); } }); } diff --git a/assets/src/bundles/browse/origin-search.js b/assets/src/bundles/browse/origin-search.js index 97508473..226d722b 100644 --- a/assets/src/bundles/browse/origin-search.js +++ b/assets/src/bundles/browse/origin-search.js @@ -1,270 +1,260 @@ /** * Copyright (C) 2018-2021 The Software Heritage developers * See the AUTHORS file at the top-level directory of this distribution * License: GNU Affero General Public License version 3, or any later version * See top-level LICENSE file for more information */ import {handleFetchError, isArchivedOrigin} from 'utils/functions'; const limit = 100; let linksPrev = []; let linkNext = null; let linkCurrent = null; let inSearch = false; function parseLinkHeader(s) { let re = /<(.+)>; rel="next"/; return s.match(re)[1]; } function fixTableRowsStyle() { setTimeout(() => { $('#origin-search-results tbody tr').removeAttr('style'); }); } function clearOriginSearchResultsTable() { $('#origin-search-results tbody tr').remove(); } -function populateOriginSearchResultsTable(origins) { +async function populateOriginSearchResultsTable(origins) { if (origins.length > 0) { $('#swh-origin-search-results').show(); $('#swh-no-result').hide(); clearOriginSearchResultsTable(); let table = $('#origin-search-results tbody'); + let promises = []; for (let [i, origin] of origins.entries()) { let browseUrl = `${Urls.browse_origin()}?origin_url=${encodeURIComponent(origin.url)}`; let tableRow = ``; tableRow += `` + '' + 'Checking'; tableRow += '' + `${origin.url}`; tableRow += `` + '' + 'Checking'; tableRow += ''; table.append(tableRow); // get async latest visit snapshot and update visit status icon let latestSnapshotUrl = Urls.api_1_origin_visit_latest(origin.url); latestSnapshotUrl += '?require_snapshot=true'; - fetch(latestSnapshotUrl) - .then(response => { - if (response.status === 404) { - throw new Error(); - } - return response.json(); - }) - .then(data => { - if (data.type) { - $(`#visit-type-origin-${i}`).html(data.type); - $(`#visit-status-origin-${i}`).html( - 'Archived'); - } else { - throw new Error(); - } - }) - .catch(() => { - $(`#visit-type-origin-${i}`).html('unknown'); - $(`#visit-status-origin-${i}`).html( - 'Pending archival'); - if ($('#swh-filter-empty-visits').prop('checked')) { - $(`#origin-${i}`).remove(); - } - }); + promises.push(fetch(latestSnapshotUrl)); + } + const responses = await Promise.all(promises); + const responsesData = await Promise.all(responses.map(r => r.json())); + for (let i = 0; i < responses.length; ++i) { + const response = responses[i]; + const data = responsesData[i]; + if (response.status !== 404 && data.type) { + $(`#visit-type-origin-${i}`).html(data.type); + $(`#visit-status-origin-${i}`).html( + 'Archived'); + } else { + $(`#visit-type-origin-${i}`).html('unknown'); + $(`#visit-status-origin-${i}`).html( + 'Pending archival'); + if ($('#swh-filter-empty-visits').prop('checked')) { + $(`#origin-${i}`).remove(); + } + } } fixTableRowsStyle(); } else { $('#swh-origin-search-results').hide(); $('#swh-no-result').text('No origins matching the search criteria were found.'); $('#swh-no-result').show(); } if (linkNext === null) { $('#origins-next-results-button').addClass('disabled'); } else { $('#origins-next-results-button').removeClass('disabled'); } if (linksPrev.length === 0) { $('#origins-prev-results-button').addClass('disabled'); } else { $('#origins-prev-results-button').removeClass('disabled'); } inSearch = false; setTimeout(() => { window.scrollTo(0, 0); }); } function searchOriginsFirst(searchQueryText, limit) { let baseSearchUrl; let searchMetadata = $('#swh-search-origin-metadata').prop('checked'); if (searchMetadata) { baseSearchUrl = new URL(Urls.api_1_origin_metadata_search(), window.location); baseSearchUrl.searchParams.append('fulltext', searchQueryText); } else { baseSearchUrl = new URL(Urls.api_1_origin_search(searchQueryText), window.location); } let withVisit = $('#swh-search-origins-with-visit').prop('checked'); baseSearchUrl.searchParams.append('limit', limit); baseSearchUrl.searchParams.append('with_visit', withVisit); const visitType = $('#swh-search-visit-type').val(); if (visitType !== 'any') { baseSearchUrl.searchParams.append('visit_type', visitType); } let searchUrl = baseSearchUrl.toString(); searchOrigins(searchUrl); } -function searchOrigins(searchUrl) { +async function searchOrigins(searchUrl) { clearOriginSearchResultsTable(); $('.swh-loading').addClass('show'); - let response = fetch(searchUrl) - .then(handleFetchError) - .then(resp => { - response = resp; - return response.json(); - }) - .then(data => { - // Save link to the current results page - linkCurrent = searchUrl; - // Save link to the next results page. - linkNext = null; - if (response.headers.has('Link')) { - let parsedLink = parseLinkHeader(response.headers.get('Link')); - if (parsedLink !== undefined) { - linkNext = parsedLink; - } + try { + const response = await fetch(searchUrl); + handleFetchError(response); + const data = await response.json(); + // Save link to the current results page + linkCurrent = searchUrl; + // Save link to the next results page. + linkNext = null; + if (response.headers.has('Link')) { + let parsedLink = parseLinkHeader(response.headers.get('Link')); + if (parsedLink !== undefined) { + linkNext = parsedLink; } - // prevLinks is updated by the caller, which is the one to know if - // we're going forward or backward in the pages. + } + // prevLinks is updated by the caller, which is the one to know if + // we're going forward or backward in the pages. - $('.swh-loading').removeClass('show'); - populateOriginSearchResultsTable(data); - }) - .catch(response => { - $('.swh-loading').removeClass('show'); - inSearch = false; - $('#swh-origin-search-results').hide(); - $('#swh-no-result').text(`Error ${response.status}: ${response.statusText}`); - $('#swh-no-result').show(); - }); + $('.swh-loading').removeClass('show'); + populateOriginSearchResultsTable(data); + } catch (response) { + $('.swh-loading').removeClass('show'); + inSearch = false; + $('#swh-origin-search-results').hide(); + $('#swh-no-result').text(`Error ${response.status}: ${response.statusText}`); + $('#swh-no-result').show(); + } } async function doSearch() { $('#swh-no-result').hide(); - let searchQueryText = $('#swh-origins-url-patterns').val(); + const searchQueryText = $('#swh-origins-url-patterns').val(); inSearch = true; if (searchQueryText.startsWith('swh:')) { - // searchQueryText may be a PID so sending search queries to PID resolve endpoint - let resolveSWHIDUrl = Urls.api_1_resolve_swhid(searchQueryText); - fetch(resolveSWHIDUrl) - .then(handleFetchError) - .then(response => response.json()) - .then(data => { - // SWHID has been successfully resolved, - // so redirect to browse page - window.location = data.browse_url; - }) - .catch(response => { - // display a useful error message if the input - // looks like a SWHID - response.json().then(data => { - $('#swh-origin-search-results').hide(); - $('.swh-search-pagination').hide(); - $('#swh-no-result').text(data.reason); - $('#swh-no-result').show(); - }); - }); + try { + // searchQueryText may be a PID so sending search queries to PID resolve endpoint + const resolveSWHIDUrl = Urls.api_1_resolve_swhid(searchQueryText); + const response = await fetch(resolveSWHIDUrl); + handleFetchError(response); + const data = await response.json(); + // SWHID has been successfully resolved, + // so redirect to browse page + window.location = data.browse_url; + } catch (response) { + // display a useful error message if the input + // looks like a SWHID + const data = await response.json(); + $('#swh-origin-search-results').hide(); + $('.swh-search-pagination').hide(); + $('#swh-no-result').text(data.reason); + $('#swh-no-result').show(); + } } else if (await isArchivedOrigin(searchQueryText)) { // redirect to the browse origin window.location.href = `${Urls.browse_origin()}?origin_url=${encodeURIComponent(searchQueryText)}`; } else { // otherwise, proceed with origins search irrespective of the error $('#swh-origin-search-results').show(); $('.swh-search-pagination').show(); searchOriginsFirst(searchQueryText, limit); } } export function initOriginSearch() { $(document).ready(() => { $('#swh-search-origins').submit(event => { event.preventDefault(); if (event.target.checkValidity()) { $(event.target).removeClass('was-validated'); let searchQueryText = $('#swh-origins-url-patterns').val().trim(); let withVisit = $('#swh-search-origins-with-visit').prop('checked'); let withContent = $('#swh-filter-empty-visits').prop('checked'); let searchMetadata = $('#swh-search-origin-metadata').prop('checked'); const visitType = $('#swh-search-visit-type').val(); let queryParameters = new URLSearchParams(); queryParameters.append('q', searchQueryText); if (withVisit) { queryParameters.append('with_visit', withVisit); } if (withContent) { queryParameters.append('with_content', withContent); } if (searchMetadata) { queryParameters.append('search_metadata', searchMetadata); } if (visitType !== 'any') { queryParameters.append('visit_type', visitType); } // Update the url, triggering page reload and effective search window.location = `${Urls.browse_search()}?${queryParameters.toString()}`; } else { $(event.target).addClass('was-validated'); } }); $('#origins-next-results-button').click(event => { if ($('#origins-next-results-button').hasClass('disabled') || inSearch) { return; } inSearch = true; linksPrev.push(linkCurrent); searchOrigins(linkNext); event.preventDefault(); }); $('#origins-prev-results-button').click(event => { if ($('#origins-prev-results-button').hasClass('disabled') || inSearch) { return; } inSearch = true; searchOrigins(linksPrev.pop()); event.preventDefault(); }); let urlParams = new URLSearchParams(window.location.search); let query = urlParams.get('q'); let withVisit = urlParams.has('with_visit'); let withContent = urlParams.has('with_content'); let searchMetadata = urlParams.has('search_metadata'); let visitType = urlParams.get('visit_type'); if (query) { $('#swh-origins-url-patterns').val(query); $('#swh-search-origins-with-visit').prop('checked', withVisit); $('#swh-filter-empty-visits').prop('checked', withContent); $('#swh-search-origin-metadata').prop('checked', searchMetadata); if (visitType) { $('#swh-search-visit-type').val(visitType); } doSearch(); } }); } diff --git a/assets/src/bundles/revision/diff-utils.js b/assets/src/bundles/revision/diff-utils.js index 33351d61..8c462d48 100644 --- a/assets/src/bundles/revision/diff-utils.js +++ b/assets/src/bundles/revision/diff-utils.js @@ -1,794 +1,793 @@ /** * 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 */ import 'waypoints/lib/jquery.waypoints'; import {swhSpinnerSrc} from 'utils/constants'; import {removeUrlFragment} from 'utils/functions'; import diffPanelTemplate from './diff-panel.ejs'; // number of changed files in the revision let changes = null; let nbChangedFiles = 0; // to track the number of already computed files diffs let nbDiffsComputed = 0; // the no newline at end of file marker from Github let noNewLineMarker = '' + '' + ''; // to track the total number of added lines in files diffs let nbAdditions = 0; // to track the total number of deleted lines in files diffs let nbDeletions = 0; // to track the already computed diffs by id let computedDiffs = {}; // map a diff id to its computation url let diffsUrls = {}; // to keep track of diff lines to highlight let startLines = null; let endLines = null; // map max line numbers characters to diff const diffMaxNumberChars = {}; // focused diff for highlighting let focusedDiff = null; // highlighting color const lineHighlightColor = '#fdf3da'; // might contain diff lines to highlight parsed from URL fragment let selectedDiffLinesInfo; // URL fragment to append when switching to 'Changes' tab const changesUrlFragment = '#swh-revision-changes'; // current displayed tab name let currentTabName = 'Files'; // to check if a DOM element is in the viewport function isInViewport(elt) { let elementTop = $(elt).offset().top; let elementBottom = elementTop + $(elt).outerHeight(); let viewportTop = $(window).scrollTop(); let viewportBottom = viewportTop + $(window).height(); return elementBottom > viewportTop && elementTop < viewportBottom; } // to format the diffs line numbers export function formatDiffLineNumbers(diffId, fromLine, toLine) { const maxNumberChars = diffMaxNumberChars[diffId]; const fromLineStr = toLnStr(fromLine); const toLineStr = toLnStr(toLine); let ret = ''; for (let i = 0; i < (maxNumberChars - fromLineStr.length); ++i) { ret += ' '; } ret += fromLineStr; ret += ' '; for (let i = 0; i < (maxNumberChars - toLineStr.length); ++i) { ret += ' '; } ret += toLineStr; return ret; } function parseDiffHunkRangeIfAny(lineText) { let baseFromLine, baseToLine; if (lineText.startsWith('@@')) { let linesInfoRegExp = new RegExp(/^@@ -(\d+),(\d+) \+(\d+),(\d+) @@$/gm); let linesInfoRegExp2 = new RegExp(/^@@ -(\d+) \+(\d+),(\d+) @@$/gm); let linesInfoRegExp3 = new RegExp(/^@@ -(\d+),(\d+) \+(\d+) @@$/gm); let linesInfoRegExp4 = new RegExp(/^@@ -(\d+) \+(\d+) @@$/gm); let linesInfo = linesInfoRegExp.exec(lineText); let linesInfo2 = linesInfoRegExp2.exec(lineText); let linesInfo3 = linesInfoRegExp3.exec(lineText); let linesInfo4 = linesInfoRegExp4.exec(lineText); if (linesInfo) { baseFromLine = parseInt(linesInfo[1]) - 1; baseToLine = parseInt(linesInfo[3]) - 1; } else if (linesInfo2) { baseFromLine = parseInt(linesInfo2[1]) - 1; baseToLine = parseInt(linesInfo2[2]) - 1; } else if (linesInfo3) { baseFromLine = parseInt(linesInfo3[1]) - 1; baseToLine = parseInt(linesInfo3[3]) - 1; } else if (linesInfo4) { baseFromLine = parseInt(linesInfo4[1]) - 1; baseToLine = parseInt(linesInfo4[2]) - 1; } } if (baseFromLine !== undefined) { return [baseFromLine, baseToLine]; } else { return null; } } function toLnInt(lnStr) { return lnStr ? parseInt(lnStr) : 0; }; function toLnStr(lnInt) { return lnInt ? lnInt.toString() : ''; }; // parse diff line numbers to an int array [from, to] export function parseDiffLineNumbers(lineNumbersStr, from, to) { let lines; if (!from && !to) { lines = lineNumbersStr.replace(/[ ]+/g, ' ').split(' '); if (lines.length > 2) { lines.shift(); } lines = lines.map(x => toLnInt(x)); } else { let lineNumber = toLnInt(lineNumbersStr.trim()); if (from) { lines = [lineNumber, 0]; } else if (to) { lines = [0, lineNumber]; } } return lines; } // serialize selected line numbers range to string for URL fragment export function selectedDiffLinesToFragment(startLines, endLines, unified) { let selectedLinesFragment = ''; selectedLinesFragment += `F${startLines[0] || 0}`; selectedLinesFragment += `T${startLines[1] || 0}`; selectedLinesFragment += `-F${endLines[0] || 0}`; selectedLinesFragment += `T${endLines[1] || 0}`; if (unified) { selectedLinesFragment += '-unified'; } else { selectedLinesFragment += '-split'; } return selectedLinesFragment; } // parse selected lines from URL fragment export function fragmentToSelectedDiffLines(fragment) { const RE_LINES = /F([0-9]+)T([0-9]+)-F([0-9]+)T([0-9]+)-([a-z]+)/; const matchObj = RE_LINES.exec(fragment); if (matchObj.length === 6) { return { startLines: [parseInt(matchObj[1]), parseInt(matchObj[2])], endLines: [parseInt(matchObj[3]), parseInt(matchObj[4])], unified: matchObj[5] === 'unified' }; } else { return null; } } // function to highlight a single diff line function highlightDiffLine(diffId, i) { let line = $(`#${diffId} .hljs-ln-line[data-line-number="${i}"]`); let lineNumbers = $(`#${diffId} .hljs-ln-numbers[data-line-number="${i}"]`); lineNumbers.css('color', 'black'); lineNumbers.css('font-weight', 'bold'); line.css('background-color', lineHighlightColor); line.css('mix-blend-mode', 'multiply'); return line; } // function to reset highlighting function resetHighlightedDiffLines(resetVars = true) { if (resetVars) { focusedDiff = null; startLines = null; endLines = null; } $('.hljs-ln-line[data-line-number]').css('background-color', 'initial'); $('.hljs-ln-line[data-line-number]').css('mix-blend-mode', 'initial'); $('.hljs-ln-numbers[data-line-number]').css('color', '#aaa'); $('.hljs-ln-numbers[data-line-number]').css('font-weight', 'initial'); if (currentTabName === 'Changes' && window.location.hash !== changesUrlFragment) { window.history.replaceState('', document.title, window.location.pathname + window.location.search + changesUrlFragment); } } // highlight lines in a diff, return first highlighted line numbers element function highlightDiffLines(diffId, startLines, endLines, unified) { let firstHighlightedLine; // unified diff case if (unified) { let start = formatDiffLineNumbers(diffId, startLines[0], startLines[1]); let end = formatDiffLineNumbers(diffId, endLines[0], endLines[1]); const startLine = $(`#${diffId} .hljs-ln-line[data-line-number="${start}"]`); const endLine = $(`#${diffId} .hljs-ln-line[data-line-number="${end}"]`); if ($(endLine).position().top < $(startLine).position().top) { [start, end] = [end, start]; firstHighlightedLine = endLine; } else { firstHighlightedLine = startLine; } const lineTd = highlightDiffLine(diffId, start); let tr = $(lineTd).closest('tr'); let lineNumbers = $(tr).children('.hljs-ln-line').data('line-number').toString(); while (lineNumbers !== end) { if (lineNumbers.trim()) { highlightDiffLine(diffId, lineNumbers); } tr = $(tr).next(); lineNumbers = $(tr).children('.hljs-ln-line').data('line-number').toString(); } highlightDiffLine(diffId, end); // split diff case } else { // highlight only from part of the diff if (startLines[0] && endLines[0]) { const start = Math.min(startLines[0], endLines[0]); const end = Math.max(startLines[0], endLines[0]); for (let i = start; i <= end; ++i) { highlightDiffLine(`${diffId}-from`, i); } firstHighlightedLine = $(`#${diffId}-from .hljs-ln-line[data-line-number="${start}"]`); // highlight only to part of the diff } else if (startLines[1] && endLines[1]) { const start = Math.min(startLines[1], endLines[1]); const end = Math.max(startLines[1], endLines[1]); for (let i = start; i <= end; ++i) { highlightDiffLine(`${diffId}-to`, i); } firstHighlightedLine = $(`#${diffId}-to .hljs-ln-line[data-line-number="${start}"]`); // highlight both part of the diff } else { let left, right; if (startLines[0] && endLines[1]) { left = startLines[0]; right = endLines[1]; } else { left = endLines[0]; right = startLines[1]; } const leftLine = $(`#${diffId}-from .hljs-ln-line[data-line-number="${left}"]`); const rightLine = $(`#${diffId}-to .hljs-ln-line[data-line-number="${right}"]`); const leftLineAbove = $(leftLine).position().top < $(rightLine).position().top; if (leftLineAbove) { firstHighlightedLine = leftLine; } else { firstHighlightedLine = rightLine; } let fromTr = $(`#${diffId}-from tr`).first(); let fromLn = $(fromTr).children('.hljs-ln-line').data('line-number'); let toTr = $(`#${diffId}-to tr`).first(); let toLn = $(toTr).children('.hljs-ln-line').data('line-number'); let canHighlight = false; while (true) { if (leftLineAbove && fromLn === left) { canHighlight = true; } else if (!leftLineAbove && toLn === right) { canHighlight = true; } if (canHighlight && fromLn) { highlightDiffLine(`${diffId}-from`, fromLn); } if (canHighlight && toLn) { highlightDiffLine(`${diffId}-to`, toLn); } if ((leftLineAbove && toLn === right) || (!leftLineAbove && fromLn === left)) { break; } fromTr = $(fromTr).next(); fromLn = $(fromTr).children('.hljs-ln-line').data('line-number'); toTr = $(toTr).next(); toLn = $(toTr).children('.hljs-ln-line').data('line-number'); } } } let selectedLinesFragment = selectedDiffLinesToFragment(startLines, endLines, unified); window.location.hash = `diff_${diffId}+${selectedLinesFragment}`; return firstHighlightedLine; } // callback to switch from side-by-side diff to unified one export function showUnifiedDiff(diffId) { $(`#${diffId}-split-diff`).css('display', 'none'); $(`#${diffId}-unified-diff`).css('display', 'block'); } // callback to switch from unified diff to side-by-side one export function showSplitDiff(diffId) { $(`#${diffId}-unified-diff`).css('display', 'none'); $(`#${diffId}-split-diff`).css('display', 'block'); } // to compute diff and process it for display -export function computeDiff(diffUrl, diffId) { +export async function computeDiff(diffUrl, diffId) { // force diff computation ? let force = diffUrl.indexOf('force=true') !== -1; // it no forced computation and diff already computed, do nothing if (!force && computedDiffs.hasOwnProperty(diffId)) { return; } function setLineNumbers(lnElt, lineNumbers) { $(lnElt).attr('data-line-number', lineNumbers || ''); $(lnElt).children().attr('data-line-number', lineNumbers || ''); $(lnElt).siblings().attr('data-line-number', lineNumbers || ''); } // mark diff computation as already requested computedDiffs[diffId] = true; $(`#${diffId}-loading`).css('visibility', 'visible'); // set spinner visible while requesting diff $(`#${diffId}-loading`).css('display', 'block'); $(`#${diffId}-highlightjs`).css('display', 'none'); // request diff computation and process it - fetch(diffUrl) - .then(response => response.json()) - .then(data => { - // increment number of computed diffs - ++nbDiffsComputed; - // toggle the 'Compute all diffs' button if all diffs have been computed - if (nbDiffsComputed === changes.length) { - $('#swh-compute-all-diffs').addClass('active'); - } + const response = await fetch(diffUrl); + const data = await response.json(); + + // increment number of computed diffs + ++nbDiffsComputed; + // toggle the 'Compute all diffs' button if all diffs have been computed + if (nbDiffsComputed === changes.length) { + $('#swh-compute-all-diffs').addClass('active'); + } - // Large diff (> threshold) are not automatically computed, - // add a button to force its computation - if (data.diff_str.indexOf('Large diff') === 0) { - $(`#${diffId}`)[0].innerHTML = data.diff_str + + // Large diff (> threshold) are not automatically computed, + // add a button to force its computation + if (data.diff_str.indexOf('Large diff') === 0) { + $(`#${diffId}`)[0].innerHTML = data.diff_str + `
'; - setDiffVisible(diffId); - } else if (data.diff_str.indexOf('@@') !== 0) { - $(`#${diffId}`).text(data.diff_str); - setDiffVisible(diffId); - } else { + setDiffVisible(diffId); + } else if (data.diff_str.indexOf('@@') !== 0) { + $(`#${diffId}`).text(data.diff_str); + setDiffVisible(diffId); + } else { - // prepare code highlighting - $(`.${diffId}`).removeClass('nohighlight'); - $(`.${diffId}`).addClass(data.language); + // prepare code highlighting + $(`.${diffId}`).removeClass('nohighlight'); + $(`.${diffId}`).addClass(data.language); - // set unified diff text - $(`#${diffId}`).text(data.diff_str); + // set unified diff text + $(`#${diffId}`).text(data.diff_str); - // code highlighting for unified diff - $(`#${diffId}`).each((i, elt) => { - hljs.highlightElement(elt); - hljs.lineNumbersElementSync(elt); - }); + // code highlighting for unified diff + $(`#${diffId}`).each((i, elt) => { + hljs.highlightElement(elt); + hljs.lineNumbersElementSync(elt); + }); - // process unified diff lines in order to generate side-by-side diffs text - // but also compute line numbers for unified and side-by-side diffs - let baseFromLine = ''; - let baseToLine = ''; - let fromToLines = []; - let fromLines = []; - let toLines = []; - let maxNumberChars = 0; - let diffFromStr = ''; - let diffToStr = ''; - let linesOffset = 0; - - $(`#${diffId} .hljs-ln-numbers`).each((i, lnElt) => { - let lnText = lnElt.nextSibling.innerText; - let linesInfo = parseDiffHunkRangeIfAny(lnText); - let fromLine = ''; - let toLine = ''; - // parsed lines info from the diff output - if (linesInfo) { - baseFromLine = linesInfo[0]; - baseToLine = linesInfo[1]; - linesOffset = 0; - diffFromStr += (lnText + '\n'); - diffToStr += (lnText + '\n'); - fromLines.push(''); + // process unified diff lines in order to generate side-by-side diffs text + // but also compute line numbers for unified and side-by-side diffs + let baseFromLine = ''; + let baseToLine = ''; + let fromToLines = []; + let fromLines = []; + let toLines = []; + let maxNumberChars = 0; + let diffFromStr = ''; + let diffToStr = ''; + let linesOffset = 0; + + $(`#${diffId} .hljs-ln-numbers`).each((i, lnElt) => { + let lnText = lnElt.nextSibling.innerText; + let linesInfo = parseDiffHunkRangeIfAny(lnText); + let fromLine = ''; + let toLine = ''; + // parsed lines info from the diff output + if (linesInfo) { + baseFromLine = linesInfo[0]; + baseToLine = linesInfo[1]; + linesOffset = 0; + diffFromStr += (lnText + '\n'); + diffToStr += (lnText + '\n'); + fromLines.push(''); + toLines.push(''); + // line removed in the from file + } else if (lnText.length > 0 && lnText[0] === '-') { + baseFromLine = baseFromLine + 1; + fromLine = baseFromLine.toString(); + fromLines.push(fromLine); + ++nbDeletions; + diffFromStr += (lnText + '\n'); + ++linesOffset; + // line added in the to file + } else if (lnText.length > 0 && lnText[0] === '+') { + baseToLine = baseToLine + 1; + toLine = baseToLine.toString(); + toLines.push(toLine); + ++nbAdditions; + diffToStr += (lnText + '\n'); + --linesOffset; + // line present in both files + } else { + baseFromLine = baseFromLine + 1; + baseToLine = baseToLine + 1; + fromLine = baseFromLine.toString(); + toLine = baseToLine.toString(); + for (let j = 0; j < Math.abs(linesOffset); ++j) { + if (linesOffset > 0) { + diffToStr += '\n'; toLines.push(''); - // line removed in the from file - } else if (lnText.length > 0 && lnText[0] === '-') { - baseFromLine = baseFromLine + 1; - fromLine = baseFromLine.toString(); - fromLines.push(fromLine); - ++nbDeletions; - diffFromStr += (lnText + '\n'); - ++linesOffset; - // line added in the to file - } else if (lnText.length > 0 && lnText[0] === '+') { - baseToLine = baseToLine + 1; - toLine = baseToLine.toString(); - toLines.push(toLine); - ++nbAdditions; - diffToStr += (lnText + '\n'); - --linesOffset; - // line present in both files } else { - baseFromLine = baseFromLine + 1; - baseToLine = baseToLine + 1; - fromLine = baseFromLine.toString(); - toLine = baseToLine.toString(); - for (let j = 0; j < Math.abs(linesOffset); ++j) { - if (linesOffset > 0) { - diffToStr += '\n'; - toLines.push(''); - } else { - diffFromStr += '\n'; - fromLines.push(''); - } - } - linesOffset = 0; - diffFromStr += (lnText + '\n'); - diffToStr += (lnText + '\n'); - toLines.push(toLine); - fromLines.push(fromLine); - } - if (!baseFromLine) { - fromLine = ''; - } - if (!baseToLine) { - toLine = ''; + diffFromStr += '\n'; + fromLines.push(''); } - fromToLines[i] = [fromLine, toLine]; - maxNumberChars = Math.max(maxNumberChars, fromLine.length); - maxNumberChars = Math.max(maxNumberChars, toLine.length); - }); + } + linesOffset = 0; + diffFromStr += (lnText + '\n'); + diffToStr += (lnText + '\n'); + toLines.push(toLine); + fromLines.push(fromLine); + } + if (!baseFromLine) { + fromLine = ''; + } + if (!baseToLine) { + toLine = ''; + } + fromToLines[i] = [fromLine, toLine]; + maxNumberChars = Math.max(maxNumberChars, fromLine.length); + maxNumberChars = Math.max(maxNumberChars, toLine.length); + }); - diffMaxNumberChars[diffId] = maxNumberChars; + diffMaxNumberChars[diffId] = maxNumberChars; - // set side-by-side diffs text - $(`#${diffId}-from`).text(diffFromStr); - $(`#${diffId}-to`).text(diffToStr); + // set side-by-side diffs text + $(`#${diffId}-from`).text(diffFromStr); + $(`#${diffId}-to`).text(diffToStr); - // code highlighting for side-by-side diffs - $(`#${diffId}-from, #${diffId}-to`).each((i, elt) => { - hljs.highlightElement(elt); - hljs.lineNumbersElementSync(elt); - }); + // code highlighting for side-by-side diffs + $(`#${diffId}-from, #${diffId}-to`).each((i, elt) => { + hljs.highlightElement(elt); + hljs.lineNumbersElementSync(elt); + }); - // diff highlighting for added/removed lines on top of code highlighting - $(`.${diffId} .hljs-ln-numbers`).each((i, lnElt) => { - let lnText = lnElt.nextSibling.innerText; - if (lnText.startsWith('@@')) { - $(lnElt).parent().addClass('swh-diff-lines-info'); - let linesInfoText = $(lnElt).parent().find('.hljs-ln-code .hljs-ln-line').text(); - $(lnElt).parent().find('.hljs-ln-code .hljs-ln-line').children().remove(); - $(lnElt).parent().find('.hljs-ln-code .hljs-ln-line').text(''); - $(lnElt).parent().find('.hljs-ln-code .hljs-ln-line').append(`${linesInfoText}`); - } else if (lnText.length > 0 && lnText[0] === '-') { - $(lnElt).parent().addClass('swh-diff-removed-line'); - } else if (lnText.length > 0 && lnText[0] === '+') { - $(lnElt).parent().addClass('swh-diff-added-line'); - } - }); + // diff highlighting for added/removed lines on top of code highlighting + $(`.${diffId} .hljs-ln-numbers`).each((i, lnElt) => { + let lnText = lnElt.nextSibling.innerText; + if (lnText.startsWith('@@')) { + $(lnElt).parent().addClass('swh-diff-lines-info'); + let linesInfoText = $(lnElt).parent().find('.hljs-ln-code .hljs-ln-line').text(); + $(lnElt).parent().find('.hljs-ln-code .hljs-ln-line').children().remove(); + $(lnElt).parent().find('.hljs-ln-code .hljs-ln-line').text(''); + $(lnElt).parent().find('.hljs-ln-code .hljs-ln-line').append(`${linesInfoText}`); + } else if (lnText.length > 0 && lnText[0] === '-') { + $(lnElt).parent().addClass('swh-diff-removed-line'); + } else if (lnText.length > 0 && lnText[0] === '+') { + $(lnElt).parent().addClass('swh-diff-added-line'); + } + }); - // set line numbers for unified diff - $(`#${diffId} .hljs-ln-numbers`).each((i, lnElt) => { - const lineNumbers = formatDiffLineNumbers(diffId, fromToLines[i][0], fromToLines[i][1]); - setLineNumbers(lnElt, lineNumbers); - }); + // set line numbers for unified diff + $(`#${diffId} .hljs-ln-numbers`).each((i, lnElt) => { + const lineNumbers = formatDiffLineNumbers(diffId, fromToLines[i][0], fromToLines[i][1]); + setLineNumbers(lnElt, lineNumbers); + }); - // set line numbers for the from side-by-side diff - $(`#${diffId}-from .hljs-ln-numbers`).each((i, lnElt) => { - setLineNumbers(lnElt, fromLines[i]); - }); + // set line numbers for the from side-by-side diff + $(`#${diffId}-from .hljs-ln-numbers`).each((i, lnElt) => { + setLineNumbers(lnElt, fromLines[i]); + }); - // set line numbers for the to side-by-side diff - $(`#${diffId}-to .hljs-ln-numbers`).each((i, lnElt) => { - setLineNumbers(lnElt, toLines[i]); - }); + // set line numbers for the to side-by-side diff + $(`#${diffId}-to .hljs-ln-numbers`).each((i, lnElt) => { + setLineNumbers(lnElt, toLines[i]); + }); - // last processing: - // - remove the '+' and '-' at the beginning of the diff lines - // from code highlighting - // - add the "no new line at end of file marker" if needed - $(`.${diffId} .hljs-ln-code`).each((i, lnElt) => { - if (lnElt.firstChild) { - if (lnElt.firstChild.nodeName !== '#text') { - let lineText = lnElt.firstChild.innerHTML; - if (lineText[0] === '-' || lineText[0] === '+') { - lnElt.firstChild.innerHTML = lineText.substr(1); - let newTextNode = document.createTextNode(lineText[0]); - $(lnElt).prepend(newTextNode); - } - } - $(lnElt).contents().filter((i, elt) => { - return elt.nodeType === 3; // Node.TEXT_NODE - }).each((i, textNode) => { - let swhNoNewLineMarker = '[swh-no-nl-marker]'; - if (textNode.textContent.indexOf(swhNoNewLineMarker) !== -1) { - textNode.textContent = textNode.textContent.replace(swhNoNewLineMarker, ''); - $(lnElt).append($(noNewLineMarker)); - } - }); + // last processing: + // - remove the '+' and '-' at the beginning of the diff lines + // from code highlighting + // - add the "no new line at end of file marker" if needed + $(`.${diffId} .hljs-ln-code`).each((i, lnElt) => { + if (lnElt.firstChild) { + if (lnElt.firstChild.nodeName !== '#text') { + let lineText = lnElt.firstChild.innerHTML; + if (lineText[0] === '-' || lineText[0] === '+') { + lnElt.firstChild.innerHTML = lineText.substr(1); + let newTextNode = document.createTextNode(lineText[0]); + $(lnElt).prepend(newTextNode); + } + } + $(lnElt).contents().filter((i, elt) => { + return elt.nodeType === 3; // Node.TEXT_NODE + }).each((i, textNode) => { + let swhNoNewLineMarker = '[swh-no-nl-marker]'; + if (textNode.textContent.indexOf(swhNoNewLineMarker) !== -1) { + textNode.textContent = textNode.textContent.replace(swhNoNewLineMarker, ''); + $(lnElt).append($(noNewLineMarker)); } }); + } + }); - // hide the diff mode switch button in case of not generated diffs - if (data.diff_str.indexOf('Diffs are not generated for non textual content') !== 0) { - $(`#diff_${diffId} .diff-styles`).css('visibility', 'visible'); - } + // hide the diff mode switch button in case of not generated diffs + if (data.diff_str.indexOf('Diffs are not generated for non textual content') !== 0) { + $(`#diff_${diffId} .diff-styles`).css('visibility', 'visible'); + } - setDiffVisible(diffId); + setDiffVisible(diffId); - // highlight diff lines if provided in URL fragment - if (selectedDiffLinesInfo && + // highlight diff lines if provided in URL fragment + if (selectedDiffLinesInfo && selectedDiffLinesInfo.diffPanelId.indexOf(diffId) !== -1) { - if (!selectedDiffLinesInfo.unified) { - showSplitDiff(diffId); - } - const firstHighlightedLine = highlightDiffLines( - diffId, selectedDiffLinesInfo.startLines, - selectedDiffLinesInfo.endLines, selectedDiffLinesInfo.unified); - - $('html, body').animate( - { - scrollTop: firstHighlightedLine.offset().top - 50 - }, - { - duration: 500 - } - ); - } + if (!selectedDiffLinesInfo.unified) { + showSplitDiff(diffId); } - }); + const firstHighlightedLine = highlightDiffLines( + diffId, selectedDiffLinesInfo.startLines, + selectedDiffLinesInfo.endLines, selectedDiffLinesInfo.unified); + + $('html, body').animate( + { + scrollTop: firstHighlightedLine.offset().top - 50 + }, + { + duration: 500 + } + ); + } + } + } function setDiffVisible(diffId) { // set the unified diff visible by default $(`#${diffId}-loading`).css('display', 'none'); $(`#${diffId}-highlightjs`).css('display', 'block'); // update displayed counters $('#swh-revision-lines-added').text(`${nbAdditions} additions`); $('#swh-revision-lines-deleted').text(`${nbDeletions} deletions`); $('#swh-nb-diffs-computed').text(nbDiffsComputed); // refresh the waypoints triggering diffs computation as // the DOM layout has been updated Waypoint.refreshAll(); } // to compute all visible diffs in the viewport function computeVisibleDiffs() { $('.swh-file-diff-panel').each((i, elt) => { if (isInViewport(elt)) { let diffId = elt.id.replace('diff_', ''); computeDiff(diffsUrls[diffId], diffId); } }); } function genDiffPanel(diffData) { let diffPanelTitle = diffData.path; if (diffData.type === 'rename') { diffPanelTitle = `${diffData.from_path} → ${diffData.to_path}`; } return diffPanelTemplate({ diffData: diffData, diffPanelTitle: diffPanelTitle, swhSpinnerSrc: swhSpinnerSrc }); } // setup waypoints to request diffs computation on the fly while scrolling function setupWaypoints() { for (let i = 0; i < changes.length; ++i) { let diffData = changes[i]; // create a waypoint that will trigger diff computation when // the top of the diff panel hits the bottom of the viewport $(`#diff_${diffData.id}`).waypoint({ handler: function() { if (isInViewport(this.element)) { let diffId = this.element.id.replace('diff_', ''); computeDiff(diffsUrls[diffId], diffId); this.destroy(); } }, offset: '100%' }); // create a waypoint that will trigger diff computation when // the bottom of the diff panel hits the top of the viewport $(`#diff_${diffData.id}`).waypoint({ handler: function() { if (isInViewport(this.element)) { let diffId = this.element.id.replace('diff_', ''); computeDiff(diffsUrls[diffId], diffId); this.destroy(); } }, offset: function() { return -$(this.element).height(); } }); } Waypoint.refreshAll(); } function scrollToDiffPanel(diffPanelId, setHash = true) { // disable waypoints while scrolling as we do not want to // launch computation of diffs the user is not interested in // (file changes list can be large) Waypoint.disableAll(); $('html, body').animate( { scrollTop: $(diffPanelId).offset().top }, { duration: 500, complete: () => { if (setHash) { window.location.hash = diffPanelId; } // enable waypoints back after scrolling Waypoint.enableAll(); // compute diffs visible in the viewport computeVisibleDiffs(); } }); } // callback when the user clicks on the 'Compute all diffs' button export function computeAllDiffs(event) { $(event.currentTarget).addClass('active'); for (let diffId in diffsUrls) { if (diffsUrls.hasOwnProperty(diffId)) { computeDiff(diffsUrls[diffId], diffId); } } event.stopPropagation(); } export async function initRevisionDiff(revisionMessageBody, diffRevisionUrl) { await import(/* webpackChunkName: "highlightjs" */ 'utils/highlightjs'); // callback when the 'Changes' tab is activated - $(document).on('shown.bs.tab', 'a[data-toggle="tab"]', e => { + $(document).on('shown.bs.tab', 'a[data-toggle="tab"]', async e => { currentTabName = e.currentTarget.text.trim(); if (currentTabName === 'Changes') { window.location.hash = changesUrlFragment; $('#readme-panel').css('display', 'none'); if (changes) { return; } // request computation of revision file changes list // when navigating to the 'Changes' tab and add diff panels // to the DOM when receiving the result - fetch(diffRevisionUrl) - .then(response => response.json()) - .then(data => { - changes = data.changes; - nbChangedFiles = data.total_nb_changes; - let changedFilesText = `${nbChangedFiles} changed file`; - if (nbChangedFiles !== 1) { - changedFilesText += 's'; - } - $('#swh-revision-changed-files').text(changedFilesText); - $('#swh-total-nb-diffs').text(changes.length); - $('#swh-revision-changes-list pre')[0].innerHTML = data.changes_msg; - - $('#swh-revision-changes-loading').css('display', 'none'); - $('#swh-revision-changes-list pre').css('display', 'block'); - $('#swh-compute-all-diffs').css('visibility', 'visible'); - $('#swh-revision-changes-list').removeClass('in'); - - if (nbChangedFiles > changes.length) { - $('#swh-too-large-revision-diff').css('display', 'block'); - $('#swh-nb-loaded-diffs').text(changes.length); - } + const response = await fetch(diffRevisionUrl); + const data = await response.json(); + + changes = data.changes; + nbChangedFiles = data.total_nb_changes; + let changedFilesText = `${nbChangedFiles} changed file`; + if (nbChangedFiles !== 1) { + changedFilesText += 's'; + } + $('#swh-revision-changed-files').text(changedFilesText); + $('#swh-total-nb-diffs').text(changes.length); + $('#swh-revision-changes-list pre')[0].innerHTML = data.changes_msg; + + $('#swh-revision-changes-loading').css('display', 'none'); + $('#swh-revision-changes-list pre').css('display', 'block'); + $('#swh-compute-all-diffs').css('visibility', 'visible'); + $('#swh-revision-changes-list').removeClass('in'); + + if (nbChangedFiles > changes.length) { + $('#swh-too-large-revision-diff').css('display', 'block'); + $('#swh-nb-loaded-diffs').text(changes.length); + } - for (let i = 0; i < changes.length; ++i) { - let diffData = changes[i]; - diffsUrls[diffData.id] = diffData.diff_url; - $('#swh-revision-diffs').append(genDiffPanel(diffData)); - } + for (let i = 0; i < changes.length; ++i) { + let diffData = changes[i]; + diffsUrls[diffData.id] = diffData.diff_url; + $('#swh-revision-diffs').append(genDiffPanel(diffData)); + } - setupWaypoints(); - computeVisibleDiffs(); + setupWaypoints(); + computeVisibleDiffs(); - if (selectedDiffLinesInfo) { - scrollToDiffPanel(selectedDiffLinesInfo.diffPanelId, false); - } + if (selectedDiffLinesInfo) { + scrollToDiffPanel(selectedDiffLinesInfo.diffPanelId, false); + } - }); } else if (currentTabName === 'Files') { removeUrlFragment(); $('#readme-panel').css('display', 'block'); } }); $(document).ready(() => { if (revisionMessageBody.length > 0) { $('#swh-revision-message').addClass('in'); } else { $('#swh-collapse-revision-message').attr('data-toggle', ''); } // callback when the user requests to scroll on a specific diff or back to top $('#swh-revision-changes-list a[href^="#"], #back-to-top a[href^="#"]').click(e => { let href = $.attr(e.currentTarget, 'href'); scrollToDiffPanel(href); return false; }); // click callback for highlighting diff lines $('body').click(evt => { if (currentTabName !== 'Changes') { return; } if (evt.target.classList.contains('hljs-ln-n')) { const diffId = $(evt.target).closest('code').prop('id'); const from = diffId.indexOf('-from') !== -1; const to = diffId.indexOf('-to') !== -1; const lineNumbers = $(evt.target).data('line-number').toString(); const currentDiff = diffId.replace('-from', '').replace('-to', ''); if (!evt.shiftKey || currentDiff !== focusedDiff || !lineNumbers.trim()) { resetHighlightedDiffLines(); focusedDiff = currentDiff; } if (currentDiff === focusedDiff && lineNumbers.trim()) { if (!evt.shiftKey) { startLines = parseDiffLineNumbers(lineNumbers, from, to); highlightDiffLines(currentDiff, startLines, startLines, !from && !to); } else if (startLines) { resetHighlightedDiffLines(false); endLines = parseDiffLineNumbers(lineNumbers, from, to); highlightDiffLines(currentDiff, startLines, endLines, !from && !to); } } } else { resetHighlightedDiffLines(); } }); // if an URL fragment for highlighting a diff is present // parse highlighting info and initiate diff loading const fragment = window.location.hash; if (fragment) { const split = fragment.split('+'); if (split.length === 2) { selectedDiffLinesInfo = fragmentToSelectedDiffLines(split[1]); if (selectedDiffLinesInfo) { selectedDiffLinesInfo.diffPanelId = split[0]; $(`.nav-tabs a[href="${changesUrlFragment}"]`).tab('show'); } } if (fragment === changesUrlFragment) { $(`.nav-tabs a[href="${changesUrlFragment}"]`).tab('show'); } } }); } diff --git a/assets/src/bundles/save/index.js b/assets/src/bundles/save/index.js index 857493d1..c2e901f6 100644 --- a/assets/src/bundles/save/index.js +++ b/assets/src/bundles/save/index.js @@ -1,567 +1,563 @@ /** * Copyright (C) 2018-2021 The Software Heritage developers * See the AUTHORS file at the top-level directory of this distribution * License: GNU Affero General Public License version 3, or any later version * See top-level LICENSE file for more information */ import {csrfPost, handleFetchError, isGitRepoUrl, htmlAlert, removeUrlFragment} from 'utils/functions'; import {swhSpinnerSrc} from 'utils/constants'; import artifactFormRowTemplate from './artifact-form-row.ejs'; let saveRequestsTable; -function originSaveRequest( +async function originSaveRequest( originType, originUrl, extraData, acceptedCallback, pendingCallback, errorCallback ) { // Actually trigger the origin save request let 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' }; }; - csrfPost(addSaveOriginRequestUrl, headers, body) - .then(handleFetchError) - .then(response => response.json()) - .then(data => { - $('.swh-processing-save-request').css('display', 'none'); - if (data.save_request_status === 'accepted') { - acceptedCallback(); - } else { - pendingCallback(); - } - }) - .catch(response => { - $('.swh-processing-save-request').css('display', 'none'); - response.json().then(errorData => { - errorCallback(response.status, errorData); - }); - }); + try { + const response = await csrfPost(addSaveOriginRequestUrl, headers, body); + handleFetchError(response); + const data = await response.json(); + $('.swh-processing-save-request').css('display', 'none'); + if (data.save_request_status === 'accepted') { + acceptedCallback(); + } else { + pendingCallback(); + } + } catch (response) { + $('.swh-processing-save-request').css('display', 'none'); + const errorData = await response.json(); + errorCallback(response.status, errorData); + }; } function addArtifactVersionAutofillHandler(formId) { // autofill artifact version input with the filename from // the artifact url without extensions $(`#swh-input-artifact-url-${formId}`).on('input', function(event) { const artifactUrl = $(this).val().trim(); let filename = artifactUrl.split('/').slice(-1)[0]; if (filename !== artifactUrl) { filename = filename.replace(/tar.*$/, 'tar'); const filenameNoExt = filename.split('.').slice(0, -1).join('.'); const artifactVersion = $(`#swh-input-artifact-version-${formId}`); if (filenameNoExt !== filename) { artifactVersion.val(filenameNoExt); } } }); } export function maybeRequireExtraInputs() { // Read the actual selected value and depending on the origin type, display some extra // inputs or hide them. This makes the extra inputs disabled when not displayed. const originType = $('#swh-input-visit-type').val(); let display = 'none'; let disabled = true; if (originType === 'archives') { display = 'flex'; disabled = false; } $('.swh-save-origin-archives-form').css('display', display); if (!disabled) { // help paragraph must have block display for proper rendering $('#swh-save-origin-archives-help').css('display', 'block'); } $('.swh-save-origin-archives-form .form-control').prop('disabled', disabled); if (originType === 'archives' && $('.swh-save-origin-archives-form').length === 1) { // insert first artifact row when the archives visit type is selected for the first time $('.swh-save-origin-archives-form').last().after( artifactFormRowTemplate({deletableRow: false, formId: 0})); addArtifactVersionAutofillHandler(0); } } export function addArtifactFormRow() { const formId = $('.swh-save-origin-artifact-form').length; $('.swh-save-origin-artifact-form').last().after( artifactFormRowTemplate({ deletableRow: true, formId: formId }) ); addArtifactVersionAutofillHandler(formId); } export function deleteArtifactFormRow(event) { $(event.target).closest('.swh-save-origin-artifact-form').remove(); } const userRequestsFilterCheckbox = `
`; export function initOriginSave() { - $(document).ready(() => { + $(document).ready(async() => { $.fn.dataTable.ext.errMode = 'none'; - fetch(Urls.origin_save_types_list()) - .then(response => response.json()) - .then(data => { - for (let originType of data) { - $('#swh-input-visit-type').append(``); - } - // set git as the default value as before - $('#swh-input-visit-type').val('git'); - }); + const response = await fetch(Urls.origin_save_types_list()); + const data = await response.json(); + + for (let originType of data) { + $('#swh-input-visit-type').append(``); + } + // set git as the default value as before + $('#swh-input-visit-type').val('git'); saveRequestsTable = $('#swh-origin-save-requests') .on('error.dt', (e, settings, techNote, message) => { $('#swh-origin-save-request-list-error').text('An error occurred while retrieving the save requests list'); console.log(message); }) .DataTable({ serverSide: true, processing: true, language: { processing: `` }, ajax: { url: Urls.origin_save_requests_list('all'), data: (d) => { if (swh.webapp.isUserLoggedIn() && $('#swh-save-requests-user-filter').prop('checked')) { d.user_requests_only = '1'; } } }, searchDelay: 1000, // see https://datatables.net/examples/advanced_init/dom_toolbar.html and the comments section // this option customizes datatables UI components by adding an extra checkbox above the table // while keeping bootstrap layout dom: '<"row"<"col-sm-3"l><"col-sm-6 text-left user-requests-filter"><"col-sm-3"f>>' + '<"row"<"col-sm-12"tr>>' + '<"row"<"col-sm-5"i><"col-sm-7"p>>', fnInitComplete: function() { if (swh.webapp.isUserLoggedIn()) { $('div.user-requests-filter').html(userRequestsFilterCheckbox); $('#swh-save-requests-user-filter').on('change', () => { saveRequestsTable.draw(); }); } }, columns: [ { data: 'save_request_date', name: 'request_date', render: (data, type, row) => { if (type === 'display') { let date = new Date(data); return date.toLocaleString(); } return data; } }, { data: 'visit_type', name: 'visit_type' }, { data: 'origin_url', name: 'origin_url', render: (data, type, row) => { if (type === 'display') { let html = ''; const sanitizedURL = $.fn.dataTable.render.text().display(data); if (row.save_task_status === 'succeeded') { let browseOriginUrl = `${Urls.browse_origin()}?origin_url=${encodeURIComponent(sanitizedURL)}`; if (row.visit_date) { browseOriginUrl += `&timestamp=${encodeURIComponent(row.visit_date)}`; } html += `${sanitizedURL}`; } else { html += sanitizedURL; } html += ` `; return html; } return data; } }, { data: 'save_request_status', name: 'status' }, { data: 'save_task_status', name: 'loading_task_status' }, { name: 'info', render: (data, type, row) => { if (row.save_task_status === 'succeeded' || row.save_task_status === 'failed') { return ``; } else { return ''; } } }, { render: (data, type, row) => { if (row.save_request_status === 'accepted') { const saveAgainButton = ''; return saveAgainButton; } else { return ''; } } } ], scrollY: '50vh', scrollCollapse: true, order: [[0, 'desc']], responsive: { details: { type: 'none' } } }); swh.webapp.addJumpToPagePopoverToDataTable(saveRequestsTable); $('#swh-origin-save-requests-list-tab').on('shown.bs.tab', () => { saveRequestsTable.draw(); window.location.hash = '#requests'; }); $('#swh-origin-save-request-help-tab').on('shown.bs.tab', () => { removeUrlFragment(); $('.swh-save-request-info').popover('dispose'); }); let saveRequestAcceptedAlert = htmlAlert( 'success', 'The "save code now" request has been accepted and will be processed as soon as possible.', true ); let saveRequestPendingAlert = htmlAlert( 'warning', 'The "save code now" request has been put in pending state and may be accepted for processing after manual review.', true ); let saveRequestRateLimitedAlert = htmlAlert( 'danger', 'The rate limit for "save code now" requests has been reached. Please try again later.', true ); let saveRequestUnknownErrorAlert = htmlAlert( 'danger', 'An unexpected error happened when submitting the "save code now request".', true ); $('#swh-save-origin-form').submit(event => { event.preventDefault(); event.stopPropagation(); $('.alert').alert('close'); if (event.target.checkValidity()) { $(event.target).removeClass('was-validated'); let originType = $('#swh-input-visit-type').val(); let originUrl = $('#swh-input-origin-url').val(); // read the extra inputs for the 'archives' type let 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) { let originUrl = $(this).val().trim(); $(this).val(originUrl); $('#swh-input-visit-type option').each(function() { let val = $(this).val(); if (val && originUrl.includes(val)) { $(this).prop('selected', true); } }); }); if (window.location.hash === '#requests') { $('.nav-tabs a[href="#swh-origin-save-requests-list"]').tab('show'); } }); } export function validateSaveOriginUrl(input) { let 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) { let allowedProtocols = ['http:', 'https:', 'svn:', 'git:']; validUrl = ( allowedProtocols.find(protocol => protocol === originUrl.protocol) !== undefined ); } if (validUrl && originType === 'git') { // additional checks for well known code hosting providers switch (originUrl.hostname) { case 'github.com': validUrl = isGitRepoUrl(originUrl); break; case 'git.code.sf.net': validUrl = isGitRepoUrl(originUrl, '/p/'); break; case 'bitbucket.org': validUrl = isGitRepoUrl(originUrl); break; default: if (originUrl.hostname.startsWith('gitlab.')) { validUrl = isGitRepoUrl(originUrl); } break; } } if (validUrl) { input.setCustomValidity(''); } else { input.setCustomValidity('The origin url is not valid or does not reference a code repository'); } } export function initTakeNewSnapshot() { let newSnapshotRequestAcceptedAlert = htmlAlert( 'success', 'The "take new snapshot" request has been accepted and will be processed as soon as possible.', true ); let newSnapshotRequestPendingAlert = htmlAlert( 'warning', 'The "take new snapshot" request has been put in pending state and may be accepted for processing after manual review.', true ); let newSnapshotRequestRateLimitAlert = htmlAlert( 'danger', 'The rate limit for "take new snapshot" requests has been reached. Please try again later.', true ); let 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(); let originType = $('#swh-input-visit-type').val(); let originUrl = $('#swh-input-origin-url').val(); let 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 function displaySaveRequestInfo(event, saveRequestId) { +export async function displaySaveRequestInfo(event, saveRequestId) { event.stopPropagation(); const saveRequestTaskInfoUrl = Urls.origin_save_task_info(saveRequestId); // close popover when clicking again on the info icon if ($(event.target).data('bs.popover')) { $(event.target).popover('dispose'); return; } $('.swh-save-request-info').popover('dispose'); $(event.target).popover({ animation: false, boundary: 'viewport', container: 'body', title: 'Save request task information ' + '`, content: `

Fetching task information ...

`, 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'); - fetch(saveRequestTaskInfoUrl) - .then(response => response.json()) - .then(saveRequestTaskInfo => { - let content; - if ($.isEmptyObject(saveRequestTaskInfo)) { - content = 'Not available'; - } else { - let 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 = ''; - for (let info of saveRequestInfo) { - content += + const response = await fetch(saveRequestTaskInfoUrl); + const saveRequestTaskInfo = await response.json(); + + let content; + if ($.isEmptyObject(saveRequestTaskInfo)) { + content = 'Not available'; + } else { + let 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 = '
'; + for (let info of saveRequestInfo) { + content += ``; - } - content += '
'; - } - $('.swh-popover').html(content); - $(event.target).popover('update'); - }); + } + content += ''; + } + $('.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() { let val = $(this).val(); if (val && originUrl.includes(val)) { $(this).prop('selected', true); originTypeFound = true; } }); if (!originTypeFound) { $('#swh-input-visit-type option').each(function() { let val = $(this).val(); if (val === visitType) { $(this).prop('selected', true); } }); } window.scrollTo(0, 0); } diff --git a/assets/src/bundles/vault/vault-create-tasks.js b/assets/src/bundles/vault/vault-create-tasks.js index 5fab1b1c..c043a90e 100644 --- a/assets/src/bundles/vault/vault-create-tasks.js +++ b/assets/src/bundles/vault/vault-create-tasks.js @@ -1,161 +1,155 @@ /** * Copyright (C) 2018-2019 The Software Heritage developers * See the AUTHORS file at the top-level directory of this distribution * License: GNU Affero General Public License version 3, or any later version * See top-level LICENSE file for more information */ import {handleFetchError, csrfPost, htmlAlert} from 'utils/functions'; const alertStyle = { 'position': 'fixed', 'left': '1rem', 'bottom': '1rem', 'z-index': '100000' }; -export function vaultRequest(objectType, objectId) { +export async function vaultRequest(objectType, objectId) { let vaultUrl; if (objectType === 'directory') { vaultUrl = Urls.api_1_vault_cook_directory(objectId); } else { vaultUrl = Urls.api_1_vault_cook_revision_gitfast(objectId); } // check if object has already been cooked - fetch(vaultUrl) - .then(response => response.json()) - .then(data => { - // object needs to be cooked - if (data.exception === 'NotFoundExc' || data.status === 'failed') { - // if last cooking has failed, remove previous task info from localStorage - // in order to force the recooking of the object - swh.vault.removeCookingTaskInfo([objectId]); - $(`#vault-cook-${objectType}-modal`).modal('show'); - // object has been cooked and should be in the vault cache, - // it will be asked to cook it again if it is not - } else if (data.status === 'done') { - $(`#vault-fetch-${objectType}-modal`).modal('show'); - } else { - const cookingServiceDownAlert = + const response = await fetch(vaultUrl); + const data = await response.json(); + + // object needs to be cooked + if (data.exception === 'NotFoundExc' || data.status === 'failed') { + // if last cooking has failed, remove previous task info from localStorage + // in order to force the recooking of the object + swh.vault.removeCookingTaskInfo([objectId]); + $(`#vault-cook-${objectType}-modal`).modal('show'); + // object has been cooked and should be in the vault cache, + // it will be asked to cook it again if it is not + } else if (data.status === 'done') { + $(`#vault-fetch-${objectType}-modal`).modal('show'); + } else { + const cookingServiceDownAlert = $(htmlAlert('danger', 'Archive cooking service is currently experiencing issues.
' + 'Please try again later.', true)); - cookingServiceDownAlert.css(alertStyle); - $('body').append(cookingServiceDownAlert); - } - }); + cookingServiceDownAlert.css(alertStyle); + $('body').append(cookingServiceDownAlert); + } } -function addVaultCookingTask(cookingTask) { +async function addVaultCookingTask(cookingTask) { const swhidsContext = swh.webapp.getSwhIdsContext(); cookingTask.origin = swhidsContext[cookingTask.object_type].context.origin; cookingTask.path = swhidsContext[cookingTask.object_type].context.path; cookingTask.browse_url = swhidsContext[cookingTask.object_type].swhid_with_context_url; if (!cookingTask.browse_url) { cookingTask.browse_url = swhidsContext[cookingTask.object_type].swhid_url; } let vaultCookingTasks = JSON.parse(localStorage.getItem('swh-vault-cooking-tasks')); if (!vaultCookingTasks) { vaultCookingTasks = []; } if (vaultCookingTasks.find(val => { return val.object_type === cookingTask.object_type && val.object_id === cookingTask.object_id; }) === undefined) { let cookingUrl; if (cookingTask.object_type === 'directory') { cookingUrl = Urls.api_1_vault_cook_directory(cookingTask.object_id); } else { cookingUrl = Urls.api_1_vault_cook_revision_gitfast(cookingTask.object_id); } if (cookingTask.email) { cookingUrl += '?email=' + cookingTask.email; } - csrfPost(cookingUrl) - .then(handleFetchError) - .then(() => { - vaultCookingTasks.push(cookingTask); - localStorage.setItem('swh-vault-cooking-tasks', JSON.stringify(vaultCookingTasks)); - $('#vault-cook-directory-modal').modal('hide'); - $('#vault-cook-revision-modal').modal('hide'); - const cookingTaskCreatedAlert = + try { + const response = await csrfPost(cookingUrl); + handleFetchError(response); + vaultCookingTasks.push(cookingTask); + localStorage.setItem('swh-vault-cooking-tasks', JSON.stringify(vaultCookingTasks)); + $('#vault-cook-directory-modal').modal('hide'); + $('#vault-cook-revision-modal').modal('hide'); + const cookingTaskCreatedAlert = $(htmlAlert('success', 'Archive cooking request successfully submitted.
' + `Go to the Downloads page ` + 'to get the download link once it is ready.', true)); - cookingTaskCreatedAlert.css(alertStyle); - $('body').append(cookingTaskCreatedAlert); - }) - .catch(() => { - $('#vault-cook-directory-modal').modal('hide'); - $('#vault-cook-revision-modal').modal('hide'); - const cookingTaskFailedAlert = + cookingTaskCreatedAlert.css(alertStyle); + $('body').append(cookingTaskCreatedAlert); + } catch (_) { + $('#vault-cook-directory-modal').modal('hide'); + $('#vault-cook-revision-modal').modal('hide'); + const cookingTaskFailedAlert = $(htmlAlert('danger', 'Archive cooking request submission failed.', true)); - cookingTaskFailedAlert.css(alertStyle); - $('body').append(cookingTaskFailedAlert); - }); + cookingTaskFailedAlert.css(alertStyle); + $('body').append(cookingTaskFailedAlert); + } } } function validateEmail(email) { let re = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; return re.test(String(email).toLowerCase()); } export function cookDirectoryArchive(directoryId) { let email = $('#swh-vault-directory-email').val().trim(); if (!email || validateEmail(email)) { let cookingTask = { 'object_type': 'directory', 'object_id': directoryId, 'email': email, 'status': 'new' }; addVaultCookingTask(cookingTask); } else { $('#invalid-email-modal').modal('show'); } } -export function fetchDirectoryArchive(directoryId) { +export async function fetchDirectoryArchive(directoryId) { $('#vault-fetch-directory-modal').modal('hide'); const vaultUrl = Urls.api_1_vault_cook_directory(directoryId); - fetch(vaultUrl) - .then(response => response.json()) - .then(data => { - swh.vault.fetchCookedObject(data.fetch_url); - }); + const response = await fetch(vaultUrl); + const data = await response.json(); + swh.vault.fetchCookedObject(data.fetch_url); } export function cookRevisionArchive(revisionId) { let email = $('#swh-vault-revision-email').val().trim(); if (!email || validateEmail(email)) { let cookingTask = { 'object_type': 'revision', 'object_id': revisionId, 'email': email, 'status': 'new' }; addVaultCookingTask(cookingTask); } else { $('#invalid-email-modal').modal('show'); } } -export function fetchRevisionArchive(revisionId) { +export async function fetchRevisionArchive(revisionId) { $('#vault-fetch-directory-modal').modal('hide'); const vaultUrl = Urls.api_1_vault_cook_revision_gitfast(revisionId); - fetch(vaultUrl) - .then(response => response.json()) - .then(data => { - swh.vault.fetchCookedObject(data.fetch_url); - }); + const response = await fetch(vaultUrl); + const data = await response.json(); + swh.vault.fetchCookedObject(data.fetch_url); } diff --git a/assets/src/bundles/vault/vault-ui.js b/assets/src/bundles/vault/vault-ui.js index 9d756bc9..22ca093c 100644 --- a/assets/src/bundles/vault/vault-ui.js +++ b/assets/src/bundles/vault/vault-ui.js @@ -1,241 +1,241 @@ /** * Copyright (C) 2018-2019 The Software Heritage developers * See the AUTHORS file at the top-level directory of this distribution * License: GNU Affero General Public License version 3, or any later version * See top-level LICENSE file for more information */ import {handleFetchError, handleFetchErrors, csrfPost} from 'utils/functions'; import vaultTableRowTemplate from './vault-table-row.ejs'; let progress = `
;`; let pollingInterval = 5000; let checkVaultId; function updateProgressBar(progressBar, cookingTask) { if (cookingTask.status === 'new') { progressBar.css('background-color', 'rgba(128, 128, 128, 0.5)'); } else if (cookingTask.status === 'pending') { progressBar.css('background-color', 'rgba(0, 0, 255, 0.5)'); } else if (cookingTask.status === 'done') { progressBar.css('background-color', '#5cb85c'); } else if (cookingTask.status === 'failed') { progressBar.css('background-color', 'rgba(255, 0, 0, 0.5)'); progressBar.css('background-image', 'none'); } progressBar.text(cookingTask.progress_message || cookingTask.status); if (cookingTask.status === 'new' || cookingTask.status === 'pending') { progressBar.addClass('progress-bar-animated'); } else { progressBar.removeClass('progress-bar-striped'); } } let recookTask; // called when the user wants to download a cooked archive -export function fetchCookedObject(fetchUrl) { +export async function fetchCookedObject(fetchUrl) { recookTask = null; // first, check if the link is still available from the vault - fetch(fetchUrl) - .then(response => { - // link is still alive, proceed to download - if (response.ok) { - $('#vault-fetch-iframe').attr('src', fetchUrl); - // link is dead - } else { - // get the associated cooking task - let vaultCookingTasks = JSON.parse(localStorage.getItem('swh-vault-cooking-tasks')); - for (let i = 0; i < vaultCookingTasks.length; ++i) { - if (vaultCookingTasks[i].fetch_url === fetchUrl) { - recookTask = vaultCookingTasks[i]; - break; - } - } - // display a modal asking the user if he wants to recook the archive - $('#vault-recook-object-modal').modal('show'); + const response = await fetch(fetchUrl); + + // link is still alive, proceed to download + if (response.ok) { + $('#vault-fetch-iframe').attr('src', fetchUrl); + // link is dead + } else { + // get the associated cooking task + let vaultCookingTasks = JSON.parse(localStorage.getItem('swh-vault-cooking-tasks')); + for (let i = 0; i < vaultCookingTasks.length; ++i) { + if (vaultCookingTasks[i].fetch_url === fetchUrl) { + recookTask = vaultCookingTasks[i]; + break; } - }); + } + // display a modal asking the user if he wants to recook the archive + $('#vault-recook-object-modal').modal('show'); + } } // called when the user wants to recook an archive // for which the download link is not available anymore -export function recookObject() { +export async function recookObject() { if (recookTask) { // stop cooking tasks status polling clearTimeout(checkVaultId); // build cook request url let cookingUrl; if (recookTask.object_type === 'directory') { cookingUrl = Urls.api_1_vault_cook_directory(recookTask.object_id); } else { cookingUrl = Urls.api_1_vault_cook_revision_gitfast(recookTask.object_id); } if (recookTask.email) { cookingUrl += '?email=' + recookTask.email; } + try { // request archive cooking - csrfPost(cookingUrl) - .then(handleFetchError) - .then(() => { - // update task status - recookTask.status = 'new'; - let vaultCookingTasks = JSON.parse(localStorage.getItem('swh-vault-cooking-tasks')); - for (let i = 0; i < vaultCookingTasks.length; ++i) { - if (vaultCookingTasks[i].object_id === recookTask.object_id) { - vaultCookingTasks[i] = recookTask; - break; - } + const response = await csrfPost(cookingUrl); + handleFetchError(response); + + // update task status + recookTask.status = 'new'; + let vaultCookingTasks = JSON.parse(localStorage.getItem('swh-vault-cooking-tasks')); + for (let i = 0; i < vaultCookingTasks.length; ++i) { + if (vaultCookingTasks[i].object_id === recookTask.object_id) { + vaultCookingTasks[i] = recookTask; + break; } - // save updated tasks to local storage - localStorage.setItem('swh-vault-cooking-tasks', JSON.stringify(vaultCookingTasks)); - // restart cooking tasks status polling - checkVaultCookingTasks(); - // hide recook archive modal - $('#vault-recook-object-modal').modal('hide'); - }) + } + // save updated tasks to local storage + localStorage.setItem('swh-vault-cooking-tasks', JSON.stringify(vaultCookingTasks)); + // restart cooking tasks status polling + checkVaultCookingTasks(); + // hide recook archive modal + $('#vault-recook-object-modal').modal('hide'); + } catch (_) { // something went wrong - .catch(() => { - checkVaultCookingTasks(); - $('#vault-recook-object-modal').modal('hide'); - }); + checkVaultCookingTasks(); + $('#vault-recook-object-modal').modal('hide'); + } } } -function checkVaultCookingTasks() { +async function checkVaultCookingTasks() { let vaultCookingTasks = JSON.parse(localStorage.getItem('swh-vault-cooking-tasks')); if (!vaultCookingTasks || vaultCookingTasks.length === 0) { $('.swh-vault-table tbody tr').remove(); checkVaultId = setTimeout(checkVaultCookingTasks, pollingInterval); return; } let cookingTaskRequests = []; let tasks = {}; let currentObjectIds = []; for (let i = 0; i < vaultCookingTasks.length; ++i) { let cookingTask = vaultCookingTasks[i]; currentObjectIds.push(cookingTask.object_id); tasks[cookingTask.object_id] = cookingTask; let cookingUrl; if (cookingTask.object_type === 'directory') { cookingUrl = Urls.api_1_vault_cook_directory(cookingTask.object_id); } else { cookingUrl = Urls.api_1_vault_cook_revision_gitfast(cookingTask.object_id); } if (cookingTask.status !== 'done' && cookingTask.status !== 'failed') { cookingTaskRequests.push(fetch(cookingUrl)); } } $('.swh-vault-table tbody tr').each((i, row) => { let objectId = $(row).find('.vault-object-info').data('object-id'); if ($.inArray(objectId, currentObjectIds) === -1) { $(row).remove(); } }); - Promise.all(cookingTaskRequests) - .then(handleFetchErrors) - .then(responses => Promise.all(responses.map(r => r.json()))) - .then(cookingTasks => { - let table = $('#vault-cooking-tasks tbody'); - for (let i = 0; i < cookingTasks.length; ++i) { - let cookingTask = tasks[cookingTasks[i].obj_id]; - cookingTask.status = cookingTasks[i].status; - cookingTask.fetch_url = cookingTasks[i].fetch_url; - cookingTask.progress_message = cookingTasks[i].progress_message; - } - for (let i = 0; i < vaultCookingTasks.length; ++i) { - let cookingTask = vaultCookingTasks[i]; - let rowTask = $(`#vault-task-${cookingTask.object_id}`); - - if (!rowTask.length) { - - let browseUrl = cookingTask.browse_url; - if (!browseUrl) { - if (cookingTask.object_type === 'directory') { - browseUrl = Urls.browse_directory(cookingTask.object_id); - } else { - browseUrl = Urls.browse_revision(cookingTask.object_id); - } + try { + const responses = await Promise.all(cookingTaskRequests); + handleFetchErrors(responses); + const cookingTasks = await Promise.all(responses.map(r => r.json())); + + let table = $('#vault-cooking-tasks tbody'); + for (let i = 0; i < cookingTasks.length; ++i) { + let cookingTask = tasks[cookingTasks[i].obj_id]; + cookingTask.status = cookingTasks[i].status; + cookingTask.fetch_url = cookingTasks[i].fetch_url; + cookingTask.progress_message = cookingTasks[i].progress_message; + } + for (let i = 0; i < vaultCookingTasks.length; ++i) { + let cookingTask = vaultCookingTasks[i]; + let rowTask = $(`#vault-task-${cookingTask.object_id}`); + + if (!rowTask.length) { + + let browseUrl = cookingTask.browse_url; + if (!browseUrl) { + if (cookingTask.object_type === 'directory') { + browseUrl = Urls.browse_directory(cookingTask.object_id); + } else { + browseUrl = Urls.browse_revision(cookingTask.object_id); } + } - let progressBar = $.parseHTML(progress)[0]; - let progressBarContent = $(progressBar).find('.progress-bar'); - updateProgressBar(progressBarContent, cookingTask); - table.prepend(vaultTableRowTemplate({ - browseUrl: browseUrl, - cookingTask: cookingTask, - progressBar: progressBar, - Urls: Urls, - swh: swh - })); - } else { - let progressBar = rowTask.find('.progress-bar'); - updateProgressBar(progressBar, cookingTask); - let downloadLink = rowTask.find('.vault-dl-link'); - if (cookingTask.status === 'done') { - downloadLink[0].innerHTML = + let progressBar = $.parseHTML(progress)[0]; + let progressBarContent = $(progressBar).find('.progress-bar'); + updateProgressBar(progressBarContent, cookingTask); + table.prepend(vaultTableRowTemplate({ + browseUrl: browseUrl, + cookingTask: cookingTask, + progressBar: progressBar, + Urls: Urls, + swh: swh + })); + } else { + let progressBar = rowTask.find('.progress-bar'); + updateProgressBar(progressBar, cookingTask); + let downloadLink = rowTask.find('.vault-dl-link'); + if (cookingTask.status === 'done') { + downloadLink[0].innerHTML = ''; - } else { - downloadLink[0].innerHTML = ''; - } + } else { + downloadLink[0].innerHTML = ''; } } - localStorage.setItem('swh-vault-cooking-tasks', JSON.stringify(vaultCookingTasks)); - checkVaultId = setTimeout(checkVaultCookingTasks, pollingInterval); - }) - .catch(error => { - console.log('Error when fetching vault cooking tasks:', error); - }); + } + localStorage.setItem('swh-vault-cooking-tasks', JSON.stringify(vaultCookingTasks)); + checkVaultId = setTimeout(checkVaultCookingTasks, pollingInterval); + + } catch (error) { + console.log('Error when fetching vault cooking tasks:', error); + } } export function removeCookingTaskInfo(tasksToRemove) { let vaultCookingTasks = JSON.parse(localStorage.getItem('swh-vault-cooking-tasks')); if (!vaultCookingTasks) { return; } vaultCookingTasks = $.grep(vaultCookingTasks, task => { return $.inArray(task.object_id, tasksToRemove) === -1; }); localStorage.setItem('swh-vault-cooking-tasks', JSON.stringify(vaultCookingTasks)); } export function initUi() { $('#vault-tasks-toggle-selection').change(event => { $('.vault-task-toggle-selection').prop('checked', event.currentTarget.checked); }); $('#vault-remove-tasks').click(() => { clearTimeout(checkVaultId); let tasksToRemove = []; $('.swh-vault-table tbody tr').each((i, row) => { let taskSelected = $(row).find('.vault-task-toggle-selection').prop('checked'); if (taskSelected) { let objectId = $(row).find('.vault-object-info').data('object-id'); tasksToRemove.push(objectId); $(row).remove(); } }); removeCookingTaskInfo(tasksToRemove); $('#vault-tasks-toggle-selection').prop('checked', false); checkVaultId = setTimeout(checkVaultCookingTasks, pollingInterval); }); checkVaultCookingTasks(); window.onfocus = () => { clearTimeout(checkVaultId); checkVaultCookingTasks(); }; } diff --git a/assets/src/bundles/webapp/notebook-rendering.js b/assets/src/bundles/webapp/notebook-rendering.js index fce6d48e..6bc1d3d1 100644 --- a/assets/src/bundles/webapp/notebook-rendering.js +++ b/assets/src/bundles/webapp/notebook-rendering.js @@ -1,137 +1,136 @@ /** * Copyright (C) 2019-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 */ import 'script-loader!notebookjs'; import AnsiUp from 'ansi_up'; import './notebook.css'; const ansiup = new AnsiUp(); ansiup.escape_for_html = false; function escapeHTML(text) { text = text.replace(//g, '>'); return text; } function unescapeHTML(text) { text = text.replace(/</g, '<'); text = text.replace(/>/g, '>'); return text; } function escapeLaTeX(text) { let blockMath = /\$\$(.+?)\$\$|\\\\\[(.+?)\\\\\]/msg; let inlineMath = /\$(.+?)\$|\\\\\((.+?)\\\\\)/g; let latexEnvironment = /\\begin\{([a-z]*\*?)\}(.+?)\\end\{\1\}/msg; let mathTextFound = []; let bm; while ((bm = blockMath.exec(text)) !== null) { mathTextFound.push(bm[1]); } let im; while ((im = inlineMath.exec(text)) !== null) { mathTextFound.push(im[1]); } let le; while ((le = latexEnvironment.exec(text)) !== null) { mathTextFound.push(le[1]); } for (let mathText of mathTextFound) { // showdown will remove line breaks in LaTex array and // some escaping sequences when converting md to html. // So we use the following escaping hacks to keep them in the html // output and avoid MathJax typesetting errors. let escapedText = mathText.replace('\\\\', '\\\\\\\\'); for (let specialLaTexChar of ['{', '}', '#', '%', '&', '_']) { escapedText = escapedText.replace(new RegExp(`\\\\${specialLaTexChar}`, 'g'), `\\\\${specialLaTexChar}`); } // some html escaping is also needed escapedText = escapeHTML(escapedText); // hack to prevent showdown to replace _ characters // by html em tags as it will break some math typesetting // (setting the literalMidWordUnderscores option is not // enough as iy only works for _ characters contained in words) escapedText = escapedText.replace(/_/g, '{@}underscore{@}'); if (mathText !== escapedText) { text = text.replace(mathText, escapedText); } } return text; } export async function renderNotebook(nbJsonUrl, domElt) { let showdown = await import(/* webpackChunkName: "showdown" */ 'utils/showdown'); await import(/* webpackChunkName: "highlightjs" */ 'utils/highlightjs'); function renderMarkdown(text) { let converter = new showdown.Converter({ tables: true, simplifiedAutoLink: true, rawHeaderId: true, literalMidWordUnderscores: true }); // some LaTeX escaping is required to get correct math typesetting text = escapeLaTeX(text); // render markdown let rendered = converter.makeHtml(text); // restore underscores in rendered HTML (see escapeLaTeX function) rendered = rendered.replace(/{@}underscore{@}/g, '_'); return rendered; } function highlightCode(text, preElt, codeElt, lang) { // no need to unescape text processed by ansiup if (text.indexOf(' { + $(document).ready(async() => { $('#pdf-prev').click(onPrevPage); $('#pdf-next').click(onNextPage); - let loadingTask = pdfjs.getDocument(pdfUrl); - loadingTask.promise.then(pdf => { + try { + const pdf = await pdfjs.getDocument(pdfUrl).promise; pdfDoc = pdf; $('#pdf-page-count').text(pdfDoc.numPages); // Initial/first page rendering renderPage(pageNum); - }, function(reason) { + } catch (reason) { // PDF loading error console.error(reason); - }); + } // Render PDF on resize $(window).on('resize', function() { queueRenderPage(pageNum); }); }); } diff --git a/assets/src/bundles/webapp/readme-rendering.js b/assets/src/bundles/webapp/readme-rendering.js index 88f048ab..2651f6c7 100644 --- a/assets/src/bundles/webapp/readme-rendering.js +++ b/assets/src/bundles/webapp/readme-rendering.js @@ -1,123 +1,118 @@ /** - * Copyright (C) 2018-2019 The Software Heritage developers + * Copyright (C) 2018-2021 The Software Heritage developers * See the AUTHORS file at the top-level directory of this distribution * License: GNU Affero General Public License version 3, or any later version * See top-level LICENSE file for more information */ import {handleFetchError} from 'utils/functions'; import {decode} from 'html-encoder-decoder'; export async function renderMarkdown(domElt, markdownDocUrl) { let showdown = await import(/* webpackChunkName: "showdown" */ 'utils/showdown'); await import(/* webpackChunkName: "highlightjs" */ 'utils/highlightjs'); // Adapted from https://github.com/Bloggify/showdown-highlight // Copyright (c) 2016-19 Bloggify (https://bloggify.org) function showdownHighlight() { return [{ type: 'output', filter: function(text, converter, options) { let left = '
]*>';
         let right = '
'; let flags = 'g'; let classAttr = 'class="'; let replacement = (wholeMatch, match, left, right) => { match = decode(match); let lang = (left.match(/class="([^ "]+)/) || [])[1]; if (left.includes(classAttr)) { let attrIndex = left.indexOf(classAttr) + classAttr.length; left = left.slice(0, attrIndex) + 'hljs ' + left.slice(attrIndex); } else { left = left.slice(0, -1) + ' class="hljs">'; } if (lang && hljs.getLanguage(lang)) { return left + hljs.highlight(match, {language: lang}).value + right; } else { return left + match + right; } }; return showdown.helper.replaceRecursiveRegExp(text, replacement, left, right, flags); } }]; } - $(document).ready(() => { + $(document).ready(async() => { let converter = new showdown.Converter({ tables: true, extensions: [showdownHighlight] }); - fetch(markdownDocUrl) - .then(handleFetchError) - .then(response => response.text()) - .then(data => { - $(domElt).addClass('swh-showdown'); - $(domElt).html(swh.webapp.filterXSS(converter.makeHtml(data))); - }) - .catch(() => { - $(domElt).text('Readme bytes are not available'); - }); + + try { + const response = await fetch(markdownDocUrl); + handleFetchError(response); + const data = await response.text(); + $(domElt).addClass('swh-showdown'); + $(domElt).html(swh.webapp.filterXSS(converter.makeHtml(data))); + } catch (_) { + $(domElt).text('Readme bytes are not available'); + } }); } export async function renderOrgData(domElt, orgDocData) { let org = await import(/* webpackChunkName: "org" */ 'utils/org'); let parser = new org.Parser(); let orgDocument = parser.parse(orgDocData, {toc: false}); let orgHTMLDocument = orgDocument.convert(org.ConverterHTML, {}); $(domElt).addClass('swh-org'); $(domElt).html(swh.webapp.filterXSS(orgHTMLDocument.toString())); // remove toc and section numbers to get consistent // with other readme renderings $('.swh-org ul').first().remove(); $('.section-number').remove(); } export function renderOrg(domElt, orgDocUrl) { - - $(document).ready(() => { - fetch(orgDocUrl) - .then(handleFetchError) - .then(response => response.text()) - .then(data => { - renderOrgData(domElt, data); - }) - .catch(() => { - $(domElt).text('Readme bytes are not available'); - }); + $(document).ready(async() => { + try { + const response = await fetch(orgDocUrl); + handleFetchError(response); + const data = await response.text(); + renderOrgData(domElt, data); + } catch (_) { + $(domElt).text('Readme bytes are not available'); + } }); - } export function renderTxt(domElt, txtDocUrl) { - - $(document).ready(() => { - fetch(txtDocUrl) - .then(handleFetchError) - .then(response => response.text()) - .then(data => { - let orgMode = '-*- mode: org -*-'; - if (data.indexOf(orgMode) !== -1) { - renderOrgData(domElt, data.replace(orgMode, '')); - } else { - $(domElt).addClass('swh-readme-txt'); - $(domElt) + $(document).ready(async() => { + try { + const response = await fetch(txtDocUrl); + handleFetchError(response); + const data = await response.text(); + + let orgMode = '-*- mode: org -*-'; + if (data.indexOf(orgMode) !== -1) { + renderOrgData(domElt, data.replace(orgMode, '')); + } else { + $(domElt).addClass('swh-readme-txt'); + $(domElt) .html('') .append($('
').text(data));
-        }
-      })
-      .catch(() => {
-        $(domElt).text('Readme bytes are not available');
-      });
+      }
+    } catch (_) {
+      $(domElt).text('Readme bytes are not available');
+    }
   });
-
 }
diff --git a/assets/src/bundles/webapp/status-widget.js b/assets/src/bundles/webapp/status-widget.js
index 5f28d9de..7eb31b3b 100644
--- a/assets/src/bundles/webapp/status-widget.js
+++ b/assets/src/bundles/webapp/status-widget.js
@@ -1,50 +1,50 @@
 /**
  * Copyright (C) 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
  */
 
 import './status-widget.css';
 
 const statusCodeColor = {
   '100': 'green', // Operational
   '200': 'blue', // Scheduled Maintenance
   '300': 'yellow', // Degraded Performance
   '400': 'yellow', // Partial Service Disruption
   '500': 'red', // Service Disruption
   '600': 'red' // Security Event
 };
 
 export function initStatusWidget(statusDataURL) {
-  $('.swh-current-status-indicator').ready(() => {
+  $('.swh-current-status-indicator').ready(async() => {
     let maxStatusCode = '';
     let maxStatusDescription = '';
     let sc = '';
     let sd = '';
-    fetch(statusDataURL)
-      .then(resp => resp.json())
-      .then(data => {
-        for (let s of data.result.status) {
-          sc = s.status_code;
-          sd = s.status;
-          if (maxStatusCode < sc) {
-            maxStatusCode = sc;
-            maxStatusDescription = sd;
-          }
-        }
-        if (maxStatusCode === '') {
-          $('.swh-current-status').remove();
-          return;
+    try {
+      const response = await fetch(statusDataURL);
+      const data = await response.json();
+
+      for (let s of data.result.status) {
+        sc = s.status_code;
+        sd = s.status;
+        if (maxStatusCode < sc) {
+          maxStatusCode = sc;
+          maxStatusDescription = sd;
         }
-        $('.swh-current-status-indicator').removeClass('green');
-        $('.swh-current-status-indicator').addClass(statusCodeColor[maxStatusCode]);
-        $('#swh-current-status-description').text(maxStatusDescription);
-      })
-      .catch(e => {
-        console.log(e);
+      }
+      if (maxStatusCode === '') {
         $('.swh-current-status').remove();
-      });
+        return;
+      }
+      $('.swh-current-status-indicator').removeClass('green');
+      $('.swh-current-status-indicator').addClass(statusCodeColor[maxStatusCode]);
+      $('#swh-current-status-description').text(maxStatusDescription);
 
+    } catch (e) {
+      console.log(e);
+      $('.swh-current-status').remove();
+    }
   });
 }
diff --git a/assets/src/bundles/webapp/webapp-utils.js b/assets/src/bundles/webapp/webapp-utils.js
index db762941..797a2469 100644
--- a/assets/src/bundles/webapp/webapp-utils.js
+++ b/assets/src/bundles/webapp/webapp-utils.js
@@ -1,401 +1,400 @@
 /**
  * Copyright (C) 2018-2021  The Software Heritage developers
  * See the AUTHORS file at the top-level directory of this distribution
  * License: GNU Affero General Public License version 3, or any later version
  * See top-level LICENSE file for more information
  */
 
 import objectFitImages from 'object-fit-images';
 import {selectText} from 'utils/functions';
 import {BREAKPOINT_MD} from 'utils/constants';
 
 let collapseSidebar = false;
 let previousSidebarState = localStorage.getItem('remember.lte.pushmenu');
 if (previousSidebarState !== undefined) {
   collapseSidebar = previousSidebarState === 'sidebar-collapse';
 }
 
 $(document).on('DOMContentLoaded', () => {
   // set state to collapsed on smaller devices
   if ($(window).width() < BREAKPOINT_MD) {
     collapseSidebar = true;
   }
 
   // restore previous sidebar state (collapsed/expanded)
   if (collapseSidebar) {
     // hack to avoid animated transition for collapsing sidebar
     // when loading a page
     let sidebarTransition = $('.main-sidebar, .main-sidebar:before').css('transition');
     let sidebarEltsTransition = $('.sidebar .nav-link p, .main-sidebar .brand-text, .sidebar .user-panel .info').css('transition');
     $('.main-sidebar, .main-sidebar:before').css('transition', 'none');
     $('.sidebar .nav-link p, .main-sidebar .brand-text, .sidebar .user-panel .info').css('transition', 'none');
     $('body').addClass('sidebar-collapse');
     $('.swh-words-logo-swh').css('visibility', 'visible');
     // restore transitions for user navigation
     setTimeout(() => {
       $('.main-sidebar, .main-sidebar:before').css('transition', sidebarTransition);
       $('.sidebar .nav-link p, .main-sidebar .brand-text, .sidebar .user-panel .info').css('transition', sidebarEltsTransition);
     });
   }
 });
 
 $(document).on('collapsed.lte.pushmenu', event => {
   if ($('body').width() >= BREAKPOINT_MD) {
     $('.swh-words-logo-swh').css('visibility', 'visible');
   }
 });
 
 $(document).on('shown.lte.pushmenu', event => {
   $('.swh-words-logo-swh').css('visibility', 'hidden');
 });
 
 function ensureNoFooterOverflow() {
   $('body').css('padding-bottom', $('footer').outerHeight() + 'px');
 }
 
 $(document).ready(() => {
   // redirect to last browse page if any when clicking on the 'Browse' entry
   // in the sidebar
   $(`.swh-browse-link`).click(event => {
     let lastBrowsePage = sessionStorage.getItem('last-browse-page');
     if (lastBrowsePage) {
       event.preventDefault();
       window.location = lastBrowsePage;
     }
   });
 
   const mainSideBar = $('.main-sidebar');
 
   function updateSidebarState() {
     const body = $('body');
     if (body.hasClass('sidebar-collapse') &&
         !mainSideBar.hasClass('swh-sidebar-collapsed')) {
       mainSideBar.removeClass('swh-sidebar-expanded');
       mainSideBar.addClass('swh-sidebar-collapsed');
       $('.swh-words-logo-swh').css('visibility', 'visible');
     } else if (!body.hasClass('sidebar-collapse') &&
                !mainSideBar.hasClass('swh-sidebar-expanded')) {
       mainSideBar.removeClass('swh-sidebar-collapsed');
       mainSideBar.addClass('swh-sidebar-expanded');
       $('.swh-words-logo-swh').css('visibility', 'hidden');
     }
     // ensure correct sidebar state when loading a page
     if (body.hasClass('hold-transition')) {
       setTimeout(() => {
         updateSidebarState();
       });
     }
   }
 
   // set sidebar state after collapse / expand animation
   mainSideBar.on('transitionend', evt => {
     updateSidebarState();
   });
 
   updateSidebarState();
 
   // ensure footer do not overflow main content for mobile devices
   // or after resizing the browser window
   ensureNoFooterOverflow();
   $(window).resize(function() {
     ensureNoFooterOverflow();
     if ($('body').hasClass('sidebar-collapse') && $('body').width() >= BREAKPOINT_MD) {
       $('.swh-words-logo-swh').css('visibility', 'visible');
     }
   });
   // activate css polyfill 'object-fit: contain' in old browsers
   objectFitImages();
 
   // reparent the modals to the top navigation div in order to be able
   // to display them
   $('.swh-browse-top-navigation').append($('.modal'));
 
   let selectedCode = null;
 
   function getCodeOrPreEltUnderPointer(e) {
     let elts = document.elementsFromPoint(e.clientX, e.clientY);
     for (let elt of elts) {
       if (elt.nodeName === 'CODE' || elt.nodeName === 'PRE') {
         return elt;
       }
     }
     return null;
   }
 
   // click handler to set focus on code block for copy
   $(document).click(e => {
     selectedCode = getCodeOrPreEltUnderPointer(e);
   });
 
   function selectCode(event, selectedCode) {
     if (selectedCode) {
       let hljsLnCodeElts = $(selectedCode).find('.hljs-ln-code');
       if (hljsLnCodeElts.length) {
         selectText(hljsLnCodeElts[0], hljsLnCodeElts[hljsLnCodeElts.length - 1]);
       } else {
         selectText(selectedCode.firstChild, selectedCode.lastChild);
       }
       event.preventDefault();
     }
   }
 
   // select the whole text of focused code block when user
   // double clicks or hits Ctrl+A
   $(document).dblclick(e => {
     if ((e.ctrlKey || e.metaKey)) {
       selectCode(e, getCodeOrPreEltUnderPointer(e));
     }
   });
 
   $(document).keydown(e => {
     if ((e.ctrlKey || e.metaKey) && e.key === 'a') {
       selectCode(e, selectedCode);
     }
   });
 
   // show/hide back-to-top button
   let scrollThreshold = 0;
   scrollThreshold += $('.swh-top-bar').height() || 0;
   scrollThreshold += $('.navbar').height() || 0;
   $(window).scroll(() => {
     if ($(window).scrollTop() > scrollThreshold) {
       $('#back-to-top').css('display', 'block');
     } else {
       $('#back-to-top').css('display', 'none');
     }
   });
 
   // navbar search form submission callback
   $('#swh-origins-search-top').submit(event => {
     event.preventDefault();
     if (event.target.checkValidity()) {
       $(event.target).removeClass('was-validated');
       let searchQueryText = $('#swh-origins-search-top-input').val().trim();
       let queryParameters = new URLSearchParams();
       queryParameters.append('q', searchQueryText);
       queryParameters.append('with_visit', true);
       queryParameters.append('with_content', true);
       window.location = `${Urls.browse_search()}?${queryParameters.toString()}`;
     } else {
       $(event.target).addClass('was-validated');
     }
   });
 });
 
 export function initPage(page) {
 
   $(document).ready(() => {
     // set relevant sidebar link to page active
     $(`.swh-${page}-item`).addClass('active');
     $(`.swh-${page}-link`).addClass('active');
 
     // triggered when unloading the current page
     $(window).on('unload', () => {
       // backup current browse page
       if (page === 'browse') {
         sessionStorage.setItem('last-browse-page', window.location);
       }
     });
   });
 }
 
 export function initHomePage() {
-  $(document).ready(() => {
+  $(document).ready(async() => {
     $('.swh-coverage-list').iFrameResize({heightCalculationMethod: 'taggedElement'});
-    fetch(Urls.stat_counters())
-      .then(response => response.json())
-      .then(data => {
-        if (data.stat_counters && !$.isEmptyObject(data.stat_counters)) {
-          for (let objectType of ['content', 'revision', 'origin', 'directory', 'person', 'release']) {
-            const count = data.stat_counters[objectType];
-            if (count !== undefined) {
-              $(`#swh-${objectType}-count`).html(count.toLocaleString());
-            } else {
-              $(`#swh-${objectType}-count`).closest('.swh-counter-container').hide();
-            }
-          }
+    const response = await fetch(Urls.stat_counters());
+    const data = await response.json();
+
+    if (data.stat_counters && !$.isEmptyObject(data.stat_counters)) {
+      for (let objectType of ['content', 'revision', 'origin', 'directory', 'person', 'release']) {
+        const count = data.stat_counters[objectType];
+        if (count !== undefined) {
+          $(`#swh-${objectType}-count`).html(count.toLocaleString());
         } else {
-          $('.swh-counter').html('0');
+          $(`#swh-${objectType}-count`).closest('.swh-counter-container').hide();
         }
-        if (data.stat_counters_history && !$.isEmptyObject(data.stat_counters_history)) {
-          for (let objectType of ['content', 'revision', 'origin']) {
-            const history = data.stat_counters_history[objectType];
-            if (history) {
-              swh.webapp.drawHistoryCounterGraph(`#swh-${objectType}-count-history`, history);
-            } else {
-              $(`#swh-${objectType}-count-history`).hide();
-            }
-
-          }
+      }
+    } else {
+      $('.swh-counter').html('0');
+    }
+    if (data.stat_counters_history && !$.isEmptyObject(data.stat_counters_history)) {
+      for (let objectType of ['content', 'revision', 'origin']) {
+        const history = data.stat_counters_history[objectType];
+        if (history) {
+          swh.webapp.drawHistoryCounterGraph(`#swh-${objectType}-count-history`, history);
         } else {
-          $('.swh-counter-history').hide();
+          $(`#swh-${objectType}-count-history`).hide();
         }
-      });
+
+      }
+    } else {
+      $('.swh-counter-history').hide();
+    }
   });
   initPage('home');
 }
 
 export function showModalMessage(title, message) {
   $('#swh-web-modal-message .modal-title').text(title);
   $('#swh-web-modal-message .modal-content p').text(message);
   $('#swh-web-modal-message').modal('show');
 }
 
 export function showModalConfirm(title, message, callback) {
   $('#swh-web-modal-confirm .modal-title').text(title);
   $('#swh-web-modal-confirm .modal-content p').text(message);
   $('#swh-web-modal-confirm #swh-web-modal-confirm-ok-btn').bind('click', () => {
     callback();
     $('#swh-web-modal-confirm').modal('hide');
     $('#swh-web-modal-confirm #swh-web-modal-confirm-ok-btn').unbind('click');
   });
   $('#swh-web-modal-confirm').modal('show');
 }
 
 export function showModalHtml(title, html) {
   $('#swh-web-modal-html .modal-title').text(title);
   $('#swh-web-modal-html .modal-body').html(html);
   $('#swh-web-modal-html').modal('show');
 }
 
 export function addJumpToPagePopoverToDataTable(dataTableElt) {
   dataTableElt.on('draw.dt', function() {
     $('.paginate_button.disabled').css('cursor', 'pointer');
     $('.paginate_button.disabled').on('click', event => {
       const pageInfo = dataTableElt.page.info();
       let content = ' / ${pageInfo.pages}`;
       $(event.target).popover({
         'title': 'Jump to page',
         'content': content,
         'html': true,
         'placement': 'top',
         'sanitizeFn': swh.webapp.filterXSS
       });
       $(event.target).popover('show');
       $('.jump-to-page').on('change', function() {
         $('.paginate_button.disabled').popover('hide');
         const pageNumber = parseInt($(this).val()) - 1;
         dataTableElt.page(pageNumber).draw('page');
       });
     });
   });
 
   dataTableElt.on('preXhr.dt', () => {
     $('.paginate_button.disabled').popover('hide');
   });
 }
 
 let swhObjectIcons;
 
 export function setSwhObjectIcons(icons) {
   swhObjectIcons = icons;
 }
 
 export function getSwhObjectIcon(swhObjectType) {
   return swhObjectIcons[swhObjectType];
 }
 
 let browsedSwhObjectMetadata = {};
 
 export function setBrowsedSwhObjectMetadata(metadata) {
   browsedSwhObjectMetadata = metadata;
 }
 
 export function getBrowsedSwhObjectMetadata() {
   return browsedSwhObjectMetadata;
 }
 
 // This will contain a mapping between an archived object type
 // and its related SWHID metadata for each object reachable from
 // the current browse view.
 // SWHID metadata contain the following keys:
 //   * object_type: type of archived object
 //   * object_id: sha1 object identifier
 //   * swhid: SWHID without contextual info
 //   * swhid_url: URL to resolve SWHID without contextual info
 //   * context: object describing SWHID context
 //   * swhid_with_context: SWHID with contextual info
 //   * swhid_with_context_url: URL to resolve SWHID with contextual info
 let swhidsContext_ = {};
 
 export function setSwhIdsContext(swhidsContext) {
   swhidsContext_ = {};
   for (let swhidContext of swhidsContext) {
     swhidsContext_[swhidContext.object_type] = swhidContext;
   }
 }
 
 export function getSwhIdsContext() {
   return swhidsContext_;
 }
 
 function setFullWidth(fullWidth) {
   if (fullWidth) {
     $('#swh-web-content').removeClass('container');
     $('#swh-web-content').addClass('container-fluid');
   } else {
     $('#swh-web-content').removeClass('container-fluid');
     $('#swh-web-content').addClass('container');
   }
   localStorage.setItem('swh-web-full-width', JSON.stringify(fullWidth));
   $('#swh-full-width-switch').prop('checked', fullWidth);
 }
 
 export function fullWidthToggled(event) {
   setFullWidth($(event.target).prop('checked'));
 }
 
 export function setContainerFullWidth() {
   let previousFullWidthState = JSON.parse(localStorage.getItem('swh-web-full-width'));
   if (previousFullWidthState !== null) {
     setFullWidth(previousFullWidthState);
   }
 }
 
 function coreSWHIDIsLowerCase(swhid) {
   const qualifiersPos = swhid.indexOf(';');
   let coreSWHID = swhid;
   if (qualifiersPos !== -1) {
     coreSWHID = swhid.slice(0, qualifiersPos);
   }
   return coreSWHID.toLowerCase() === coreSWHID;
 }
 
 export async function validateSWHIDInput(swhidInputElt) {
   const swhidInput = swhidInputElt.value.trim();
   let customValidity = '';
   if (swhidInput.toLowerCase().startsWith('swh:')) {
     if (coreSWHIDIsLowerCase(swhidInput)) {
       const resolveSWHIDUrl = Urls.api_1_resolve_swhid(swhidInput);
       const response = await fetch(resolveSWHIDUrl);
       const responseData = await response.json();
       if (responseData.hasOwnProperty('exception')) {
         customValidity = responseData.reason;
       }
     } else {
       const qualifiersPos = swhidInput.indexOf(';');
       if (qualifiersPos === -1) {
         customValidity = 'Invalid SWHID: all characters must be in lowercase. ';
         customValidity += `Valid SWHID is ${swhidInput.toLowerCase()}`;
       } else {
         customValidity = 'Invalid SWHID: the core part must be in lowercase. ';
         const coreSWHID = swhidInput.slice(0, qualifiersPos);
         customValidity += `Valid SWHID is ${swhidInput.replace(coreSWHID, coreSWHID.toLowerCase())}`;
       }
     }
   }
   swhidInputElt.setCustomValidity(customValidity);
   $(swhidInputElt).siblings('.invalid-feedback').text(customValidity);
 }
 
 export function isUserLoggedIn() {
   return JSON.parse($('#swh_user_logged_in').text());
 }