v{{ swh_web_version }}
v{{ swh_web_version|split:"+"|first }}
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:
+
+ You need to select the line number before proceeding to
next step.
+
+ 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 %}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.
A significant amount of source code has already been ingested in the Software Heritage archive. It currently includes:
As of today the archive already contains and keeps safe for you the following amount of objects:
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.
To reference or cite the objects present in the Software Heritage archive, permalinks based on
SoftWare Heritage persistent IDentifiers (SWHIDs)
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).
Select below a type of object currently browsed in order to display its associated SWHID and permalink.
{{ swhid_info.swhid }}+
{{ swhid_info.swhid }}{% endif %} {% if swhid_info.swhid_with_context is not None %}
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.
Use the form below to proceed. Once a request has been submitted and accepted, it will be processed as soon as possible. You can then check its processing state by visiting this dedicated page.
Processing "take a new snapshot" request ...
You have requested the cooking of the directory with identifier {{ vault_cooking.directory_id }}
into a standard tar.gz archive
.
Are you sure you want to continue ?
You have requested the download of the directory with identifier {{ vault_cooking.directory_id }}
as a standard tar.gz archive
.
Are you sure you want to continue ?
You have requested the cooking of the history heading to revision with identifier {{ vault_cooking.revision_id }}
into a git fast-import archive
.
Are you sure you want to continue ?
You have requested the download of the history heading to revision with identifier {{ vault_cooking.revision_id }}
as a git fast-import archive
.
Are you sure you want to continue ?
The provided email is not well-formed.