diff --git a/assets/config/webpack.config.development.js b/assets/config/webpack.config.development.js index 8660afd5..504bb449 100644 --- a/assets/config/webpack.config.development.js +++ b/assets/config/webpack.config.development.js @@ -1,482 +1,487 @@ /** * 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 */ // webpack configuration for compiling static assets in development mode // import required node modules and webpack plugins const chalk = require('chalk'); const fs = require('fs'); const path = require('path'); const webpack = require('webpack'); const BundleTracker = require('webpack-bundle-tracker'); const RobotstxtPlugin = require('robotstxt-webpack-plugin'); const CleanWebpackPlugin = require('clean-webpack-plugin').CleanWebpackPlugin; const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const FixSwhSourceMapsPlugin = require('./webpack-plugins/fix-swh-source-maps-webpack-plugin'); const CopyWebpackPlugin = require('copy-webpack-plugin'); const GenerateWebLabelsPlugin = require('./webpack-plugins/generate-weblabels-webpack-plugin'); const ProgressBarPlugin = require('progress-bar-webpack-plugin'); const DumpHighlightjsLanguagesDataPlugin = require('./webpack-plugins/dump-highlightjs-languages-data-plugin'); // are we running webpack-dev-server ? const isDevServer = process.argv.find(v => v.includes('serve')) !== undefined; // webpack-dev-server configuration const devServerPort = 3000; const devServerPublicPath = 'http://localhost:' + devServerPort + '/static/'; // set publicPath according if we are using webpack-dev-server to serve // our assets or not const publicPath = isDevServer ? devServerPublicPath : '/static/'; const nodeModules = path.resolve(__dirname, '../../node_modules/'); // collect all bundles we want to produce with webpack var bundles = {}; const bundlesDir = path.join(__dirname, '../src/bundles'); fs.readdirSync(bundlesDir).forEach(file => { bundles[file] = ['bundles/' + file + '/index.js']; // workaround for https://github.com/webpack/webpack-dev-server/issues/2692 if (isDevServer) { bundles[file].unshift(`webpack-dev-server/client/index.js?http://localhost:${devServerPort}`); } }); // common loaders for css related assets (css, sass) const cssLoaders = [ MiniCssExtractPlugin.loader, { loader: 'cache-loader' }, { loader: 'css-loader', options: { sourceMap: !isDevServer } }, { loader: 'postcss-loader', options: { sourceMap: !isDevServer, postcssOptions: { plugins: [ // lint swh-web stylesheets ['stylelint', { 'config': { 'extends': 'stylelint-config-standard', 'rules': { 'indentation': 4, 'font-family-no-missing-generic-family-keyword': null, 'no-descending-specificity': null }, 'ignoreFiles': ['node_modules/**/*.css', 'assets/src/thirdparty/**/*.css'] } }], // automatically add vendor prefixes to css rules 'autoprefixer', 'postcss-normalize', ['postcss-reporter', { clearReportedMessages: true }] ] } } } ]; // webpack development configuration module.exports = { // use caching to speedup incremental builds cache: { type: 'memory' }, // set mode to development mode: 'development', // workaround for https://github.com/webpack/webpack-dev-server/issues/2758 target: process.env.NODE_ENV === 'development' ? 'web' : 'browserslist', // use eval source maps when using webpack-dev-server for quick debugging, // otherwise generate source map files (more expensive) devtool: isDevServer ? 'eval' : 'source-map', // webpack-dev-server configuration devServer: { clientLogLevel: 'warning', host: '0.0.0.0', port: devServerPort, publicPath: devServerPublicPath, // enable to serve static assets not managed by webpack contentBase: path.resolve('./'), // we do not use hot reloading here (as a framework like React needs to be used in order to fully benefit from that feature) // and prefer to fully reload the frontend application in the browser instead hot: false, inline: true, historyApiFallback: true, headers: { 'Access-Control-Allow-Origin': '*' }, compress: true, stats: 'errors-only', overlay: { warnings: true, errors: true }, // workaround for https://github.com/webpack/webpack-dev-server/issues/2692 injectClient: false, transportMode: 'ws' }, // set entries to the bundles we want to produce entry: bundles, // assets output configuration output: { path: path.resolve('./static/'), filename: 'js/[name].[contenthash].js', chunkFilename: 'js/[name].[contenthash].js', publicPath: publicPath, // each bundle will be compiled as a umd module with its own namespace // in order to easily use them in django templates library: ['swh', '[name]'], libraryTarget: 'umd' }, // module resolving configuration resolve: { // alias pdfjs to its minified version alias: { 'pdfjs-dist': 'pdfjs-dist/build/pdf.min.js' }, // configure base paths for resolving modules with webpack modules: [ 'node_modules', path.resolve(__dirname, '../src') ] }, stats: 'errors-warnings', // module import configuration module: { rules: [ { // Preprocess all js files with eslint for consistent code style // and avoid bad js development practices. enforce: 'pre', test: /\.js$/, exclude: /node_modules/, use: [{ loader: 'eslint-loader', options: { configFile: path.join(__dirname, '.eslintrc'), ignorePath: path.join(__dirname, '.eslintignore'), cache: true, emitWarning: true } }] }, { // Use babel-loader in order to use es6 syntax in js files // but also advanced js features like async/await syntax. // All code get transpiled to es5 in order to be executed // in a large majority of browsers. test: /\.js$/, exclude: /node_modules/, use: [ { loader: 'cache-loader' }, { loader: 'babel-loader', options: { presets: [ // use env babel presets to benefit from es6 syntax ['@babel/preset-env', { // Do not transform es6 module syntax to another module type // in order to benefit from dead code elimination (aka tree shaking) // when running webpack in production mode 'loose': true, 'modules': false }] ], plugins: [ // use babel transform-runtime plugin in order to use aync/await syntax ['@babel/plugin-transform-runtime', { 'regenerator': true }], // use other babel plugins to benefit from advanced js features (es2017) '@babel/plugin-syntax-dynamic-import' ], env: { test: { plugins: ['istanbul'] } } } }] }, { test: /\.ejs$/, use: [{ loader: 'ejs-compiled-loader', options: { htmlmin: true, htmlminOptions: { removeComments: true } } }] }, // expose jquery to the global context as $ and jQuery when importing it { test: require.resolve('jquery'), use: [{ loader: 'expose-loader', options: { exposes: [ { globalName: '$', override: true }, { globalName: 'jQuery', override: true } ] } }] }, // expose highlightjs to the global context as hljs when importing it { test: require.resolve('highlight.js'), use: [{ loader: 'expose-loader', options: { exposes: { globalName: 'hljs', override: true } } }] }, { test: require.resolve('js-cookie'), use: [{ loader: 'expose-loader', options: { exposes: { globalName: 'Cookies', override: true } } }] }, // css import configuration: // - first process it with postcss // - then extract it to a dedicated file associated to each bundle { test: /\.css$/, use: cssLoaders }, // sass import configuration: // - generate css with sass-loader // - process it with postcss // - then extract it to a dedicated file associated to each bundle { test: /\.scss$/, use: cssLoaders.concat([ { loader: 'sass-loader', options: { sourceMap: !isDevServer } } ]) }, // web fonts import configuration { test: /\.(woff|woff2)(\?v=\d+\.\d+\.\d+)?$/, use: [{ loader: 'file-loader', options: { name: '[name].[ext]', outputPath: 'fonts/' } }] }, { test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, use: [{ loader: 'file-loader', options: { name: '[name].[ext]', outputPath: 'fonts/' } }] }, { test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, use: [{ loader: 'file-loader', options: { name: '[name].[ext]', outputPath: 'fonts/' } }] }, { test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, use: [{ loader: 'file-loader', options: { name: '[name].[ext]', outputPath: 'fonts/' } }] }, { test: /\.otf(\?v=\d+\.\d+\.\d+)?$/, use: [{ loader: 'file-loader', options: { name: '[name].[ext]', outputPath: 'fonts/' } }] }, { test: /\.png$/, use: [{ loader: 'file-loader', options: { name: '[name].[ext]', outputPath: 'img/thirdParty/' } }] + }, + { + test: /\.ya?ml$/, + type: 'json', + use: 'yaml-loader' } ], // tell webpack to not parse already minified files to speedup build process noParse: [path.resolve(nodeModules, 'pdfjs-dist/build/pdf.min.js'), path.resolve(nodeModules, 'mathjax/es5/tex-mml-chtml.js')] }, // webpack plugins plugins: [ // cleanup previously generated assets new CleanWebpackPlugin({ cleanOnceBeforeBuildPatterns: ['**/*', '!xml', '!xml/*', '!img', '!img/*', '!img/logos', '!img/logos/*', '!img/icons', '!img/icons/*', '!json', '!json/*'] }), // needed in order to use django_webpack_loader new BundleTracker({ filename: './static/webpack-stats.json' }), // for generating the robots.txt file new RobotstxtPlugin({ policy: [{ userAgent: '*', disallow: '/api/' }] }), // for extracting all stylesheets in separate css files new MiniCssExtractPlugin({ filename: 'css/[name].[contenthash].css', chunkFilename: 'css/[name].[contenthash].css' }), // fix generated asset sourcemaps to workaround a Firefox issue new FixSwhSourceMapsPlugin(), // define some global variables accessible from js code new webpack.DefinePlugin({ __STATIC__: JSON.stringify(publicPath) }), // needed in order to use bootstrap 4.x new webpack.ProvidePlugin({ Popper: ['popper.js', 'default'], Alert: 'exports-loader?Alert!bootstrap/js/dist/alert', Button: 'exports-loader?Button!bootstrap/js/dist/button', Carousel: 'exports-loader?Carousel!bootstrap/js/dist/carousel', Collapse: 'exports-loader?Collapse!bootstrap/js/dist/collapse', Dropdown: 'exports-loader?Dropdown!bootstrap/js/dist/dropdown', Modal: 'exports-loader?Modal!bootstrap/js/dist/modal', Popover: 'exports-loader?Popover!bootstrap/js/dist/popover', Scrollspy: 'exports-loader?Scrollspy!bootstrap/js/dist/scrollspy', Tab: 'exports-loader?Tab!bootstrap/js/dist/tab', Tooltip: 'exports-loader?Tooltip!bootstrap/js/dist/tooltip', Util: 'exports-loader?Util!bootstrap/js/dist/util' }), // needed in order to use pdf.js new webpack.IgnorePlugin(/^\.\/pdf.worker.js$/), new CopyWebpackPlugin({ patterns: [ { from: path.resolve(nodeModules, 'pdfjs-dist/build/pdf.worker.min.js'), to: path.resolve(__dirname, '../../static/js/') }, { from: path.resolve(nodeModules, 'mathjax/es5/output/chtml/fonts/woff-v2/**'), to: path.resolve(__dirname, '../../static/fonts/[name].[ext]') } ] }), new GenerateWebLabelsPlugin({ outputType: 'json', exclude: ['mini-css-extract-plugin', 'bootstrap-loader'], srcReplace: { './node_modules/pdfjs-dist/build/pdf.min.js': './node_modules/pdfjs-dist/build/pdf.js', './node_modules/admin-lte/dist/js/adminlte.min.js': './node_modules/admin-lte/dist/js/adminlte.js' }, licenseOverride: { './assets/src/thirdparty/jquery.tabSlideOut/jquery.tabSlideOut.js': { 'spdxLicenseExpression': 'GPL-3.0', 'licenseFilePath': './assets/src/thirdparty/jquery.tabSlideOut/LICENSE' } }, additionalScripts: Object.assign( { 'js/pdf.worker.min.js': [ { 'id': 'pdfjs-dist/build/pdf.worker.js', 'path': './node_modules/pdfjs-dist/build/pdf.worker.js', 'spdxLicenseExpression': 'Apache-2.0', 'licenseFilePath': './node_modules/pdfjs-dist/LICENSE' } ], '/jsreverse/': [ { 'id': 'jsreverse', 'path': '/jsreverse/', 'spdxLicenseExpression': 'AGPL-3.0-or-later', 'licenseFilePath': './LICENSE' } ], 'https://piwik.inria.fr/matomo.js': [ { 'id': 'matomo.js', 'path': 'https://github.com/matomo-org/matomo/blob/master/js/piwik.js', 'spdxLicenseExpression': 'BSD-3-Clause', 'licenseFilePath': 'https://github.com/matomo-org/matomo/blob/master/js/LICENSE.txt' } ] } ) }), new ProgressBarPlugin({ format: chalk.cyan.bold('webpack build of swh-web assets') + ' [:bar] ' + chalk.green.bold(':percent') + ' (:elapsed seconds)', width: 50 }), new DumpHighlightjsLanguagesDataPlugin() ], // webpack optimizations optimization: { // ensure the vendors bundle gets emitted in a single chunk splitChunks: { cacheGroups: { defaultVendors: { test: 'vendors', chunks: 'all', name: 'vendors', enforce: true } } } }, // disable webpack warnings about bundle sizes performance: { hints: false } }; diff --git a/assets/src/bundles/browse/swhid-utils.js b/assets/src/bundles/browse/swhid-utils.js index e0583216..36b5504d 100644 --- a/assets/src/bundles/browse/swhid-utils.js +++ b/assets/src/bundles/browse/swhid-utils.js @@ -1,122 +1,128 @@ /** * 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 ClipboardJS from 'clipboard'; import 'thirdparty/jquery.tabSlideOut/jquery.tabSlideOut'; import 'thirdparty/jquery.tabSlideOut/jquery.tabSlideOut.css'; import {BREAKPOINT_SM} from 'utils/constants'; export function swhIdObjectTypeToggled(event) { event.preventDefault(); $(event.target).tab('show'); } export function swhIdContextOptionToggled(event) { event.stopPropagation(); const swhIdElt = $(event.target).closest('.swhid-ui').find('.swhid'); const swhIdWithContext = $(event.target).data('swhid-with-context'); const swhIdWithContextUrl = $(event.target).data('swhid-with-context-url'); let currentSwhId = swhIdElt.text(); if ($(event.target).prop('checked')) { swhIdElt.attr('href', swhIdWithContextUrl); currentSwhId = swhIdWithContext.replace(/;/g, ';\n'); } else { const pos = currentSwhId.indexOf(';'); if (pos !== -1) { currentSwhId = currentSwhId.slice(0, pos); } swhIdElt.attr('href', '/' + currentSwhId); } swhIdElt.text(currentSwhId); addLinesInfo(); } function addLinesInfo() { const swhIdElt = $('#swhid-tab-content').find('.swhid'); let currentSwhId = swhIdElt.text().replace(/;\n/g, ';'); const lines = []; let linesPart = ';lines='; const linesRegexp = new RegExp(/L(\d+)/g); let line = linesRegexp.exec(window.location.hash); while (line) { lines.push(parseInt(line[1])); line = linesRegexp.exec(window.location.hash); } if (lines.length > 0) { linesPart += lines[0]; } if (lines.length > 1) { linesPart += '-' + lines[1]; } if ($('#swhid-context-option-content').prop('checked')) { currentSwhId = currentSwhId.replace(/;lines=\d+-*\d*/g, ''); if (lines.length > 0) { currentSwhId += linesPart; } swhIdElt.text(currentSwhId.replace(/;/g, ';\n')); swhIdElt.attr('href', '/' + currentSwhId); } } $(document).ready(() => { new ClipboardJS('.btn-swhid-copy', { text: trigger => { const swhId = $(trigger).closest('.swhid-ui').find('.swhid').text(); return swhId.replace(/;\n/g, ';'); } }); new ClipboardJS('.btn-swhid-url-copy', { text: trigger => { const swhIdUrl = $(trigger).closest('.swhid-ui').find('.swhid').attr('href'); return window.location.origin + swhIdUrl; } }); if (window.innerWidth * 0.7 > 1000) { $('#swh-identifiers').css('width', '1000px'); } + // prevent automatic closing of SWHIDs tab during guided tour + // as it is displayed programmatically + function clickScreenToCloseFilter() { + return $('.introjs-overlay').length > 0; + } + const tabSlideOptions = { tabLocation: 'right', - clickScreenToCloseFilters: ['.ui-slideouttab-panel', '.modal'], + clickScreenToCloseFilters: [clickScreenToCloseFilter, '.ui-slideouttab-panel', '.modal'], offset: function() { const width = $(window).width(); if (width < BREAKPOINT_SM) { return '250px'; } else { return '200px'; } } }; // ensure tab scrolling on small screens if (window.innerHeight < 600 || window.innerWidth < 500) { tabSlideOptions['otherOffset'] = '20px'; } // initiate the sliding identifiers tab $('#swh-identifiers').tabSlideOut(tabSlideOptions); // set the tab visible once the close animation is terminated $('#swh-identifiers').css('display', 'block'); $('.swhid-context-option').trigger('click'); // highlighted code lines changed $(window).on('hashchange', () => { addLinesInfo(); }); // highlighted code lines removed $('body').click(() => { addLinesInfo(); }); }); diff --git a/assets/src/bundles/guided_tour/guided-tour-steps.yaml b/assets/src/bundles/guided_tour/guided-tour-steps.yaml new file mode 100644 index 00000000..f3106275 --- /dev/null +++ b/assets/src/bundles/guided_tour/guided-tour-steps.yaml @@ -0,0 +1,306 @@ +# Copyright (C) 2021 The Software Heritage developers +# See the AUTHORS file at the top-level directory of this distribution +# License: GNU Affero General Public License version 3, or any later version +# See top-level LICENSE file for more information + +homepage: + - title: Welcome to the guided tour ! + intro: | + This guided tour will showcase Software Heritage web application + features in order to help you navigate into the archive + + - title: Homepage + intro: | + This is the entry point of Software Heritage web application, + let's see what we can do from here. + + - element: .swh-search-box + title: Search archived software origins + intro: | + An origin corresponds to a location from which a coherent set of + source codes has been obtained, like a git repository, a directory + containing tarballs, etc.

+ Software origins are identified by URLs (git clone URLs for instance).

+ That form enables to search for terms in the full set of archived software + origin URLs. You will be redirected to a dedicated interface displaying search + results. Clicking on an origin URL will then take you to the source code browsing + interface. If you enter a complete archived origin URL, you will be immediately + redirected to its source code browsing interface. + + - element: .swh-origin-save-link + title: Save code now + intro: | + If you haven't found the software origin you were looking for, you can use the + Save Code Now interface to submit a save request that will be immediately processed. + + - element: .swh-vault-link + title: Downloads from the vault + intro: | + Show the list of downloads you requested from the Software Heritage Vault + while browsing the archive.
+ Those downloads correspond to tarballs containing source directories + archived by Software Heritage.
+ That list of downloads is stored in your browser local storage so it + will be persistent across your visits. + + - element: .swh-help-link + title: Launch guided tour + intro: Replay that guided tour. + + - element: "#swh-login" + title: Login or register + intro: | + Come and join our users community with a Software Heritage account. + Click here and register in less than 30 seconds. + When authenticated, you can benefit from extended features like a higher + rate-limit quota for the Web API.

+ If you are already logged in, that link will take you to your user + profile interface where you can generate bearer token for Web API + authentication. + + - element: "#swh-web-api-link" + title: Software Heritage Web API + intro: | + In the Software Heritage Web API documentation you will find the complete list + of endpoints and how to use each one with a detailed example.
+ Please note that the Web API can also be queried from your web browser + through a dedicated HTML interface displaying query results. + + - title: Browsing source code of an archived software origin + intro: | + Come on in, let's introduce the Web UI to browse the content of an + archived software origin. + +browseOrigin: + - title: Browse source code of an archived software origin + intro: | + You just arrived into the first view of the archived source code of an origin. + The displayed source code files are taken from the most recent snapshot taken by + Software Heritage. By default, the content of the HEAD branch is displayed. + Continue your journey and dive deeper into the code and its development history. + + - element: "#swh-origin-url" + title: Software origin URL + intro: | + Here you can find the URL of the archived software origin.
+ Following that link will always bring you back to the code in the HEAD branch + as captured by the latest Software Heritage visit. + position: bottom + + - element: "#swh-go-to-origin" + title: Visit software origin + intro: | + You can visit the software origin URL where source code was captured from + by following that link. + position: bottom + + - element: "#swh-origin-visit" + title: Software Heritage origin visit date + intro: | + Here you can find the date when Software Heritage captured the source code of + that origin.
+ Following that link will always bring you back to the code in the HEAD branch + as captured by that visit. + position: bottom + + - element: "#swh-browse-code-nav-link" + title: Browse source code + intro: | + Here you can browse the source code of a software origin.
+ Clicking on the Code tab will always bring you back to the code in the HEAD branch + for the currently selected Software Heritage visit. + position: bottom + + - element: "#swh-browse-snapshot-branches-nav-link" + title: Browse branches + intro: | + Here you can browse the list of branches for a software origin.
+ Links are offered to browse the source code contained in each branch. + position: bottom + + - element: "#swh-browse-snapshot-releases-nav-link" + title: Browse releases + intro: | + Here you can browse the list of releases for a software origin.
+ Links are offered to browse the source code contained in each release.
+ Please note that for git origins, only annotated tags are considered as releases. + For non annotated git tags, you can browse them in the Branches tab. + position: bottom + + - element: "#swh-browse-origin-visits-nav-link" + title: Browse origin visits + intro: | + Here you can find when Software Heritage captured the source code. + These visits are called snapshots and visualized in various ways: timeline, + calendar and simple list. + Like with a way-back machine, you can travel in time and see the code as it was + when crawled by Software Heritage. + position: bottom + + - element: "#swh-branches-releases-dd" + title: Switch between branches and releases + intro: | + You can easily switch between different branches and releases using this dropdown. + position: bottom + + - element: "#swh-breadcrumbs-container" + title: Current navigation path + intro: | + You can see here the current path you are taking in the code, which will make it + easier to navigate back. + position: bottom + + - element: .swh-tr-link + title: Browse revisions history + intro: | + Display the list of revisions (aka commits) for the current branch in various + orderings. Links are offered to browse source code as it was in each revision. + The list of files changes introduced in each revision can also be computed and + the associated diffs displayed. + position: bottom + + - element: .swh-vault-download + title: Download source code in an archive + intro: | + You can request the creation of an archive in .tar.gz format that will contain + the currently browsed directory. + You can follow the archive creation progress and download it once done by + visiting the Downloads page (link can be found in the left sidebar). + position: bottom + + - element: "#swh-take-new-snashot" + title: Request to save origin again + intro: | + If the archived software origin currently browsed is not synchronized with its + upstream version (for instance when new commits have been issued), you can + explicitly request Software Heritage to take a new snapshot of it. + position: bottom + + - element: "#swh-tip-revision" + title: Branch tip revision + intro: | + Here you can see the latest revision (commit) archived by Software Heritage + for the current branch. + position: bottom + + - element: "#swhids-handle" + title: Display SWHIDs of browsed objects + intro: | + When clicking on this handle, a tab will be displayed containing Software Heritage + IDentifiers of currently browsed objects. + position: left + + - element: "#swh-identifiers" + title: Get SWHIDs of browsed objects + intro: | + In that tab, you can get the SWHIDs of currently browsed objects. + Let's see what we can do from here. + position: left + + - element: "#swhid-object-types" + title: Select archived object type + intro: | + Software Heritage computes identifiers for all archived objects whose type can be: + + Based on the current context, you can get the SWHID of each browsed object in a + dedicated tab. + position: left + + - element: .swh-badges + title: Software Heritage badges + intro: | + You can include Software Heritage badges in the README file of you code repository + to indicate its archival by Software Heritage.
+ Clicking on a badge will show you how to do so depending on your README format. + + - element: .swhid + title: Software Heritage IDentifier (SWHID) + intro: | + Here you can find the SWHID of the selected object. + position: left + + - element: "#swhid-options" + title: Add / remove qualifiers to SWHID + intro: | + Toggle the adding of qualifiers to the SWHID which adds extra information regarding + the context the object has been found. + position: bottom + + - element: "#swhid-copy-buttons" + title: Copy SWHID for a given browsed object + intro: | + You can easily copy to clipboard a SWHID or its permalink using these dedicated + buttons. + position: bottom + + - title: Browsing a source code file + intro: | + Special features are also offered when browsing a source code file.
+ This is what we will see in the next part of that tour. + +browseContent: + + - title: Browsing a source code file + intro: | + You just arrived in the source code view of Software Heritage web application.
+ Extra features are available in it compared to source directory view, let's make + a review of them. + + - element: .swh-tr-link + title: Download source code file + intro: | + You can download the raw bytes of the source code file and save it locally + by using the "Save Page" feature of your browser. + position: bottom + + - element: .chosen-container + title: Select programming language + intro: | + If Software Heritage did not manage to automatically find a programming language + for the browsed source code file or did not find the right one, you can explicitly + set the language to highlight using this dropdown. + position: bottom + + - element: .hljs-ln-numbers[data-line-number="11"] + title: Highlight a source code line + intro: | + Click on the line number to highlight the corresponding line of code.
+ When a line gets selected, it is automatically added in the SWHID qualifiers + for the associated content object. It enables to easily browse back that + specific line of code. + position: bottom + + - element: .hljs-ln-numbers[data-line-number="17"] + title: Highlight a range of source code lines, + intro: | + Hold Shift key and click on the line number to highlight a range of source + code lines.
+ When a range of lines get selected, it is automatically added in the SWHID qualifiers + for the associated content object. It enables to easily browse back that specific + code snippet. + position: bottom + + - title: End of guided tour + intro: | + Thank your for having followed that guided tour !
+ You will be now redirected to the page you were browsing prior launching it. diff --git a/assets/src/bundles/guided_tour/index.js b/assets/src/bundles/guided_tour/index.js new file mode 100644 index 00000000..4c31a2e9 --- /dev/null +++ b/assets/src/bundles/guided_tour/index.js @@ -0,0 +1,182 @@ +/** + * Copyright (C) 2021 The Software Heritage developers + * See the AUTHORS file at the top-level directory of this distribution + * License: GNU Affero General Public License version 3, or any later version + * See top-level LICENSE file for more information + */ + +import * as introJs from 'intro.js'; +import 'intro.js/introjs.css'; +import './swh-introjs.css'; +import guidedTourSteps from './guided-tour-steps.yaml'; +import {disableScrolling, enableScrolling} from 'utils/scrolling'; + +let guidedTour = []; +let tour = null; +let previousElement = null; +// we use a origin available both in production and swh-web tests +// environment to ease tour testing +const originUrl = 'https://github.com/memononen/libtess2'; + +// init guided tour configuration when page loads in order +// to hack on it in cypress tests +$(() => { + // tour is defined by an array of objects containing: + // - URL of page to run a tour + // - intro.js configuration with tour steps + // - optional intro.js callback function for tour interactivity + guidedTour = [ + { + url: Urls.swh_web_homepage(), + introJsOptions: { + disableInteraction: true, + scrollToElement: false, + steps: guidedTourSteps.homepage + } + }, + { + url: `${Urls.browse_origin_directory()}?origin_url=${originUrl}`, + introJsOptions: { + disableInteraction: true, + scrollToElement: false, + steps: guidedTourSteps.browseOrigin + }, + onBeforeChange: function(targetElement) { + // open SWHIDs tab before its tour step + if (targetElement && targetElement.id === 'swh-identifiers') { + if (!$('#swh-identifiers').tabSlideOut('isOpen')) { + $('.introjs-helperLayer, .introjs-tooltipReferenceLayer').hide(); + $('#swh-identifiers').tabSlideOut('open'); + setTimeout(() => { + $('.introjs-helperLayer, .introjs-tooltipReferenceLayer').show(); + tour.nextStep(); + }, 500); + return false; + } + } + return true; + } + }, + { + url: `${Urls.browse_origin_content()}?origin_url=${originUrl}&path=Example/example.c`, + introJsOptions: { + steps: guidedTourSteps.browseContent + }, + onBeforeChange: function(targetElement) { + const lineNumberStart = 11; + const lineNumberEnd = 17; + // forbid move to next step until user clicks on line numbers + if (targetElement && targetElement.dataset.lineNumber === `${lineNumberEnd}`) { + const background = $(`.hljs-ln-numbers[data-line-number="${lineNumberStart}"]`).css('background-color'); + const canGoNext = background !== 'rgba(0, 0, 0, 0)'; + if (!canGoNext && $('#swh-next-step-disabled').length === 0) { + $('.introjs-tooltiptext').append( + `

+ You need to select the line number before proceeding to
next step. +

`); + } + previousElement = targetElement; + return canGoNext; + } else if (previousElement && previousElement.dataset.lineNumber === `${lineNumberEnd}`) { + let canGoNext = true; + for (let i = lineNumberStart; i <= lineNumberEnd; ++i) { + const background = $(`.hljs-ln-numbers[data-line-number="${i}"]`).css('background-color'); + canGoNext = canGoNext && background !== 'rgba(0, 0, 0, 0)'; + if (!canGoNext) { + swh.webapp.resetHighlightedLines(); + swh.webapp.scrollToLine(swh.webapp.highlightLine(lineNumberStart, true)); + if ($('#swh-next-step-disabled').length === 0) { + $('.introjs-tooltiptext').append( + `

+ You need to select the line numbers range from ${lineNumberStart} + to ${lineNumberEnd} before proceeding to next step. +

`); + } + break; + } + } + return canGoNext; + } + previousElement = targetElement; + return true; + } + } + ]; + // init guided tour on page if guided_tour query parameter is present + const searchParams = new URLSearchParams(window.location.search); + if (searchParams && searchParams.has('guided_tour')) { + initGuidedTour(parseInt(searchParams.get('guided_tour'))); + } +}); + +export function getGuidedTour() { + return guidedTour; +} + +export function guidedTourButtonClick(event) { + event.preventDefault(); + initGuidedTour(); +} + +export function initGuidedTour(page = 0) { + if (page >= guidedTour.length) { + return; + } + const pageUrl = new URL(window.location.origin + guidedTour[page].url); + const currentUrl = new URL(window.location.href); + const guidedTourNext = currentUrl.searchParams.get('guided_tour_next'); + currentUrl.searchParams.delete('guided_tour'); + currentUrl.searchParams.delete('guided_tour_next'); + const pageUrlStr = decodeURIComponent(pageUrl.toString()); + const currentUrlStr = decodeURIComponent(currentUrl.toString()); + if (currentUrlStr !== pageUrlStr) { + // go to guided tour page URL if current one does not match + pageUrl.searchParams.set('guided_tour', page); + if (page === 0) { + // user will be redirected to the page he was at the end of the tour + pageUrl.searchParams.set('guided_tour_next', currentUrlStr); + } + window.location = decodeURIComponent(pageUrl.toString()); + } else { + // create intro.js guided tour and configure it + tour = introJs().setOptions(guidedTour[page].introJsOptions); + tour.setOptions({ + 'exitOnOverlayClick': false, + 'showBullets': false + }); + if (page < guidedTour.length - 1) { + // if not on the last page of the tour, rename next button label + // and schedule next page loading when clicking on it + tour.setOption('doneLabel', 'Next page') + .onexit(() => { + // re-enable page scrolling when exiting tour + enableScrolling(); + }) + .oncomplete(() => { + const nextPageUrl = new URL(window.location.origin + guidedTour[page + 1].url); + nextPageUrl.searchParams.set('guided_tour', page + 1); + if (guidedTourNext) { + nextPageUrl.searchParams.set('guided_tour_next', guidedTourNext); + } + window.location.href = decodeURIComponent(nextPageUrl.toString()); + }); + } else { + tour.oncomplete(() => { + enableScrolling(); // re-enable page scrolling when tour is complete + if (guidedTourNext) { + window.location.href = guidedTourNext; + } + }); + } + if (guidedTour[page].hasOwnProperty('onBeforeChange')) { + tour.onbeforechange(guidedTour[page].onBeforeChange); + } + setTimeout(() => { + // run guided tour with a little delay to ensure every asynchronous operations + // after page load have been executed + disableScrolling(); // disable page scrolling with mouse or keyboard while tour runs. + tour.start(); + window.scrollTo(0, 0); + }, 500); + } +}; diff --git a/assets/src/bundles/guided_tour/swh-introjs.css b/assets/src/bundles/guided_tour/swh-introjs.css new file mode 100644 index 00000000..426899b9 --- /dev/null +++ b/assets/src/bundles/guided_tour/swh-introjs.css @@ -0,0 +1,18 @@ +/** + * Copyright (C) 2021 The Software Heritage developers + * See the AUTHORS file at the top-level directory of this distribution + * License: GNU Affero General Public License version 3, or any later version + * See top-level LICENSE file for more information + */ + +.introjs-tooltip { + min-width: 500px; +} + +.introjs-tooltip.introjs-floating { + /* center tooltip not attached to a DOM element to the center of the screen */ + position: fixed !important; + top: 50% !important; + margin: 0 auto !important; + transform: translate(-50%, -50%) !important; +} diff --git a/assets/src/bundles/webapp/code-highlighting.js b/assets/src/bundles/webapp/code-highlighting.js index 5a873869..ac8983ba 100644 --- a/assets/src/bundles/webapp/code-highlighting.js +++ b/assets/src/bundles/webapp/code-highlighting.js @@ -1,114 +1,117 @@ /** * 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 {removeUrlFragment} from 'utils/functions'; -export async function highlightCode(showLineNumbers = true) { +// keep track of the first highlighted line +let firstHighlightedLine = null; +// highlighting color +const lineHighlightColor = 'rgb(193, 255, 193)'; - await import(/* webpackChunkName: "highlightjs" */ 'utils/highlightjs'); +// function to highlight a line +export function highlightLine(i, firstHighlighted = false) { + const lineTd = $(`.hljs-ln-line[data-line-number="${i}"]`); + lineTd.css('background-color', lineHighlightColor); + if (firstHighlighted) { + firstHighlightedLine = i; + } + return lineTd; +} - // keep track of the first highlighted line - let firstHighlightedLine = null; - // highlighting color - const lineHighlightColor = 'rgb(193, 255, 193)'; +// function to reset highlighting +export function resetHighlightedLines() { + firstHighlightedLine = null; + $('.hljs-ln-line[data-line-number]').css('background-color', 'inherit'); +} - // function to highlight a line - function highlightLine(i) { - const lineTd = $(`.hljs-ln-line[data-line-number="${i}"]`); - lineTd.css('background-color', lineHighlightColor); - return lineTd; +export function scrollToLine(lineDomElt) { + if ($(lineDomElt).closest('.swh-content').length > 0) { + $('html, body').animate({ + scrollTop: $(lineDomElt).offset().top - 70 + }, 500); } +} - // function to reset highlighting - function resetHighlightedLines() { - firstHighlightedLine = null; - $('.hljs-ln-line[data-line-number]').css('background-color', 'inherit'); - } +export async function highlightCode(showLineNumbers = true) { - function scrollToLine(lineDomElt) { - if ($(lineDomElt).closest('.swh-content').length > 0) { - $('html, body').animate({ - scrollTop: $(lineDomElt).offset().top - 70 - }, 500); - } - } + await import(/* webpackChunkName: "highlightjs" */ 'utils/highlightjs'); // function to highlight lines based on a url fragment // in the form '#Lx' or '#Lx-Ly' function parseUrlFragmentForLinesToHighlight() { const lines = []; const linesRegexp = new RegExp(/L(\d+)/g); let line = linesRegexp.exec(window.location.hash); if (line === null) { return; } while (line) { lines.push(parseInt(line[1])); line = linesRegexp.exec(window.location.hash); } resetHighlightedLines(); if (lines.length === 1) { firstHighlightedLine = parseInt(lines[0]); scrollToLine(highlightLine(lines[0])); } else if (lines[0] < lines[lines.length - 1]) { firstHighlightedLine = parseInt(lines[0]); scrollToLine(highlightLine(lines[0])); for (let i = lines[0] + 1; i <= lines[lines.length - 1]; ++i) { highlightLine(i); } } } $(document).ready(() => { // highlight code and add line numbers $('code').each((i, elt) => { hljs.highlightElement(elt); if (showLineNumbers) { hljs.lineNumbersElement(elt, {singleLine: true}); } }); if (!showLineNumbers) { return; } // click handler to dynamically highlight line(s) // when the user clicks on a line number (lines range // can also be highlighted while holding the shift key) $('.swh-content').click(evt => { if (evt.target.classList.contains('hljs-ln-n')) { const line = parseInt($(evt.target).data('line-number')); if (evt.shiftKey && firstHighlightedLine && line > firstHighlightedLine) { const firstLine = firstHighlightedLine; resetHighlightedLines(); for (let i = firstLine; i <= line; ++i) { highlightLine(i); } firstHighlightedLine = firstLine; window.location.hash = `#L${firstLine}-L${line}`; } else { resetHighlightedLines(); highlightLine(line); window.location.hash = `#L${line}`; scrollToLine(evt.target); } } else if ($(evt.target).closest('.hljs-ln').length) { resetHighlightedLines(); removeUrlFragment(); } }); // update lines highlighting when the url fragment changes $(window).on('hashchange', () => parseUrlFragmentForLinesToHighlight()); // schedule lines highlighting if any as hljs.lineNumbersElement() is async setTimeout(() => { parseUrlFragmentForLinesToHighlight(); }); }); } diff --git a/assets/src/utils/scrolling.js b/assets/src/utils/scrolling.js new file mode 100644 index 00000000..6b773108 --- /dev/null +++ b/assets/src/utils/scrolling.js @@ -0,0 +1,47 @@ +/** + * Copyright (C) 2021 The Software Heritage developers + * See the AUTHORS file at the top-level directory of this distribution + * License: GNU Affero General Public License version 3, or any later version + * See top-level LICENSE file for more information + */ + +// adapted from https://stackoverflow.com/questions/4770025/how-to-disable-scrolling-temporarily + +// up: 38, down: 40, spacebar: 32, pageup: 33, pagedown: 34, end: 35, home: 36 +const keys = {38: 1, 40: 1, 32: 1, 33: 1, 34: 1, 35: 1, 36: 1}; + +function preventDefault(e) { + e.preventDefault(); +} + +function preventDefaultForScrollKeys(e) { + if (keys[e.keyCode]) { + preventDefault(e); + return false; + } +} + +// modern Chrome requires { passive: false } when adding event +let supportsPassive = false; +try { + window.addEventListener('test', null, Object.defineProperty({}, 'passive', { + get: function() { supportsPassive = true; } + })); +} catch (e) {} + +const wheelOpt = supportsPassive ? {passive: false} : false; +const wheelEvent = 'onwheel' in document.createElement('div') ? 'wheel' : 'mousewheel'; + +export function disableScrolling() { + window.addEventListener('DOMMouseScroll', preventDefault, false); // older FF + window.addEventListener(wheelEvent, preventDefault, wheelOpt); // modern desktop + window.addEventListener('touchmove', preventDefault, wheelOpt); // mobile + window.addEventListener('keydown', preventDefaultForScrollKeys, false); +} + +export function enableScrolling() { + window.removeEventListener('DOMMouseScroll', preventDefault, false); + window.removeEventListener(wheelEvent, preventDefault, wheelOpt); + window.removeEventListener('touchmove', preventDefault, wheelOpt); + window.removeEventListener('keydown', preventDefaultForScrollKeys, false); +} diff --git a/cypress/integration/guided-tour.spec.js b/cypress/integration/guided-tour.spec.js new file mode 100644 index 00000000..67b583f1 --- /dev/null +++ b/cypress/integration/guided-tour.spec.js @@ -0,0 +1,123 @@ +/** + * Copyright (C) 2021 The Software Heritage developers + * See the AUTHORS file at the top-level directory of this distribution + * License: GNU Affero General Public License version 3, or any later version + * See top-level LICENSE file for more information + */ + +describe('Guided Tour Tests', function() { + + // utility function to traverse all guided tour steps in a page + const clickNextStepButtons = (stopAtTitle = null) => { + cy.get('.introjs-nextbutton').then($button => { + const buttonText = $button.text(); + const headerText = $button.parent().siblings('.introjs-tooltip-header').text(); + if (buttonText === 'Next' && headerText.slice(0, -1) !== stopAtTitle) { + cy.get('.introjs-nextbutton') + .click({force: true}) + .then(() => { + cy.get('.introjs-tooltip').should('be.visible'); + clickNextStepButtons(stopAtTitle); + }); + } + }); + }; + + it('should start UI guided tour when clicking on help button', function() { + cy.ambassadorLogin(); + cy.visit('/'); + cy.get('.swh-help-link') + .click(); + + cy.get('.introjs-tooltip') + .should('exist'); + }); + + it('should change guided tour page after current page steps', function() { + cy.ambassadorLogin(); + cy.visit('/'); + + cy.get('.swh-help-link') + .click(); + + cy.url().then(url => { + clickNextStepButtons(); + cy.get('.introjs-nextbutton') + .should('have.text', 'Next page') + .click(); + cy.url().should('not.eq', url); + }); + + }); + + it('should automatically open SWHIDs tab on second page of the guided tour', function() { + const guidedTourPageIndex = 1; + cy.ambassadorLogin(); + cy.visit('/').window().then(win => { + const guidedTour = win.swh.guided_tour.getGuidedTour(); + // jump to third guided tour page + cy.visit(guidedTour[guidedTourPageIndex].url); + cy.window().then(win => { + // SWHIDs tab should be closed when tour begins + cy.get('.ui-slideouttab-open').should('not.exist'); + // init guided tour on the page + win.swh.guided_tour.initGuidedTour(guidedTourPageIndex); + clickNextStepButtons(); + // SWHIDs tab should be opened when tour begins + cy.get('.ui-slideouttab-open').should('exist'); + }); + }); + }); + + it('should stay at step while line numbers not clicked on content view tour', function() { + const guidedTourPageIndex = 2; + cy.ambassadorLogin(); + // jump to third guided tour page + cy.visit('/').window().then(win => { + const guidedTour = win.swh.guided_tour.getGuidedTour(); + cy.visit(guidedTour[guidedTourPageIndex].url); + cy.window().then(win => { + // init guided tour on the page + win.swh.guided_tour.initGuidedTour(guidedTourPageIndex); + + clickNextStepButtons('Highlight a source code line'); + + cy.get('.introjs-tooltip-header').then($header => { + const headerText = $header.text(); + // user did not click yet on line numbers and should stay + // blocked on first step of the tour + cy.get('.introjs-nextbutton') + .click(); + cy.get('.introjs-tooltip-header') + .should('have.text', headerText); + // click on line numbers + cy.get('.hljs-ln-numbers[data-line-number="11"]') + .click(); + // check move to next step is allowed + cy.get('.introjs-nextbutton') + .click(); + cy.get('.introjs-tooltip-header') + .should('not.have.text', headerText); + }); + + cy.get('.introjs-tooltip-header').then($header => { + const headerText = $header.text(); + // user did not click yet on line numbers and should stay + // blocked on first step of the tour + cy.get('.introjs-nextbutton') + .click(); + cy.get('.introjs-tooltip-header') + .should('have.text', headerText); + // click on line numbers + cy.get('.hljs-ln-numbers[data-line-number="17"]') + .click({shiftKey: true}); + // check move to next step is allowed + cy.get('.introjs-nextbutton') + .click(); + cy.get('.introjs-tooltip-header') + .should('not.have.text', headerText); + }); + }); + }); + }); +}); diff --git a/package.json b/package.json index 00a92490..fdaf47aa 100644 --- a/package.json +++ b/package.json @@ -1,136 +1,138 @@ { "name": "swh-web", "version": "0.0.316", "description": "Static assets management for swh-web", "scripts": { "build-dev": "NODE_ENV=development webpack --config assets/config/webpack.config.development.js --color", "build-test": "NODE_ENV=test webpack --config assets/config/webpack.config.development.js --color", "start-dev": "NODE_ENV=development nodemon --watch swh/web/api --watch swh/web/browse --watch swh/web/templates --watch swh/web/common --watch swh/web/settings --watch assets/config --ext py,html,js --exec \"webpack serve --config assets/config/webpack.config.development.js --color\"", "build": "NODE_ENV=production webpack --config assets/config/webpack.config.production.js --color", "mochawesome": "mochawesome-merge cypress/mochawesome/results/*.json > cypress/mochawesome/mochawesome.json && marge -o cypress/mochawesome/report cypress/mochawesome/mochawesome.json", "eslint": "eslint -c assets/config/.eslintrc --fix assets/** cypress/integration/** cypress/plugins/** cypress/support/**", "preinstall": "npm -v || (SWH_WEB=$PWD && cd /tmp && yarn add npm && cd node_modules/npm && yarn link && cd $SWH_WEB && yarn link npm)", "nyc-report": "nyc report --reporter=lcov" }, "repository": { "type": "git", "url": "https://forge.softwareheritage.org/source/swh-web" }, "author": "The Software Heritage developers", "license": "AGPL-3.0-or-later", "dependencies": { "@babel/runtime-corejs3": "^7.14.7", "@mdi/font": "^5.9.55", "@sentry/browser": "^6.8.0", "admin-lte": "^3.1.0", "ansi_up": "^5.0.1", "bootstrap": "^4.6.0", "chosen-js": "^1.8.7", "clipboard": "^2.0.8", "core-js": "^3.15.2", "d3": "^7.0.0", "datatables.net-responsive-bs4": "^2.2.9", "dompurify": "^2.3.0", "highlight.js": "^11.1.0", "highlightjs-line-numbers.js": "^2.8.0", "html-encoder-decoder": "^1.3.9", "iframe-resizer": "^4.3.2", + "intro.js": "^4.1.0", "jquery": "^3.6.0", "js-cookie": "^2.2.1", "js-year-calendar": "^1.0.2", "mathjax": "^3.2.0", "notebookjs": "^0.6.6", "object-fit-images": "^3.2.4", "org": "^0.2.0", "pdfjs-dist": "^2.8.335", "popper.js": "^1.16.1", "showdown": "^1.9.1", "typeface-alegreya": "^1.1.13", "typeface-alegreya-sans": "^1.1.13", "waypoints": "^4.0.1", "whatwg-fetch": "^3.6.2" }, "devDependencies": { "@babel/core": "^7.14.6", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-transform-runtime": "^7.14.5", "@babel/preset-env": "^7.14.7", "@cypress/code-coverage": "^3.9.8", "autoprefixer": "^10.2.6", "axios": "^0.21.1", "babel-eslint": "^10.1.0", "babel-loader": "^8.2.2", "babel-plugin-istanbul": "^6.0.0", "bootstrap-loader": "^3.0.4", "cache-loader": "^4.1.0", "clean-webpack-plugin": "^3.0.0", "copy-webpack-plugin": "^9.0.1", "css-loader": "^5.2.6", "cypress": "^7.7.0", "cypress-hmr-restarter": "^2.0.2", "cypress-multi-reporters": "^1.5.0", "ejs": "^3.1.6", "ejs-compiled-loader": "^3.1.0", "eslint": "^7.30.0", "eslint-loader": "^4.0.2", "eslint-plugin-chai-friendly": "^0.7.1", "eslint-plugin-cypress": "^2.11.3", "eslint-plugin-import": "^2.23.4", "eslint-plugin-node": "^11.1.0", "eslint-plugin-promise": "^5.1.0", "eslint-plugin-standard": "^5.0.0", "exports-loader": "^3.0.0", "expose-loader": "^3.0.0", "file-loader": "^6.2.0", "imports-loader": "^3.0.0", "istanbul-lib-coverage": "^3.0.0", "json-stable-stringify": "^1.0.1", "mini-css-extract-plugin": "^2.1.0", "mocha": "^9.0.2", "mocha-junit-reporter": "^2.0.0", "mochawesome": "^6.2.2", "mochawesome-merge": "^4.2.0", "mochawesome-report-generator": "^5.2.0", "node-sass": "^6.0.1", "nodemon": "^2.0.10", "nyc": "^15.1.0", "optimize-css-assets-webpack-plugin": "^6.0.1", "postcss": "^8.3.5", "postcss-loader": "^6.1.1", "postcss-normalize": "^10.0.0", "postcss-reporter": "^7.0.2", "progress-bar-webpack-plugin": "^2.1.0", "resolve-url-loader": "^4.0.0", "robotstxt-webpack-plugin": "^7.0.0", "sass-loader": "^12.1.0", "schema-utils": "^3.1.0", "script-loader": "^0.7.2", "spdx-expression-parse": "^3.0.1", "style-loader": "^3.0.0", "stylelint": "^13.13.1", "stylelint-config-standard": "^22.0.0", "terser-webpack-plugin": "^5.1.4", "url-loader": "^4.1.1", "webpack": "^5.44.0", "webpack-bundle-tracker": "^1.1.0", "webpack-cli": "^4.7.2", - "webpack-dev-server": "^3.11.2" + "webpack-dev-server": "^3.11.2", + "yaml-loader": "^0.6.0" }, "resolutions": { "jquery": "^3.6.0" }, "browserslist": [ "cover 99.5%", "not dead" ], "nyc": { "report-dir": "cypress/coverage", "exclude": [ "assets/src/bundles/vendors/index.js", "assets/src/thirdparty/**/*.js" ] }, "engines": { "node": ">=12.0.0" } } \ No newline at end of file diff --git a/swh/web/templates/homepage.html b/swh/web/templates/homepage.html index 0bb6a35b..caa259a6 100644 --- a/swh/web/templates/homepage.html +++ b/swh/web/templates/homepage.html @@ -1,117 +1,117 @@ {% 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 static %} {% load render_bundle from webpack_loader %} {% block header %} {% render_bundle 'browse' %} {% endblock %} {% block title %}Welcome to the Software Heritage archive{% endblock %} {% block navbar-content %}

Welcome to the Software Heritage archive

{% endblock %} {% block content %} -
+

... or check our - + Web API

Overview

The long term goal of the Software Heritage initiative is to collect all publicly available software in source code form together with its development history, replicate it massively to ensure its preservation, and share it with everyone who needs it. The Software Heritage archive is growing over time as we crawl new source code from software projects and development forges.

Content

A significant amount of source code has already been ingested in the Software Heritage archive. It currently includes:

Size

As of today the archive already contains and keeps safe for you the following amount of objects:

Source files
0
Commits
0
Projects
0
Directories
0
Authors
0
Releases
0

Note: the counters and graphs above are based on heuristics that might not reflect the exact size of the archive. While the long-term trends shown and ballpark figures are reliable, individual point-in-time values might not be.

{% endblock %} diff --git a/swh/web/templates/includes/revision-info.html b/swh/web/templates/includes/revision-info.html index 87ba16ce..16b48adb 100644 --- a/swh/web/templates/includes/revision-info.html +++ b/swh/web/templates/includes/revision-info.html @@ -1,35 +1,35 @@ {% comment %} -Copyright (C) 2020 The Software Heritage developers +Copyright (C) 2020-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 {% endcomment %} {% if snapshot_context and snapshot_context.revision_id %} -
+
Tip revision: {{ snapshot_context.revision_id }} authored by {{ snapshot_context.revision_info.author.name }} on {{ snapshot_context.revision_info.date }}
{{ snapshot_context.revision_info.message_header }}
{% endif %} \ No newline at end of file diff --git a/swh/web/templates/includes/show-swhids.html b/swh/web/templates/includes/show-swhids.html index 72eb46fd..122ae6a7 100644 --- a/swh/web/templates/includes/show-swhids.html +++ b/swh/web/templates/includes/show-swhids.html @@ -1,102 +1,108 @@ {% comment %} -Copyright (C) 2017-2020 The Software Heritage developers +Copyright (C) 2017-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 {% endcomment %} {% load swh_templatetags %} {% if swhids_info %}