diff --git a/cypress/integration/origin-search.spec.js b/cypress/integration/origin-search.spec.js index 5f8b1501..e7f0ff04 100644 --- a/cypress/integration/origin-search.spec.js +++ b/cypress/integration/origin-search.spec.js @@ -1,429 +1,429 @@ /** * 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 */ const nonExistentText = 'NoMatchExists'; let origin; let url; function doSearch(searchText) { cy.get('#origins-url-patterns') .type(searchText) .get('.swh-search-icon') .click(); } function searchShouldRedirect(searchText, redirectUrl) { doSearch(searchText); cy.location('pathname') .should('equal', redirectUrl); } function searchShouldShowNotFound(searchText, msg) { doSearch(searchText); cy.get('#swh-no-result') .should('be.visible') .and('contain', msg); } function stubOriginVisitLatestRequests() { cy.server(); cy.route({ method: 'GET', url: '**/visit/latest/**', response: { type: 'tar' } }).as('originVisitLatest'); } describe('Test origin-search', function() { before(function() { origin = this.origin[0]; url = this.Urls.browse_search(); }); beforeEach(function() { cy.visit(url); }); it('should show in result when url is searched', function() { cy.get('#origins-url-patterns') .type(origin.url); cy.get('.swh-search-icon') .click(); cy.get('#origin-search-results') .should('be.visible'); cy.contains('tr', origin.url) .should('be.visible') .find('.swh-visit-status') .find('i') .should('have.class', 'mdi-check-bold') .and('have.attr', 'title', 'Origin has at least one full visit by Software Heritage'); }); it('should show not found message when no repo matches', function() { searchShouldShowNotFound(nonExistentText, 'No origins matching the search criteria were found.'); }); it('should add appropriate URL parameters', function() { // Check all three checkboxes and check if // correct url params are added cy.get('#swh-search-origins-with-visit') - .check() + .check({force: true}) .get('#swh-filter-empty-visits') - .check() + .check({force: true}) .get('#swh-search-origin-metadata') - .check() + .check({force: true}) .then(() => { const searchText = origin.url; doSearch(searchText); cy.location('search').then(locationSearch => { const urlParams = new URLSearchParams(locationSearch); const query = urlParams.get('q'); const withVisit = urlParams.has('with_visit'); const withContent = urlParams.has('with_content'); const searchMetadata = urlParams.has('search_metadata'); assert.strictEqual(query, searchText); assert.strictEqual(withVisit, true); assert.strictEqual(withContent, true); assert.strictEqual(searchMetadata, true); }); }); }); it('should not send request to the resolve endpoint', function() { cy.server(); cy.route({ method: 'GET', url: `${this.Urls.api_1_resolve_swh_pid('').slice(0, -1)}**` }).as('resolvePid'); cy.route({ method: 'GET', url: `${this.Urls.api_1_origin_search(origin.url)}**` }).as('searchOrigin'); cy.get('#origins-url-patterns') .type(origin.url); cy.get('.swh-search-icon') .click(); cy.wait('@searchOrigin'); cy.xhrShouldBeCalled('resolvePid', 0); cy.xhrShouldBeCalled('searchOrigin', 1); }); context('Test pagination', function() { it('should not paginate if there are not many results', function() { // Setup search cy.get('#swh-search-origins-with-visit') - .uncheck() + .uncheck({force: true}) .get('#swh-filter-empty-visits') - .uncheck() + .uncheck({force: true}) .then(() => { const searchText = 'libtess'; // Get first page of results doSearch(searchText); cy.get('.swh-search-result-entry') .should('have.length', 1); cy.get('.swh-search-result-entry#origin-0 td a') .should('have.text', 'https://github.com/memononen/libtess2'); cy.get('#origins-prev-results-button') .should('have.class', 'disabled'); cy.get('#origins-next-results-button') .should('have.class', 'disabled'); }); }); it('should paginate forward when there are many results', function() { stubOriginVisitLatestRequests(); // Setup search cy.get('#swh-search-origins-with-visit') - .uncheck() + .uncheck({force: true}) .get('#swh-filter-empty-visits') - .uncheck() + .uncheck({force: true}) .then(() => { const searchText = 'many.origins'; // Get first page of results doSearch(searchText); cy.wait('@originVisitLatest'); cy.get('.swh-search-result-entry') .should('have.length', 100); cy.get('.swh-search-result-entry#origin-0 td a') .should('have.text', 'https://many.origins/1'); cy.get('.swh-search-result-entry#origin-99 td a') .should('have.text', 'https://many.origins/100'); cy.get('#origins-prev-results-button') .should('have.class', 'disabled'); cy.get('#origins-next-results-button') .should('not.have.class', 'disabled'); // Get second page of results cy.get('#origins-next-results-button a') .click(); cy.wait('@originVisitLatest'); cy.get('.swh-search-result-entry') .should('have.length', 100); cy.get('.swh-search-result-entry#origin-0 td a') .should('have.text', 'https://many.origins/101'); cy.get('.swh-search-result-entry#origin-99 td a') .should('have.text', 'https://many.origins/200'); cy.get('#origins-prev-results-button') .should('not.have.class', 'disabled'); cy.get('#origins-next-results-button') .should('not.have.class', 'disabled'); // Get third (and last) page of results cy.get('#origins-next-results-button a') .click(); cy.wait('@originVisitLatest'); cy.get('.swh-search-result-entry') .should('have.length', 50); cy.get('.swh-search-result-entry#origin-0 td a') .should('have.text', 'https://many.origins/201'); cy.get('.swh-search-result-entry#origin-49 td a') .should('have.text', 'https://many.origins/250'); cy.get('#origins-prev-results-button') .should('not.have.class', 'disabled'); cy.get('#origins-next-results-button') .should('have.class', 'disabled'); }); }); it('should paginate backward from a middle page', function() { stubOriginVisitLatestRequests(); // Setup search cy.get('#swh-search-origins-with-visit') - .uncheck() + .uncheck({force: true}) .get('#swh-filter-empty-visits') - .uncheck() + .uncheck({force: true}) .then(() => { const searchText = 'many.origins'; // Get first page of results doSearch(searchText); cy.wait('@originVisitLatest'); cy.get('#origins-prev-results-button') .should('have.class', 'disabled'); cy.get('#origins-next-results-button') .should('not.have.class', 'disabled'); // Get second page of results cy.get('#origins-next-results-button a') .click(); cy.wait('@originVisitLatest'); cy.get('#origins-prev-results-button') .should('not.have.class', 'disabled'); cy.get('#origins-next-results-button') .should('not.have.class', 'disabled'); // Get first page of results again cy.get('#origins-prev-results-button a') .click(); cy.wait('@originVisitLatest'); cy.get('.swh-search-result-entry') .should('have.length', 100); cy.get('.swh-search-result-entry#origin-0 td a') .should('have.text', 'https://many.origins/1'); cy.get('.swh-search-result-entry#origin-99 td a') .should('have.text', 'https://many.origins/100'); cy.get('#origins-prev-results-button') .should('have.class', 'disabled'); cy.get('#origins-next-results-button') .should('not.have.class', 'disabled'); }); }); it('should paginate backward from the last page', function() { stubOriginVisitLatestRequests(); // Setup search cy.get('#swh-search-origins-with-visit') - .uncheck() + .uncheck({force: true}) .get('#swh-filter-empty-visits') - .uncheck() + .uncheck({force: true}) .then(() => { const searchText = 'many.origins'; // Get first page of results doSearch(searchText); cy.wait('@originVisitLatest'); cy.get('#origins-prev-results-button') .should('have.class', 'disabled'); cy.get('#origins-next-results-button') .should('not.have.class', 'disabled'); // Get second page of results cy.get('#origins-next-results-button a') .click(); cy.wait('@originVisitLatest'); cy.get('#origins-prev-results-button') .should('not.have.class', 'disabled'); cy.get('#origins-next-results-button') .should('not.have.class', 'disabled'); // Get third (and last) page of results cy.get('#origins-next-results-button a') .click(); cy.get('#origins-prev-results-button') .should('not.have.class', 'disabled'); cy.get('#origins-next-results-button') .should('have.class', 'disabled'); // Get second page of results again cy.get('#origins-prev-results-button a') .click(); cy.wait('@originVisitLatest'); cy.get('.swh-search-result-entry') .should('have.length', 100); cy.get('.swh-search-result-entry#origin-0 td a') .should('have.text', 'https://many.origins/101'); cy.get('.swh-search-result-entry#origin-99 td a') .should('have.text', 'https://many.origins/200'); cy.get('#origins-prev-results-button') .should('not.have.class', 'disabled'); cy.get('#origins-next-results-button') .should('not.have.class', 'disabled'); // Get first page of results again cy.get('#origins-prev-results-button a') .click(); cy.wait('@originVisitLatest'); cy.get('.swh-search-result-entry') .should('have.length', 100); cy.get('.swh-search-result-entry#origin-0 td a') .should('have.text', 'https://many.origins/1'); cy.get('.swh-search-result-entry#origin-99 td a') .should('have.text', 'https://many.origins/100'); cy.get('#origins-prev-results-button') .should('have.class', 'disabled'); cy.get('#origins-next-results-button') .should('not.have.class', 'disabled'); }); }); }); context('Test valid persistent ids', function() { it('should resolve directory', function() { const redirectUrl = this.Urls.browse_directory(origin.content[0].directory); const persistentId = `swh:1:dir:${origin.content[0].directory}`; searchShouldRedirect(persistentId, redirectUrl); }); it('should resolve revision', function() { const redirectUrl = this.Urls.browse_revision(origin.revisions[0]); const persistentId = `swh:1:rev:${origin.revisions[0]}`; searchShouldRedirect(persistentId, redirectUrl); }); it('should resolve snapshot', function() { const redirectUrl = this.Urls.browse_snapshot_directory(origin.snapshot); const persistentId = `swh:1:snp:${origin.snapshot}`; searchShouldRedirect(persistentId, redirectUrl); }); it('should resolve content', function() { const redirectUrl = this.Urls.browse_content(`sha1_git:${origin.content[0].sha1git}`); const persistentId = `swh:1:cnt:${origin.content[0].sha1git}`; searchShouldRedirect(persistentId, redirectUrl); }); it('should not send request to the search endpoint', function() { cy.server(); const persistentId = `swh:1:rev:${origin.revisions[0]}`; cy.route({ method: 'GET', url: this.Urls.api_1_resolve_swh_pid(persistentId) }).as('resolvePid'); cy.route({ method: 'GET', url: `${this.Urls.api_1_origin_search('').slice(0, -1)}**` }).as('searchOrigin'); cy.get('#origins-url-patterns') .type(persistentId); cy.get('.swh-search-icon') .click(); cy.wait('@resolvePid'); cy.xhrShouldBeCalled('resolvePid', 1); cy.xhrShouldBeCalled('searchOrigin', 0); }); }); context('Test invalid persistent ids', function() { it('should show not found for directory', function() { const persistentId = `swh:1:dir:${this.unarchivedRepo.rootDirectory}`; const msg = `Directory with sha1_git ${this.unarchivedRepo.rootDirectory} not found`; searchShouldShowNotFound(persistentId, msg); }); it('should show not found for snapshot', function() { const persistentId = `swh:1:snp:${this.unarchivedRepo.snapshot}`; const msg = `Snapshot with id ${this.unarchivedRepo.snapshot} not found!`; searchShouldShowNotFound(persistentId, msg); }); it('should show not found for revision', function() { const persistentId = `swh:1:rev:${this.unarchivedRepo.revision}`; const msg = `Revision with sha1_git ${this.unarchivedRepo.revision} not found.`; searchShouldShowNotFound(persistentId, msg); }); it('should show not found for content', function() { const persistentId = `swh:1:cnt:${this.unarchivedRepo.content[0].sha1git}`; const msg = `Content with sha1_git checksum equals to ${this.unarchivedRepo.content[0].sha1git} not found!`; searchShouldShowNotFound(persistentId, msg); }); }); }); diff --git a/cypress/integration/persistent-identifiers.spec.js b/cypress/integration/persistent-identifiers.spec.js index fa889f63..e6268054 100644 --- a/cypress/integration/persistent-identifiers.spec.js +++ b/cypress/integration/persistent-identifiers.spec.js @@ -1,228 +1,228 @@ /** * 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 */ let origin, originBadgeUrl, originBrowseUrl; let url, urlPrefix; let cntSWHID, cntSWHIDWithContext; let dirSWHID, dirSWHIDWithContext; let relSWHID, relSWHIDWithContext; let revSWHID, revSWHIDWithContext; let snpSWHID, snpSWHIDWithContext; let testsData; const firstSelLine = 6; const lastSelLine = 12; describe('Persistent Identifiers Tests', function() { before(function() { origin = this.origin[1]; url = `${this.Urls.browse_origin_content()}?origin_url=${origin.url}&path=${origin.content[0].path}`; url = `${url}&release=${origin.release}#L${firstSelLine}-L${lastSelLine}`; originBadgeUrl = this.Urls.swh_badge('origin', origin.url); originBrowseUrl = `${this.Urls.browse_origin()}?origin_url=${origin.url}`; cy.visit(url).window().then(win => { urlPrefix = `${win.location.protocol}//${win.location.hostname}`; if (win.location.port) { urlPrefix += `:${win.location.port}`; } const swhids = win.swh.webapp.getSwhIdsContext(); cntSWHID = swhids.content.swhid; cntSWHIDWithContext = swhids.content.swhid_with_context; cntSWHIDWithContext += `;lines=${firstSelLine}-${lastSelLine}`; dirSWHID = swhids.directory.swhid; dirSWHIDWithContext = swhids.directory.swhid_with_context; revSWHID = swhids.revision.swhid; revSWHIDWithContext = swhids.revision.swhid_with_context; relSWHID = swhids.release.swhid; relSWHIDWithContext = swhids.release.swhid_with_context; snpSWHID = swhids.snapshot.swhid; snpSWHIDWithContext = swhids.snapshot.swhid_with_context; testsData = [ { 'objectType': 'content', 'objectPids': [cntSWHIDWithContext, cntSWHID], 'badgeUrl': this.Urls.swh_badge('content', swhids.content.object_id), 'badgePidUrl': this.Urls.swh_badge_pid(cntSWHID), 'browseUrl': this.Urls.browse_swh_id(cntSWHIDWithContext) }, { 'objectType': 'directory', 'objectPids': [dirSWHIDWithContext, dirSWHID], 'badgeUrl': this.Urls.swh_badge('directory', swhids.directory.object_id), 'badgePidUrl': this.Urls.swh_badge_pid(dirSWHID), 'browseUrl': this.Urls.browse_swh_id(dirSWHIDWithContext) }, { 'objectType': 'release', 'objectPids': [relSWHIDWithContext, relSWHID], 'badgeUrl': this.Urls.swh_badge('release', swhids.release.object_id), 'badgePidUrl': this.Urls.swh_badge_pid(relSWHID), 'browseUrl': this.Urls.browse_swh_id(relSWHIDWithContext) }, { 'objectType': 'revision', 'objectPids': [revSWHIDWithContext, revSWHID], 'badgeUrl': this.Urls.swh_badge('revision', swhids.revision.object_id), 'badgePidUrl': this.Urls.swh_badge_pid(revSWHID), 'browseUrl': this.Urls.browse_swh_id(revSWHIDWithContext) }, { 'objectType': 'snapshot', 'objectPids': [snpSWHIDWithContext, snpSWHID], 'badgeUrl': this.Urls.swh_badge('snapshot', swhids.snapshot.object_id), 'badgePidUrl': this.Urls.swh_badge_pid(snpSWHID), 'browseUrl': this.Urls.browse_swh_id(snpSWHIDWithContext) } ]; }); }); beforeEach(function() { cy.visit(url); }); it('should open and close identifiers tab when clicking on handle', function() { cy.get('#swh-identifiers') .should('have.class', 'ui-slideouttab-ready'); cy.get('.ui-slideouttab-handle') .click(); cy.get('#swh-identifiers') .should('have.class', 'ui-slideouttab-open'); cy.get('.ui-slideouttab-handle') .click(); cy.get('#swh-identifiers') .should('not.have.class', 'ui-slideouttab-open'); }); it('should display identifiers with permalinks for browsed objects', function() { cy.get('.ui-slideouttab-handle') .click(); for (let td of testsData) { cy.get(`a[href="#swh-id-tab-${td.objectType}"]`) .click(); cy.get(`#swh-id-tab-${td.objectType}`) .should('be.visible'); cy.get(`#swh-id-tab-${td.objectType} .swh-id`) .contains(td.objectPids[0]) .should('have.attr', 'href', this.Urls.browse_swh_id(td.objectPids[0])); } }); it('should update other object identifiers contextual info when toggling context checkbox', function() { cy.get('.ui-slideouttab-handle') .click(); for (let td of testsData) { cy.get(`a[href="#swh-id-tab-${td.objectType}"]`) .click(); cy.get(`#swh-id-tab-${td.objectType} .swh-id`) .contains(td.objectPids[0]) .should('have.attr', 'href', this.Urls.browse_swh_id(td.objectPids[0])); - cy.get(`#swh-id-tab-${td.objectType} .swh-id-context-option`) + cy.get(`#swh-id-tab-${td.objectType} .swh-id-option`) .click(); cy.get(`#swh-id-tab-${td.objectType} .swh-id`) .contains(td.objectPids[1]) .should('have.attr', 'href', this.Urls.browse_swh_id(td.objectPids[1])); - cy.get(`#swh-id-tab-${td.objectType} .swh-id-context-option`) + cy.get(`#swh-id-tab-${td.objectType} .swh-id-option`) .click(); cy.get(`#swh-id-tab-${td.objectType} .swh-id`) .contains(td.objectPids[0]) .should('have.attr', 'href', this.Urls.browse_swh_id(td.objectPids[0])); } }); it('should display swh badges in identifiers tab for browsed objects', function() { cy.get('.ui-slideouttab-handle') .click(); const originBadgeUrl = this.Urls.swh_badge('origin', origin.url); for (let td of testsData) { cy.get(`a[href="#swh-id-tab-${td.objectType}"]`) .click(); cy.get(`#swh-id-tab-${td.objectType} .swh-badge-origin`) .should('have.attr', 'src', originBadgeUrl); cy.get(`#swh-id-tab-${td.objectType} .swh-badge-${td.objectType}`) .should('have.attr', 'src', td.badgeUrl); } }); it('should display badge integration info when clicking on it', function() { cy.get('.ui-slideouttab-handle') .click(); for (let td of testsData) { cy.get(`a[href="#swh-id-tab-${td.objectType}"]`) .click(); cy.get(`#swh-id-tab-${td.objectType} .swh-badge-origin`) .click() .wait(500); for (let badgeType of ['html', 'md', 'rst']) { cy.get(`.modal .swh-badge-${badgeType}`) .contains(`${urlPrefix}${originBrowseUrl}`) .contains(`${urlPrefix}${originBadgeUrl}`); } cy.get('.modal.show .close') .click() .wait(500); cy.get(`#swh-id-tab-${td.objectType} .swh-badge-${td.objectType}`) .click() .wait(500); for (let badgeType of ['html', 'md', 'rst']) { cy.get(`.modal .swh-badge-${badgeType}`) .contains(`${urlPrefix}${td.browseUrl}`) .contains(`${urlPrefix}${td.badgePidUrl}`); } cy.get('.modal.show .close') .click() .wait(500); } }); it('should be possible to retrieve SWHIDs context from JavaScript', function() { cy.window().then(win => { const swhIdsContext = win.swh.webapp.getSwhIdsContext(); for (let testData of testsData) { assert.isTrue(swhIdsContext.hasOwnProperty(testData.objectType)); assert.equal(swhIdsContext[testData.objectType].swhid, testData.objectPids.slice(-1)[0]); } }); }); }); diff --git a/cypress/integration/vault.spec.js b/cypress/integration/vault.spec.js index 1a1fe782..b40ff58f 100644 --- a/cypress/integration/vault.spec.js +++ b/cypress/integration/vault.spec.js @@ -1,427 +1,427 @@ /** * Copyright (C) 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 */ let vaultItems = []; const progressbarColors = { 'new': 'rgba(128, 128, 128, 0.5)', 'pending': 'rgba(0, 0, 255, 0.5)', 'done': 'rgb(92, 184, 92)' }; function checkVaultCookingTask(objectType) { cy.contains('button', 'Actions') .click(); cy.contains('.dropdown-item', 'Download') .click(); cy.contains('.dropdown-item', objectType) .click(); cy.wait('@checkVaultCookingTask'); } function updateVaultItemList(vaultUrl, vaultItems) { cy.visit(vaultUrl) .then(() => { // Add uncooked task to localStorage // which updates it in vault items list window.localStorage.setItem('swh-vault-cooking-tasks', JSON.stringify(vaultItems)); }); } // Mocks API response : /api/1/vault/(:objectType)/(:hash) // objectType : {'directory', 'revision'} function genVaultCookingResponse(objectType, objectId, status, message, fetchUrl) { return { 'obj_type': objectType, 'id': 1, 'progress_message': message, 'status': status, 'obj_id': objectId, 'fetch_url': fetchUrl }; }; // Tests progressbar color, status // And status in localStorage function testStatus(taskId, color, statusMsg, status) { cy.get(`.swh-vault-table #vault-task-${taskId}`) .should('be.visible') .find('.progress-bar') .should('be.visible') .and('have.css', 'background-color', color) .and('contain', statusMsg) .then(() => { // Vault item with object_id as taskId should exist in localStorage const currentVaultItems = JSON.parse(window.localStorage.getItem('swh-vault-cooking-tasks')); const vaultItem = currentVaultItems.find(obj => obj.object_id === taskId); assert.isNotNull(vaultItem); assert.strictEqual(vaultItem.status, status); }); } describe('Vault Cooking User Interface Tests', function() { before(function() { this.directory = this.origin[0].directory[0].id; this.directoryUrl = this.Urls.browse_directory(this.directory); this.vaultDirectoryUrl = this.Urls.api_1_vault_cook_directory(this.directory); this.vaultFetchDirectoryUrl = this.Urls.api_1_vault_fetch_directory(this.directory); this.revision = this.origin[1].revisions[0]; this.revisionUrl = this.Urls.browse_revision(this.revision); this.vaultRevisionUrl = this.Urls.api_1_vault_cook_revision_gitfast(this.revision); this.vaultFetchRevisionUrl = this.Urls.api_1_vault_fetch_revision_gitfast(this.revision); vaultItems[0] = { 'object_type': 'revision', 'object_id': this.revision, 'email': '', 'status': 'done', 'fetch_url': `/api/1/vault/revision/${this.revision}/gitfast/raw/`, 'progress_message': null }; }); beforeEach(function() { this.genVaultDirCookingResponse = (status, message = null) => { return genVaultCookingResponse('directory', this.directory, status, message, this.vaultFetchDirectoryUrl); }; this.genVaultRevCookingResponse = (status, message = null) => { return genVaultCookingResponse('revision', this.revision, status, message, this.vaultFetchRevisionUrl); }; cy.server(); }); it('should create a directory cooking task and report its status', function() { // Browse a directory cy.visit(this.directoryUrl); // Stub responses when requesting the vault API to simulate // a task has been created cy.route({ method: 'GET', url: this.vaultDirectoryUrl, response: {'exception': 'NotFoundExc'} }).as('checkVaultCookingTask'); cy.route({ method: 'POST', url: this.vaultDirectoryUrl, response: this.genVaultDirCookingResponse('new') }).as('createVaultCookingTask'); checkVaultCookingTask('as tarball'); cy.route({ method: 'GET', url: this.vaultDirectoryUrl, response: this.genVaultDirCookingResponse('new') }).as('checkVaultCookingTask'); // Create a vault cooking task through the GUI cy.get('.modal-dialog') .contains('button:visible', 'Ok') .click(); cy.wait('@createVaultCookingTask'); // Check that a redirection to the vault UI has been performed cy.url().should('eq', Cypress.config().baseUrl + this.Urls.browse_vault()); cy.wait('@checkVaultCookingTask').then(() => { testStatus(this.directory, progressbarColors['new'], 'new', 'new'); }); // Stub response to the vault API indicating the task is processing cy.route({ method: 'GET', url: this.vaultDirectoryUrl, response: this.genVaultDirCookingResponse('pending', 'Processing...') }).as('checkVaultCookingTask'); cy.wait('@checkVaultCookingTask').then(() => { testStatus(this.directory, progressbarColors['pending'], 'Processing...', 'pending'); }); // Stub response to the vault API indicating the task is finished cy.route({ method: 'GET', url: this.vaultDirectoryUrl, response: this.genVaultDirCookingResponse('done') }).as('checkVaultCookingTask'); cy.wait('@checkVaultCookingTask').then(() => { testStatus(this.directory, progressbarColors['done'], 'done', 'done'); }); // Stub response to the vault API to simulate archive download cy.route({ method: 'GET', url: this.vaultFetchDirectoryUrl, response: `fx:${this.directory}.tar.gz,binary`, headers: { 'Content-disposition': `attachment; filename=${this.directory}.tar.gz`, 'Content-Type': 'application/gzip' } }).as('fetchCookedArchive'); cy.get(`#vault-task-${this.directory} .vault-dl-link button`) .click(); cy.wait('@fetchCookedArchive').then((xhr) => { assert.isNotNull(xhr.response.body); }); }); it('should create a revision cooking task and report its status', function() { // Browse a revision cy.visit(this.revisionUrl); // Stub responses when requesting the vault API to simulate // a task has been created cy.route({ method: 'GET', url: this.vaultRevisionUrl, response: {'exception': 'NotFoundExc'} }).as('checkVaultCookingTask'); cy.route({ method: 'POST', url: this.vaultRevisionUrl, response: this.genVaultRevCookingResponse('new') }).as('createVaultCookingTask'); // Create a vault cooking task through the GUI checkVaultCookingTask('as git'); cy.route({ method: 'GET', url: this.vaultRevisionUrl, response: this.genVaultRevCookingResponse('new') }).as('checkVaultCookingTask'); // Create a vault cooking task through the GUI cy.get('.modal-dialog') .contains('button:visible', 'Ok') .click(); cy.wait('@createVaultCookingTask'); // Check that a redirection to the vault UI has been performed cy.url().should('eq', Cypress.config().baseUrl + this.Urls.browse_vault()); cy.wait('@checkVaultCookingTask').then(() => { testStatus(this.revision, progressbarColors['new'], 'new', 'new'); }); // Stub response to the vault API indicating the task is processing cy.route({ method: 'GET', url: this.vaultRevisionUrl, response: this.genVaultRevCookingResponse('pending', 'Processing...') }).as('checkVaultCookingTask'); cy.wait('@checkVaultCookingTask').then(() => { testStatus(this.revision, progressbarColors['pending'], 'Processing...', 'pending'); }); // Stub response to the vault API indicating the task is finished cy.route({ method: 'GET', url: this.vaultRevisionUrl, response: this.genVaultRevCookingResponse('done') }).as('checkVaultCookingTask'); cy.wait('@checkVaultCookingTask').then(() => { testStatus(this.revision, progressbarColors['done'], 'done', 'done'); }); // Stub response to the vault API indicating to simulate archive // download cy.route({ method: 'GET', url: this.vaultFetchRevisionUrl, response: `fx:${this.revision}.gitfast.gz,binary`, headers: { 'Content-disposition': `attachment; filename=${this.revision}.gitfast.gz`, 'Content-Type': 'application/gzip' } }).as('fetchCookedArchive'); cy.get(`#vault-task-${this.revision} .vault-dl-link button`) .click(); cy.wait('@fetchCookedArchive').then((xhr) => { assert.isNotNull(xhr.response.body); }); }); it('should offer to recook an archive if no more available to download', function() { updateVaultItemList(this.Urls.browse_vault(), vaultItems); // Send 404 when fetching vault item cy.route({ method: 'GET', status: 404, url: this.vaultFetchRevisionUrl, response: { 'exception': 'NotFoundExc', 'reason': `Revision with ID '${this.revision}' not found.` }, headers: { 'Content-Type': 'json' } }).as('fetchCookedArchive'); cy.get(`#vault-task-${this.revision} .vault-dl-link button`) .click(); cy.wait('@fetchCookedArchive').then(() => { cy.route({ method: 'POST', url: this.vaultRevisionUrl, response: this.genVaultRevCookingResponse('new') }).as('createVaultCookingTask'); cy.route({ method: 'GET', url: this.vaultRevisionUrl, response: this.genVaultRevCookingResponse('new') }).as('checkVaultCookingTask'); cy.get('#vault-recook-object-modal > .modal-dialog') .should('be.visible') .contains('button:visible', 'Ok') .click(); cy.wait('@createVaultCookingTask') .wait('@checkVaultCookingTask') .then(() => { testStatus(this.revision, progressbarColors['new'], 'new', 'new'); }); }); }); it('should remove selected vault items', function() { updateVaultItemList(this.Urls.browse_vault(), vaultItems); cy.get(`#vault-task-${this.revision}`) .find('input[type="checkbox"]') - .click(); + .click({force: true}); cy.contains('button', 'Remove selected tasks') .click(); cy.get(`#vault-task-${this.revision}`) .should('not.exist'); }); it('should offer to immediately download a directory tarball if already cooked', function() { // Browse a directory cy.visit(this.directoryUrl); // Stub responses when requesting the vault API to simulate // the directory tarball has already been cooked cy.route({ method: 'GET', url: this.vaultDirectoryUrl, response: this.genVaultDirCookingResponse('done') }).as('checkVaultCookingTask'); // Stub response to the vault API to simulate archive download cy.route({ method: 'GET', url: this.vaultFetchDirectoryUrl, response: `fx:${this.directory}.tar.gz,binary`, headers: { 'Content-disposition': `attachment; filename=${this.directory}.tar.gz`, 'Content-Type': 'application/gzip' } }).as('fetchCookedArchive'); // Create a vault cooking task through the GUI checkVaultCookingTask('as tarball'); // Start archive download through the GUI cy.get('.modal-dialog') .contains('button:visible', 'Ok') .click(); cy.wait('@fetchCookedArchive'); }); it('should offer to immediately download a revision gitfast archive if already cooked', function() { // Browse a directory cy.visit(this.revisionUrl); // Stub responses when requesting the vault API to simulate // the directory tarball has already been cooked cy.route({ method: 'GET', url: this.vaultRevisionUrl, response: this.genVaultRevCookingResponse('done') }).as('checkVaultCookingTask'); // Stub response to the vault API to simulate archive download cy.route({ method: 'GET', url: this.vaultFetchRevisionUrl, response: `fx:${this.revision}.gitfast.gz,binary`, headers: { 'Content-disposition': `attachment; filename=${this.revision}.gitfast.gz`, 'Content-Type': 'application/gzip' } }).as('fetchCookedArchive'); checkVaultCookingTask('as git'); // Start archive download through the GUI cy.get('.modal-dialog') .contains('button:visible', 'Ok') .click(); cy.wait('@fetchCookedArchive'); }); it('should offer to recook an object if previous vault task failed', function() { cy.visit(this.directoryUrl); // Stub responses when requesting the vault API to simulate // the last cooking of the directory tarball has failed cy.route({ method: 'GET', url: this.vaultDirectoryUrl, response: this.genVaultDirCookingResponse('failed') }).as('checkVaultCookingTask'); checkVaultCookingTask('as tarball'); // Check that recooking the directory is offered to user cy.get('.modal-dialog') .contains('button:visible', 'Ok') .should('be.visible'); }); }); diff --git a/swh/web/assets/config/bootstrap-pre-customize.scss b/swh/web/assets/config/bootstrap-pre-customize.scss index 484a74c6..0116b22d 100644 --- a/swh/web/assets/config/bootstrap-pre-customize.scss +++ b/swh/web/assets/config/bootstrap-pre-customize.scss @@ -1,40 +1,41 @@ /** * 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 */ // override some global bootstrap sass variables before generating stylesheets // global text colors and fonts +$primary: #e20026; $body-color: rgba(0, 0, 0, 0.55); $font-family-sans-serif: "Alegreya Sans", sans-serif !important; $link-color: rgba(0, 0, 0, 0.75); $code-color: #c7254e; // headings $headings-line-height: 1.1; $headings-color: #e20026; $headings-font-family: "Alegreya Sans", sans-serif !important; // remove the ugly box shadow from bootstrap 4.x $input-btn-focus-width: 0; // dropdown menu padding $dropdown-padding-y: 0.25rem; $dropdown-item-padding-x: 0; $dropdown-item-padding-y: 0; // card header padding $card-spacer-y: 0.5rem; // nav pills colors $nav-pills-link-active-color: rgba(0, 0, 0, 0.55); $nav-pills-link-active-bg: #f2f4f5; // table cell padding $table-cell-padding: 0.4rem; // remove container padding $grid-gutter-width: 0; diff --git a/swh/web/assets/src/bundles/vault/vault-ui.js b/swh/web/assets/src/bundles/vault/vault-ui.js index 1e77001c..60553412 100644 --- a/swh/web/assets/src/bundles/vault/vault-ui.js +++ b/swh/web/assets/src/bundles/vault/vault-ui.js @@ -1,252 +1,254 @@ /** * 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'; let progress = `<div class="progress"> <div class="progress-bar progress-bar-success progress-bar-striped" role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width: 100%;height: 100%;"> </div> </div>;`; 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) { 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'); } }); } // called when the user wants to recook an archive // for which the download link is not available anymore export 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; } // 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; } } // 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'); }) // something went wrong .catch(() => { checkVaultCookingTasks(); $('#vault-recook-object-modal').modal('hide'); }); } } 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-id').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); let downloadLinkWait = 'Waiting for download link to be available'; if (!rowTask.length) { let 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); let tableRow; if (cookingTask.object_type === 'directory') { tableRow = `<tr id="vault-task-${cookingTask.object_id}" title="Once downloaded, the directory can be extracted with the ` + `following command:\n\n$ tar xvzf ${cookingTask.object_id}.tar.gz">`; } else { tableRow = `<tr id="vault-task-${cookingTask.object_id}" title="Once downloaded, the git repository can be imported with the ` + `following commands:\n\n$ git init\n$ zcat ${cookingTask.object_id}.gitfast.gz | git fast-import">`; } - tableRow += '<td><input type="checkbox" class="vault-task-toggle-selection"/></td>'; - tableRow += `<td style="width: 120px"><i class="${swh.webapp.getSwhObjectIcon(cookingTask.object_type)} mdi-fw" aria-hidden="true"></i>${cookingTask.object_type}</td>`; + tableRow += '<td><div class="custom-control custom-checkbox">'; + tableRow += `<input type="checkbox" class="custom-control-input vault-task-toggle-selection" id="vault-task-toggle-selection-${cookingTask.object_id}"/>`; + tableRow += `<label class="custom-control-label" for="vault-task-toggle-selection-${cookingTask.object_id}"></label></td>`; + tableRow += `<td style="width: 120px"><i class="${swh.webapp.getSwhObjectIcon(cookingTask.object_type)} mdi-fw"></i>${cookingTask.object_type}</td>`; tableRow += `<td class="vault-object-id" data-object-id="${cookingTask.object_id}"><a href="${browseUrl}">${cookingTask.object_id}</a></td>`; tableRow += `<td style="width: 350px">${progressBar.outerHTML}</td>`; let downloadLink = downloadLinkWait; if (cookingTask.status === 'done') { downloadLink = `<button class="btn btn-default btn-sm" onclick="swh.vault.fetchCookedObject('${cookingTask.fetch_url}')` + '"><i class="mdi mdi-download mdi-fw" aria-hidden="true"></i>Download</button>'; } else if (cookingTask.status === 'failed') { downloadLink = ''; } tableRow += `<td class="vault-dl-link" style="width: 320px">${downloadLink}</td>`; tableRow += '</tr>'; table.prepend(tableRow); } else { let progressBar = rowTask.find('.progress-bar'); updateProgressBar(progressBar, cookingTask); let downloadLink = rowTask.find('.vault-dl-link'); if (cookingTask.status === 'done') { downloadLink[0].innerHTML = `<button class="btn btn-default btn-sm" onclick="swh.vault.fetchCookedObject('${cookingTask.fetch_url}')` + '"><i class="mdi mdi-download mdi-fw" aria-hidden="true"></i>Download</button>'; } else if (cookingTask.status === 'failed') { downloadLink[0].innerHTML = ''; } else if (cookingTask.status === 'new') { downloadLink[0].innerHTML = downloadLinkWait; } } } localStorage.setItem('swh-vault-cooking-tasks', JSON.stringify(vaultCookingTasks)); checkVaultId = setTimeout(checkVaultCookingTasks, pollingInterval); }) .catch(() => {}); } 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-id').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/swh/web/templates/browse/browse.html b/swh/web/templates/browse/browse.html index 8a7b07f6..184a4846 100644 --- a/swh/web/templates/browse/browse.html +++ b/swh/web/templates/browse/browse.html @@ -1,63 +1,65 @@ {% extends "./layout.html" %} {% comment %} Copyright (C) 2017-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 {% endcomment %} {% load swh_templatetags %} {% block title %}{{ heading }} – Software Heritage archive{% endblock %} {% block navbar-content %} + {% if snapshot_context %} <h4> <i class="{{ swh_object_icons|key_value:swh_object_name.lower }} mdi-fw" aria-hidden="true"></i> {% if snapshot_context.origin_info %} Browse archived {{ swh_object_name.lower }} for origin <a href="{% url 'browse-origin' %}?origin_url={{ snapshot_context.origin_info.url }}"> {{ snapshot_context.origin_info.url }} </a> {% if snapshot_context.origin_info.url|slice:"0:4" == "http" %} <a href="{{ snapshot_context.origin_info.url }}" title="Go to origin"> <i class="mdi mdi-open-in-new" aria-hidden="true"></i> </a> {% endif %} {% else %} Browse archived {{ swh_object_name.lower }} for snapshot <a href="{% url 'browse-swh-id' snapshot_context.snapshot_swhid %}"> {{ snapshot_context.snapshot_swhid }} </a> {% endif %} </h4> {% else %} <h4> <i class="{{ swh_object_icons|key_value:swh_object_name.lower }} mdi-fw" aria-hidden="true"></i> Browse archived {{ swh_object_name.lower }} <a href="{% url 'browse-swh-id' swh_object_id %}"> {{ swh_object_id }} </a> </h4> {% endif %} + {% endblock %} {% block browse-content %} {% block swh-browse-before-content %} {% if snapshot_context %} {% include "includes/snapshot-context.html" %} {% endif %} {% endblock %} {% block swh-browse-content %}{% endblock %} {% block swh-browse-after-content %}{% endblock %} <script> swh.webapp.initPage('browse'); </script> {% endblock %} diff --git a/swh/web/templates/browse/origin-visits.html b/swh/web/templates/browse/origin-visits.html index 1049cdde..324c76f6 100644 --- a/swh/web/templates/browse/origin-visits.html +++ b/swh/web/templates/browse/origin-visits.html @@ -1,79 +1,82 @@ {% extends "./browse.html" %} {% comment %} Copyright (C) 2017-2018 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 {% endcomment %} {% load static %} {% load swh_templatetags %} {% load render_bundle from webpack_loader %} {% block header %} {{ block.super }} {% render_bundle 'origin' %} {% endblock %} {% block swh-browse-content %} <h4>Overview</h4> <ul> <li class="d-inline-block"> <b>Total number of visits: </b>{{ origin_visits|length }} <i class="mdi mdi-fw" aria-hidden="true"></i> </li> <li class="d-inline-block"> <b>Last full visit: </b><span style="margin-left: 20px;" id="swh-last-full-visit"></span> <i class="mdi mdi-fw" aria-hidden="true"></i> </li> <li class="d-inline-block"> <b>First full visit: </b><span style="margin-left: 20px;" id="swh-first-full-visit"></span> <i class="mdi mdi-fw" aria-hidden="true"></i> </li> <li class="d-inline-block"> <b>Last visit: </b><span style="margin-left: 20px;" id="swh-last-visit"></span> <i class="mdi mdi-fw" aria-hidden="true"></i> </li> </ul> <h4>History</h4> -<div class="text-center"> - <div class="form-check-inline"> - <label class="form-check-label active" onclick="swh.origin.showFullVisitsDifferentSnapshots(event)"> - <input type="radio" class="form-check-input" name="optradio" checked>Show full visits with different snapshots +<form class="text-center"> + <div class="custom-control custom-radio custom-control-inline"> + <input class="custom-control-input" type="radio" id="swh-different-snapshot-visits" name="swh-visits" value="option1" checked> + <label class="custom-control-label font-weight-normal" for="swh-different-snapshot-visits" onclick="swh.origin.showFullVisitsDifferentSnapshots(event)"> + Show full visits with different snapshots </label> </div> - <div class="form-check-inline"> - <label class="form-check-label" onclick="swh.origin.showFullVisits(event)"> - <input type="radio" class="form-check-input" name="optradio">Show all full visits + <div class="custom-control custom-radio custom-control-inline"> + <input class="custom-control-input" type="radio" id="swh-full-visits" name="swh-visits" value="option2"> + <label class="custom-control-label font-weight-normal" for="swh-full-visits" onclick="swh.origin.showFullVisits(event)"> + Show all full visits </label> </div> - <div class="form-check-inline disabled"> - <label class="form-check-label" onclick="swh.origin.showAllVisits(event)"> - <input type="radio" class="form-check-input" name="optradio">Show all visits + <div class="custom-control custom-radio custom-control-inline"> + <input class="custom-control-input" type="radio" id="swh-all-visits" name="swh-visits" value="option3"> + <label class="custom-control-label font-weight-normal" for="swh-all-visits" onclick="swh.origin.showAllVisits(event)"> + Show all visits </label> </div> -</div> +</form> <h5>Calendar</h5> <div id="swh-visits-calendar"></div> <h5>List</h5> <div id="swh-visits-list"></div> <h5>Timeline</h5> <div id="swh-visits-timeline" class="d3-wrapper"></div> <script> // all origin visits var visits = {{ origin_visits|jsonify }}; swh.origin.initVisitsReporting(visits); </script> {% endblock %} diff --git a/swh/web/templates/browse/revision-log.html b/swh/web/templates/browse/revision-log.html index 4b1bfc82..8863d189 100644 --- a/swh/web/templates/browse/revision-log.html +++ b/swh/web/templates/browse/revision-log.html @@ -1,119 +1,119 @@ {% extends "./browse.html" %} {% comment %} Copyright (C) 2017-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 {% endcomment %} {% load render_bundle from webpack_loader %} {% load swh_templatetags %} {% block header %} {{ block.super }} {% render_bundle 'revision' %} {% endblock %} {% block swh-browse-content %} {% if snapshot_context %} {% include "includes/top-navigation.html" %} {% endif %} {% if snapshot_context and snapshot_context.is_empty %} {% include "includes/empty-snapshot.html" %} {% else %} <hr class="mt-0 mb-0"> - <div class="text-center"> + <form class="text-center"> sort by: - <div class="form-check form-check-inline" title="reverse chronological order"> - <input class="form-check-input" type="radio" name="revs-ordering" id="revs-ordering-date" + <div class="custom-control custom-radio custom-control-inline" title="reverse chronological order"> + <input class="custom-control-input" type="radio" name="revs-ordering" id="revs-ordering-date" value="" onclick="swh.revision.revsOrderingTypeClicked(event)" checked> - <label class="form-check-label active" for="revs-ordering-date">revision date</label> + <label class="custom-control-label font-weight-normal" for="revs-ordering-date">revision date</label> </div> - <div class="form-check form-check-inline" title="pre-order, depth-first visit on the revision graph"> - <input class="form-check-input" type="radio" name="revs-ordering" id="revs-ordering-dfs" + <div class="custom-control custom-radio custom-control-inline" title="pre-order, depth-first visit on the revision graph"> + <input class="custom-control-input" type="radio" name="revs-ordering" id="revs-ordering-dfs" value="dfs" onclick="swh.revision.revsOrderingTypeClicked(event)"> - <label class="form-check-label" for="revs-ordering-dfs">DFS</label> + <label class="custom-control-label font-weight-normal" for="revs-ordering-dfs">DFS</label> </div> - <div class="form-check form-check-inline" title="post-order, depth-first visit on the revision graph"> - <input class="form-check-input" type="radio" name="revs-ordering" id="revs-ordering-dfs-post" + <div class="custom-control custom-radio custom-control-inline" title="post-order, depth-first visit on the revision graph"> + <input class="custom-control-input" type="radio" name="revs-ordering" id="revs-ordering-dfs-post" value="dfs_post" onclick="swh.revision.revsOrderingTypeClicked(event)"> - <label class="form-check-label" for="revs-ordering-dfs-post">DFS post-ordering</label> + <label class="custom-control-label font-weight-normal" for="revs-ordering-dfs-post">DFS post-ordering</label> </div> - <div class="form-check form-check-inline" title="breadth-first visit on the revision graph"> - <input class="form-check-input" type="radio" name="revs-ordering" id="revs-ordering-bfs" + <div class="custom-control custom-radio custom-control-inline" title="breadth-first visit on the revision graph"> + <input class="custom-control-input" type="radio" name="revs-ordering" id="revs-ordering-bfs" value="bfs" onclick="swh.revision.revsOrderingTypeClicked(event)"> - <label class="form-check-label" for="revs-ordering-bfs">BFS</label> + <label class="custom-control-label font-weight-normal" for="revs-ordering-bfs">BFS</label> </div> - </div> + </form> <div class="table-responsive mb-3"> <table class="table swh-table swh-table-striped"> <thead> <tr> <th><i class="{{ swh_object_icons.revision }} mdi-fw" aria-hidden="true"></i>Revision</th> <th>Author</th> <th>Date</th> <th>Message</th> <th>Commit Date</th> </tr> </thead> <tbody> {% for rev in revision_log %} <tr class="swh-revision-log-entry swh-tr-hover-highlight" title="{{ rev.tooltip }}"> <td class="swh-revision-log-entry-id"> <a href="{{ rev.url }}"> <i class="{{ swh_object_icons|key_value:'revision' }} mdi-fw" aria-hidden="true"></i>{{ rev.id }} </a> </td> <td class="swh-revision-log-entry-author"> {{ rev.author }} </td> <td class="swh-revision-log-entry-date"> {{ rev.date }} </td> <td class="swh-log-entry-message swh-table-cell-text-overflow"> {{ rev.message }} </td> <td class="swh-revision-log-entry-commit-date"> {{ rev.commit_date }} </td> </tr> {% endfor %} </tbody> </table> </div> <script> swh.revision.initRevisionsLog(); </script> {% endif %} {% endblock %} {% block swh-browse-after-content %} {% if not snapshot_context or not snapshot_context.is_empty %} <ul class="pagination justify-content-center"> {% if next_log_url %} <li class="page-item"> <a class="page-link" href="{{ next_log_url }}">{% if revs_ordering %}Previous{% else %}Newer{% endif %}</a> </li> {% else %} <li class="page-item disabled"> <a class="page-link">{% if revs_ordering %}Previous{% else %}Newer{% endif %}</a> </li> {% endif %} {% if prev_log_url %} <li class="page-item"> <a class="page-link" href="{{ prev_log_url }}">{% if revs_ordering %}Next{% else %}Older{% endif %}</a> </li> {% else %} <li class="page-item disabled"> <a class="page-link">{% if revs_ordering %}Next{% else %}Older{% endif %}</a> </li> {% endif %} </ul> {% endif %} {% endblock %} diff --git a/swh/web/templates/browse/search.html b/swh/web/templates/browse/search.html index 7ff29445..5ea1e6fb 100644 --- a/swh/web/templates/browse/search.html +++ b/swh/web/templates/browse/search.html @@ -1,75 +1,81 @@ {% extends "./layout.html" %} {% comment %} Copyright (C) 2017-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 {% endcomment %} {% load static %} {% block navbar-content %} <h4>Search archived software</h4> {% endblock %} {% block browse-content %} <form class="form-horizontal" id="swh-search-origins"> <div class="input-group"> <input class="form-control" placeholder="Enter a persistent id to resolve or string pattern(s) to search for in origin urls" type="text" id="origins-url-patterns"/> <div class="input-group-append"> - <button class="btn btn-default" type="submit"><i class="swh-search-icon mdi mdi-24px mdi-magnify" aria-hidden="true"></i></button> + <button class="btn btn-primary" type="submit"><i class="swh-search-icon mdi mdi-24px mdi-magnify" aria-hidden="true"></i></button> </div> </div> - <div class="form-check swh-id-option"> - <input class="form-check-input" value="option-origins-with-visit" type="checkbox" + <div class="custom-control custom-checkbox swh-id-option"> + <input class="custom-control-input" value="option-origins-with-visit" type="checkbox" id="swh-search-origins-with-visit" checked> - <label class="form-check-label" for="swh-search-origins-with-visit">only show origins visited at least once</label> + <label class="custom-control-label font-weight-normal" for="swh-search-origins-with-visit"> + only show origins visited at least once + </label> </div> - <div class="form-check swh-id-option"> - <input class="form-check-input" value="option-filter-empty-visits" type="checkbox" + <div class="custom-control custom-checkbox swh-id-option"> + <input class="custom-control-input" value="option-filter-empty-visits" type="checkbox" id="swh-filter-empty-visits" checked> - <label class="form-check-label" for="swh-filter-empty-visits">filter out origins with no archived content</label> + <label class="custom-control-label font-weight-normal" for="swh-filter-empty-visits"> + filter out origins with no archived content + </label> </div> - <div class="form-check swh-id-option"> - <input class="form-check-input" value="option-filter-empty-visits" type="checkbox" + <div class="custom-control custom-checkbox swh-id-option"> + <input class="custom-control-input" value="option-filter-empty-visits" type="checkbox" id="swh-search-origin-metadata"> - <label class="form-check-label" for="swh-search-origin-metadata">search in metadata (instead of URL)</label> + <label class="custom-control-label font-weight-normal" for="swh-search-origin-metadata"> + search in metadata (instead of URL) + </label> </div> </form> <hr> <div id="swh-origin-search-results" class="mb-3" style="display: none;"> <div class="table-responsive"> <table class="table swh-table swh-table-striped" id="origin-search-results"> <thead> <tr> <th>Origin url</th> <th>Visit type</th> <th>Visit status</th> </tr> </thead> <tbody> </tbody> </table> </div> </div> <div class="swh-loading"> <img src="{% static 'img/swh-spinner.gif' %}"></img> <p>Searching origins ...</p> </div> <p id="swh-no-result" style="display: none; white-space: pre;"> <br/> No origins matching the search criteria were found. </p> <ul class="pagination justify-content-center swh-search-pagination"> <li class="disabled page-item" id="origins-prev-results-button"><a class="page-link" href="#" tabindex="-1">Previous</a></li> <li class="disabled page-item" id="origins-next-results-button"><a class="page-link" href="#" tabindex="-1">Next</a></li> </ul> <script> swh.webapp.initPage('search'); swh.browse.initOriginSearch(); </script> {% endblock %} diff --git a/swh/web/templates/browse/vault-ui.html b/swh/web/templates/browse/vault-ui.html index 99043d42..b99d2ee3 100644 --- a/swh/web/templates/browse/vault-ui.html +++ b/swh/web/templates/browse/vault-ui.html @@ -1,45 +1,50 @@ {% extends "./layout.html" %} {% comment %} Copyright (C) 2017-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 {% endcomment %} {% load render_bundle from webpack_loader %} {% block navbar-content %} <h4>Download archived software</h4> {% endblock %} {% block browse-content %} <p> This interface enables to track the status of the different Software Heritage Vault cooking tasks created while browsing the archive. </p> <p> Once a cooking task is finished, a link will be made available in order to download the associated archive. </p> <button type="button" class="btn btn-default btn-sm" id="vault-remove-tasks">Remove selected tasks</button> <div class="table-responsive mt-3"> <table class="table swh-table swh-table-striped swh-vault-table" id="vault-cooking-tasks"> <thead> <tr> - <th><input type="checkbox" id="vault-tasks-toggle-selection"/></th> + <th> + <div class="custom-control custom-checkbox"> + <input type="checkbox" class="custom-control-input" id="vault-tasks-toggle-selection"> + <label class="custom-control-label" for="vault-tasks-toggle-selection"></label> + </div> + </th> <th style="width: 100px">Object type</th> <th>Object id</th> <th style="width: 350px">Cooking status</th> <th style="width: 320px"></th> </tr> </thead> <tbody></tbody> </table> </div> {% include "includes/vault-common.html" %} <script> swh.webapp.initPage('vault'); swh.vault.initUi(); </script> {% endblock %} diff --git a/swh/web/templates/includes/show-swh-ids.html b/swh/web/templates/includes/show-swh-ids.html index 3892e53f..888d1b41 100644 --- a/swh/web/templates/includes/show-swh-ids.html +++ b/swh/web/templates/includes/show-swh-ids.html @@ -1,98 +1,98 @@ {% comment %} Copyright (C) 2017-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 {% endcomment %} {% load swh_templatetags %} {% if swhids_info %} <div id="swh-identifiers" style="display: none;"> {% if swhids_info|length > 1 %} <a id="right-handle" class="handle ui-slideouttab-handle ui-slideouttab-handle-rounded"><i class="mdi mdi-link-variant mdi-fw" aria-hidden="true"></i>Permalinks</a> {% else %} <a id="right-handle" class="handle ui-slideouttab-handle ui-slideouttab-handle-rounded"><i class="mdi mdi-link-variant mdi-fw" aria-hidden="true"></i>Permalink</a> {% endif %} <div id="swh-identifiers-content"> <p> To reference or cite the objects present in the Software Heritage archive, permalinks based on <a href="https://docs.softwareheritage.org/devel/swh-model/persistent-identifiers.html">persistent identifiers</a> must be used instead of copying and pasting the url from the address bar of the browser (as there is no guarantee the current URI scheme will remain the same over time). <br/> <br/> Select below a type of object currently browsed in order to display its associated persistent identifier and permalink. </p> <ul class="nav nav-pills ml-auto p-2"> {% for swhid_info in swhids_info %} {% if forloop.first %} <li class="nav-item"> <a class="nav-link active" href="#swh-id-tab-{{ swhid_info.object_type }}" data-toggle="tab" onclick="swh.browse.swhIdObjectTypeToggled(event)"> <i class="{{ swh_object_icons|key_value:swhid_info.object_type }} mdi-fw" aria-hidden="true"></i>{{ swhid_info.object_type }} </a> </li> {% else %} <li class="nav-item"> <a class="nav-link" href="#swh-id-tab-{{ swhid_info.object_type }}" data-toggle="tab" onclick="swh.browse.swhIdObjectTypeToggled(event)"> <i class="{{ swh_object_icons|key_value:swhid_info.object_type }} mdi-fw" aria-hidden="true"></i>{{ swhid_info.object_type }} </a> </li> {% endif %} {% endfor %} </ul> <div class="tab-content"> {% for swhid_info in swhids_info %} {% if forloop.first %} <div class="tab-pane active" id="swh-id-tab-{{ swhid_info.object_type }}"> {% else %} <div class="tab-pane" id="swh-id-tab-{{ swhid_info.object_type }}"> {% endif %} <div class="card"> <div class="card-body swh-id-ui"> {% if snapshot_context and snapshot_context.origin_info %} <img class="swh-badge swh-badge-origin" src="{% url 'swh-badge' 'origin' snapshot_context.origin_info.url %}" onclick="swh.webapp.showBadgeInfoModal('origin', '{{ snapshot_context.origin_info.url }}')" title="Click to display badge integration info"> {% endif %} {% if swhid_info.object_id %} <img class="swh-badge swh-badge-{{ swhid_info.object_type }}" src="{% url 'swh-badge' swhid_info.object_type swhid_info.object_id %}" onclick="swh.webapp.showBadgeInfoModal('{{ swhid_info.object_type }}', $(this).parent().find('.swh-id').text())" title="Click to display badge integration info"> <pre><a class="swh-id" id="{{ swhid_info.swhid }}" href="{{ swhid_info.swhid_url }}">{{ swhid_info.swhid }}</a></pre> {% endif %} {% if swhid_info.swhid_with_context is not None %} <div class="float-left"> <form id="swh-id-options"> - <div class="form-check swh-id-option"> - <input class="form-check-input swh-id-context-option" value="option-origin" type="checkbox" + <div class="custom-control custom-checkbox swh-id-option"> + <input class="custom-control-input swh-id-context-option" value="option-origin" type="checkbox" id="swh-id-context-option-{{ swhid_info.object_type }}" data-swhid-with-context="{{ swhid_info.swhid_with_context }}" onclick="swh.browse.swhIdContextOptionToggled(event)"> - <label class="form-check-label" for="swh-id-context-option-{{ swhid_info.object_type }}">Add contextual information</label> + <label class="custom-control-label font-weight-normal" for="swh-id-context-option-{{ swhid_info.object_type }}">Add contextual information</label> </div> </form> </div> {% endif %} <div class="float-right"> <button type="button" class="btn btn-default btn-sm btn-swh-id-copy" title="Copy persistent identifier to clipboard"> <i class="mdi mdi-content-copy mdi-fw" aria-hidden="true"></i>Copy identifier <button type="button" class="btn btn-default btn-sm btn-swh-id-url-copy" title="Copy url resolving the persistent identifier to clipboard"> <i class="mdi mdi-content-copy mdi-fw" aria-hidden="true"></i>Copy permalink </button> </div> </div> </div> </div> {% endfor %} </div> </div> </div> <script> swh.webapp.setSwhIdsContext({{ swhids_info|jsonify }}); </script> {% endif %}