mirror of
https://github.com/verdaccio/verdaccio.git
synced 2025-01-20 22:52:46 -05:00
feature: ✨ new sidebar in detail page
This commit is contained in:
parent
691bc97007
commit
1ffd4d7c5f
16 changed files with 455 additions and 53 deletions
|
@ -50,12 +50,16 @@ module.exports = function(route, auth, storage) {
|
|||
});
|
||||
|
||||
route.get('/-/package/:package/dist-tags', can('access'), function(req, res, next) {
|
||||
storage.get_package(req.params.package, {req: req}, function(err, info) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
storage.get_package({
|
||||
name: req.params.package,
|
||||
req,
|
||||
callback: function(err, info) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
next(info['dist-tags']);
|
||||
next(info['dist-tags']);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ module.exports = function(route, auth, storage, config) {
|
|||
const can = Middleware.allow(auth);
|
||||
// TODO: anonymous user?
|
||||
route.get('/:package/:version?', can('access'), function(req, res, next) {
|
||||
storage.get_package(req.params.package, {req: req}, function(err, info) {
|
||||
const getPackageMetaCallback = function(err, info) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
@ -37,6 +37,12 @@ module.exports = function(route, auth, storage, config) {
|
|||
}
|
||||
|
||||
return next( createError[404]('version not found: ' + req.params.version) );
|
||||
};
|
||||
|
||||
storage.get_package({
|
||||
name: req.params.package,
|
||||
req,
|
||||
callback: getPackageMetaCallback,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
const bodyParser = require('body-parser');
|
||||
const express = require('express');
|
||||
const marked = require('marked');
|
||||
const crypto = require('crypto');
|
||||
const _ = require('lodash');
|
||||
const Search = require('../../lib/search');
|
||||
const Middleware = require('./middleware');
|
||||
const match = Middleware.match;
|
||||
|
@ -76,12 +78,16 @@ module.exports = function(config, auth, storage) {
|
|||
if (req.params.scope) {
|
||||
packageName = `@${req.params.scope}/${packageName}`;
|
||||
}
|
||||
storage.get_package(packageName, {req: req}, function(err, info) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
res.set('Content-Type', 'text/plain');
|
||||
next(marked(info.readme || 'ERROR: No README data found!'));
|
||||
storage.get_package({
|
||||
name: packageName,
|
||||
req,
|
||||
callback: function(err, info) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
res.set('Content-Type', 'text/plain');
|
||||
next(marked(info.readme || 'ERROR: No README data found!'));
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -91,22 +97,25 @@ module.exports = function(config, auth, storage) {
|
|||
const packages = [];
|
||||
|
||||
const getPackageInfo = function(i) {
|
||||
storage.get_package(results[i].ref, (err, entry) => {
|
||||
if (!err && entry) {
|
||||
auth.allow_access(entry.name, req.remote_user, function(err, allowed) {
|
||||
if (err || !allowed) {
|
||||
return;
|
||||
}
|
||||
storage.get_package({
|
||||
name: results[i].ref,
|
||||
callback: (err, entry) => {
|
||||
if (!err && entry) {
|
||||
auth.allow_access(entry.name, req.remote_user, function(err, allowed) {
|
||||
if (err || !allowed) {
|
||||
return;
|
||||
}
|
||||
|
||||
packages.push(entry.versions[entry['dist-tags'].latest]);
|
||||
});
|
||||
}
|
||||
packages.push(entry.versions[entry['dist-tags'].latest]);
|
||||
});
|
||||
}
|
||||
|
||||
if (i >= results.length - 1) {
|
||||
next(packages);
|
||||
} else {
|
||||
getPackageInfo(i + 1);
|
||||
}
|
||||
if (i >= results.length - 1) {
|
||||
next(packages);
|
||||
} else {
|
||||
getPackageInfo(i + 1);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -138,6 +147,53 @@ module.exports = function(config, auth, storage) {
|
|||
res.redirect(base);
|
||||
});
|
||||
|
||||
route.get('/sidebar(/@:scope?)?/:package', function(req, res, next) {
|
||||
storage.get_package({
|
||||
name: req.params.package,
|
||||
keepUpLinkData: true,
|
||||
req,
|
||||
callback: function(err, info) {
|
||||
res.set('Content-Type', 'application/json');
|
||||
|
||||
if (!err) {
|
||||
info.latest = info.versions[info['dist-tags'].latest];
|
||||
let propertyToDelete = ['readme', 'versions'];
|
||||
|
||||
_.forEach(propertyToDelete, ((property) => {
|
||||
delete info[property];
|
||||
}));
|
||||
|
||||
let defaultGravatar = 'https://www.gravatar.com/avatar/00000000000000000000000000000000?d=mm';
|
||||
|
||||
if (typeof _.get(info, 'latest.author.email') === 'string') {
|
||||
info.latest.author.avatar = generateGravatarUrl(info.latest.author.email);
|
||||
} else {
|
||||
// _.get can't guarantee author property exist
|
||||
_.set(info, 'latest.author.avatar', defaultGravatar);
|
||||
}
|
||||
|
||||
if (_.get(info, 'latest.contributors.length', 0) > 0) {
|
||||
info.latest.contributors = _.map(info.latest.contributors, (contributor) => {
|
||||
if (typeof contributor.email === 'string') {
|
||||
contributor.avatar = generateGravatarUrl(contributor.email);
|
||||
} else {
|
||||
contributor.avatar = defaultGravatar;
|
||||
}
|
||||
|
||||
return contributor;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
res.end(JSON.stringify(info));
|
||||
} else {
|
||||
res.status(404);
|
||||
res.end();
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// What are you looking for? logout? client side will remove token when user click logout,
|
||||
// or it will auto expire after 24 hours.
|
||||
// This token is different with the token send to npm client.
|
||||
|
@ -145,3 +201,15 @@ module.exports = function(config, auth, storage) {
|
|||
|
||||
return route;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate gravatar url from email address
|
||||
* @param {string} email
|
||||
* @return {string} url
|
||||
*/
|
||||
function generateGravatarUrl(email) {
|
||||
email = email.trim().toLocaleLowerCase();
|
||||
let emailMD5 = crypto.createHash('md5').update(email).digest('hex');
|
||||
|
||||
return `https://www.gravatar.com/avatar/${emailMD5}`;
|
||||
}
|
||||
|
|
|
@ -232,7 +232,7 @@ class Storage implements IStorage {
|
|||
/**
|
||||
* Ensure the dist file remains as the same protocol
|
||||
* @param {Object} hash metadata
|
||||
* @param {String} upLink registry key
|
||||
* @param {String} upLinkKey registry key
|
||||
* @private
|
||||
*/
|
||||
_updateUplinkToRemoteProtocol(hash: DistFile, upLinkKey: string): void {
|
||||
|
|
|
@ -360,28 +360,33 @@ class Storage {
|
|||
uplink with proxy_access rights against {name} and combines results
|
||||
into one json object
|
||||
Used storages: local && uplink (proxy_access)
|
||||
* @param {*} name
|
||||
* @param {*} options
|
||||
* @param {*} callback
|
||||
*/
|
||||
get_package(name, options, callback) {
|
||||
if (_.isFunction(options)) {
|
||||
callback = options, options = {};
|
||||
}
|
||||
|
||||
this.localStorage.getPackageMetadata(name, (err, data) => {
|
||||
* @param {object} options
|
||||
* @property {string} options.name Package Name
|
||||
* @property {object} options.req Express `req` object
|
||||
* @property {boolean} options.keepUpLinkData keep up link info in package meta, last update, etc.
|
||||
* @property {function} options.callback Callback for receive data
|
||||
*/
|
||||
get_package(options) {
|
||||
this.localStorage.getPackageMetadata(options.name, {req: options.req}, (err, data) => {
|
||||
if (err && (!err.status || err.status >= 500)) {
|
||||
// report internal errors right away
|
||||
return callback(err);
|
||||
return options.callback(err);
|
||||
}
|
||||
|
||||
this._syncUplinksMetadata(name, data, options, function(err, result, uplink_errors) {
|
||||
this._syncUplinksMetadata(options.name, data, {req: options.req}, function(err, result, uplink_errors) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
return options.callback(err);
|
||||
}
|
||||
|
||||
const propertyToKeep = [];
|
||||
Array.prototype.push.apply(propertyToKeep, WHITELIST);
|
||||
if (options.keepUpLinkData === true) {
|
||||
propertyToKeep.push('_uplinks');
|
||||
}
|
||||
|
||||
for (let i in result) {
|
||||
if (WHITELIST.indexOf(i) === -1) {
|
||||
if (propertyToKeep.indexOf(i) === -1) { // Remove sections like '_uplinks' from response
|
||||
delete result[i];
|
||||
}
|
||||
}
|
||||
|
@ -391,7 +396,7 @@ class Storage {
|
|||
// npm can throw if this field doesn't exist
|
||||
result._attachments = {};
|
||||
|
||||
callback(null, result, uplink_errors);
|
||||
options.callback(null, result, uplink_errors);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import storage from '../../../utils/storage';
|
|||
|
||||
import classes from './header.scss';
|
||||
import './logo.png';
|
||||
import getRegistryURL from '../../../utils/getRegistryURL';
|
||||
|
||||
export default class Header extends React.Component {
|
||||
state = {
|
||||
|
@ -138,8 +139,7 @@ export default class Header extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
// Don't add slash if it's not a sub directory
|
||||
const registryURL = `${location.origin}${location.pathname === '/' ? '' : location.pathname}`;
|
||||
const registryURL = getRegistryURL();
|
||||
|
||||
return (
|
||||
<header className={ classes.header }>
|
||||
|
|
|
@ -1,16 +1,15 @@
|
|||
|
||||
import React from 'react';
|
||||
import SyntaxHighlighter, {registerLanguage} from 'react-syntax-highlighter/dist/light';
|
||||
import sunburst from 'react-syntax-highlighter/src/styles/sunburst';
|
||||
import js from 'react-syntax-highlighter/dist/languages/javascript';
|
||||
|
||||
import classes from './help.scss';
|
||||
import getRegistryURL from '../../../utils/getRegistryURL';
|
||||
|
||||
registerLanguage('javascript', js);
|
||||
|
||||
const Help = () => {
|
||||
// Don't add slash if it's not a sub directory
|
||||
const registryURL = `${location.origin}${location.pathname === '/' ? '' : location.pathname}`;
|
||||
const registryURL = getRegistryURL();
|
||||
|
||||
return (
|
||||
<div className={classes.help}>
|
||||
|
|
|
@ -18,7 +18,6 @@ const PackageDetail = (props) => {
|
|||
return (
|
||||
<div className={classes.pkgDetail}>
|
||||
<h1 className={ classes.title }>{ props.package }</h1>
|
||||
<hr/>
|
||||
<div className={classes.readme}>
|
||||
{displayState(props.readMe)}
|
||||
</div>
|
||||
|
|
|
@ -2,8 +2,13 @@
|
|||
|
||||
.pkgDetail {
|
||||
.title {
|
||||
font-size: 28px;
|
||||
color: $text-black;
|
||||
font-size: 38px;
|
||||
color: $primary-color;
|
||||
border-bottom: 1px solid $border-color;
|
||||
text-transform: capitalize;
|
||||
font-weight: 600;
|
||||
margin: 0 0 10px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.readme {
|
||||
|
|
182
src/webui/src/components/PackageSidebar/index.jsx
Normal file
182
src/webui/src/components/PackageSidebar/index.jsx
Normal file
|
@ -0,0 +1,182 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import _ from 'lodash';
|
||||
|
||||
import API from '../../../utils/api';
|
||||
|
||||
import classes from './style.scss';
|
||||
import getRegistryURL from '../../../utils/getRegistryURL';
|
||||
|
||||
export default class PackageSidebar extends React.Component {
|
||||
state = {
|
||||
lastUpdate: 'Loading',
|
||||
recentReleases: [],
|
||||
author: null,
|
||||
contributors: null,
|
||||
showAllContributors: false,
|
||||
dependencies: {}
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.showAllContributors = this.showAllContributors.bind(this);
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
let packageMeta;
|
||||
|
||||
try {
|
||||
packageMeta = (await API.get(`sidebar/${this.props.packageName}`)).data;
|
||||
} catch (err) {
|
||||
this.setState({
|
||||
failed: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let lastUpdate = 0;
|
||||
Object.keys(packageMeta._uplinks).forEach((upLinkName) => {
|
||||
const status = packageMeta._uplinks[upLinkName];
|
||||
|
||||
if (status.fetched > lastUpdate) {
|
||||
lastUpdate = status.fetched;
|
||||
}
|
||||
});
|
||||
|
||||
let recentReleases = Object.keys(packageMeta.time).map((version) => {
|
||||
return {
|
||||
version,
|
||||
time: packageMeta.time[version]
|
||||
};
|
||||
});
|
||||
recentReleases = recentReleases.slice(recentReleases.length - 3, recentReleases.length).reverse();
|
||||
|
||||
this.setState({
|
||||
lastUpdate: (new Date(lastUpdate)).toLocaleString(),
|
||||
recentReleases,
|
||||
author: packageMeta.latest.author,
|
||||
contributors: packageMeta.latest.contributors,
|
||||
dependencies: packageMeta.latest.dependencies,
|
||||
showAllContributors: _.size(packageMeta.latest.contributors) <= 5
|
||||
});
|
||||
}
|
||||
|
||||
showAllContributors() {
|
||||
this.setState({
|
||||
showAllContributors: true
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
let {author, contributors, recentReleases, lastUpdate, showAllContributors, dependencies} = this.state;
|
||||
|
||||
let uniqueContributors = [];
|
||||
if (contributors) {
|
||||
uniqueContributors = _.filter(_.uniqBy(contributors, (contributor) => contributor.name), (contributor) => {
|
||||
return contributor.name !== _.get(author, 'name');
|
||||
})
|
||||
.slice(0, 5);
|
||||
}
|
||||
|
||||
return (
|
||||
<aside>
|
||||
<Module
|
||||
title="Last Sync"
|
||||
description={lastUpdate}
|
||||
className={classes.releasesModule}
|
||||
>
|
||||
<ul>
|
||||
{recentReleases.length > 0 && recentReleases.map((versionInfo) => {
|
||||
return (
|
||||
<li key={versionInfo.version}>
|
||||
<span>{versionInfo.version}</span>
|
||||
<span>{(new Date(versionInfo.time)).toLocaleString()}</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</Module>
|
||||
|
||||
<Module
|
||||
title="Maintainers"
|
||||
className={classes.authorsModule}
|
||||
>
|
||||
<ul>
|
||||
{author && <MaintainerInfo title="Author" name={author.name} avatar={author.avatar}/>}
|
||||
{contributors && (showAllContributors ? contributors : uniqueContributors).map((contributor, index) => {
|
||||
return <MaintainerInfo key={index} title="Contributors" name={contributor.name} avatar={contributor.avatar}/>;
|
||||
})}
|
||||
</ul>
|
||||
{!this.state.showAllContributors && (
|
||||
<button
|
||||
onClick={this.showAllContributors}
|
||||
className={classes.showAllContributors}
|
||||
title="Current list only show the author and first 5 contributors unique by name"
|
||||
>
|
||||
Show all contributor
|
||||
</button>
|
||||
)}
|
||||
</Module>
|
||||
|
||||
{/* Package management module? Help us implement it! */}
|
||||
|
||||
<Module
|
||||
title="Dependencies"
|
||||
className={classes.dependenciesModule}
|
||||
>
|
||||
<ul>
|
||||
{_.size(dependencies) ? (
|
||||
Object.keys(dependencies).map((dependenceName, index) => {
|
||||
return (
|
||||
<li key={index} title={`Depend on version: ${dependencies[dependenceName]}`}>
|
||||
<a href={`${getRegistryURL()}/#/detail/${dependenceName}`} target="_blank">{dependenceName}</a>
|
||||
{index + 1 < _.size(dependencies) && <span>, </span>}
|
||||
</li>
|
||||
);
|
||||
})
|
||||
): <p className={classes.emptyPlaceholder}>Zero dependencies</p>}
|
||||
</ul>
|
||||
</Module>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
}
|
||||
PackageSidebar.propTypes = {
|
||||
packageName: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
function Module(props) {
|
||||
return (
|
||||
<div className={`${classes.module} ${props.className}`}>
|
||||
<h2 className={classes.moduleTitle}>
|
||||
{props.title}
|
||||
{props.description && <span>{props.description}</span>}
|
||||
</h2>
|
||||
<div>
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Module.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
description: PropTypes.string,
|
||||
children: PropTypes.any.isRequired,
|
||||
className: PropTypes.string
|
||||
};
|
||||
|
||||
function MaintainerInfo(props) {
|
||||
let avatarDescription = `${props.title} ${props.name}'s avatar`;
|
||||
return (
|
||||
<div className={classes.maintainer} title={props.name}>
|
||||
<img src={props.avatar} alt={avatarDescription} title={avatarDescription}/>
|
||||
<span>{props.name}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
MaintainerInfo.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
avatar: PropTypes.string.isRequired
|
||||
};
|
90
src/webui/src/components/PackageSidebar/style.scss
Normal file
90
src/webui/src/components/PackageSidebar/style.scss
Normal file
|
@ -0,0 +1,90 @@
|
|||
@import '../../styles/variable';
|
||||
|
||||
.module {
|
||||
.moduleTitle {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
font-size: 24px;
|
||||
color: $primary-color;
|
||||
margin: 0 0 10px;
|
||||
padding: 5px 0;
|
||||
font-weight: 600;
|
||||
border-bottom: 1px solid $border-color;
|
||||
|
||||
span { // description
|
||||
font-size: 14px;
|
||||
color: $text-grey;
|
||||
margin-left: auto;
|
||||
font-weight: lighter;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.releasesModule {
|
||||
li {
|
||||
display: flex;
|
||||
font-size: 14px;
|
||||
line-height: 2;
|
||||
span:last-child {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.authorsModule {
|
||||
.maintainer {
|
||||
$mine-height: 30px;
|
||||
display: flex;
|
||||
line-height: $mine-height;
|
||||
cursor: default;
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
img {
|
||||
width: $mine-height;
|
||||
height: $mine-height;
|
||||
margin-right: 10px;
|
||||
border-radius: 100%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
span {
|
||||
font-size: 14px;
|
||||
flex-shrink: 1;
|
||||
white-space: nowrap;
|
||||
word-break: break-all;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.showAllContributors {
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 14px;
|
||||
color: $primary-color;
|
||||
text-align: center;
|
||||
padding: 10px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.dependenciesModule {
|
||||
li {
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
|
||||
a {
|
||||
color: $primary-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.emptyPlaceholder {
|
||||
text-align: center;
|
||||
margin: 20px 0;
|
||||
font-size: 16px;
|
||||
color: $text-grey;
|
||||
}
|
|
@ -1,2 +1,25 @@
|
|||
@import '../../styles/variable';
|
||||
|
||||
.twoColumn {
|
||||
@include container-size();
|
||||
margin: auto 10px;
|
||||
display: flex;
|
||||
|
||||
> div {
|
||||
&:first-child {
|
||||
flex-shrink: 1;
|
||||
min-width: 300px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
> aside {
|
||||
&:last-child {
|
||||
margin-left: auto;
|
||||
|
||||
padding-left: 15px;
|
||||
flex-shrink: 0;
|
||||
width: 285px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,9 @@ import PackageDetail from '../../components/PackageDetail';
|
|||
import NotFound from '../../components/NotFound';
|
||||
import API from '../../../utils/api';
|
||||
|
||||
import classes from './detail.scss';
|
||||
import PackageSidebar from '../../components/PackageSidebar/index';
|
||||
|
||||
const loadingMessage = 'Loading...';
|
||||
|
||||
export default class Detail extends React.Component {
|
||||
|
@ -39,6 +42,11 @@ export default class Detail extends React.Component {
|
|||
} else if (isEmpty(this.state.readMe)) {
|
||||
return <Loading text={loadingMessage} />;
|
||||
}
|
||||
return <PackageDetail readMe={this.state.readMe} package={this.props.match.params.package}/>;
|
||||
return (
|
||||
<div className={classes.twoColumn}>
|
||||
<PackageDetail readMe={this.state.readMe} package={this.props.match.params.package}/>
|
||||
<PackageSidebar packageName={this.props.match.params.package} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,13 @@
|
|||
body {
|
||||
font-family: $font;
|
||||
font-size: 12px;
|
||||
color: $text-black;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
:global {
|
||||
|
|
|
@ -4,13 +4,15 @@
|
|||
margin-right: auto;
|
||||
width: 100%;
|
||||
min-width: 400px;
|
||||
max-width: 960px;
|
||||
max-width: 1140px;
|
||||
}
|
||||
|
||||
$space-lg: 30px;
|
||||
$font: "Arial";
|
||||
// Font family from Bootstrap v4 Reboot.css
|
||||
$font: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||
|
||||
/* Colors */
|
||||
$primary-color: #de4136;
|
||||
$border-color: #e4e8f1;
|
||||
$border-color: #e3e3e3;
|
||||
$text-black: #3c3c3c;
|
||||
$text-grey: #95989A;
|
||||
|
|
4
src/webui/utils/getRegistryURL.js
Normal file
4
src/webui/utils/getRegistryURL.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
export default function getRegistryURL() {
|
||||
// Don't add slash if it's not a sub directory
|
||||
return `${location.origin}${location.pathname === '/' ? '' : location.pathname}`;
|
||||
}
|
Loading…
Add table
Reference in a new issue