mirror of
https://github.com/verdaccio/verdaccio.git
synced 2025-04-01 02:42:23 -05:00
refactor: 🔨 follow review, split modules to multiple components
This commit is contained in:
parent
ba6811bd91
commit
b2d54e192b
18 changed files with 365 additions and 240 deletions
|
@ -11,7 +11,7 @@ import storage from '../../../utils/storage';
|
|||
|
||||
import classes from './header.scss';
|
||||
import './logo.png';
|
||||
import getRegistryURL from '../../../utils/getRegistryURL';
|
||||
import {getRegistryURL} from '../../../utils/url';
|
||||
|
||||
export default class Header extends React.Component {
|
||||
state = {
|
||||
|
|
|
@ -4,7 +4,7 @@ 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';
|
||||
import {getRegistryURL} from '../../../utils/url';
|
||||
|
||||
registerLanguage('javascript', js);
|
||||
|
||||
|
|
25
src/webui/src/components/PackageSidebar/Module/index.jsx
Normal file
25
src/webui/src/components/PackageSidebar/Module/index.jsx
Normal file
|
@ -0,0 +1,25 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import classes from './style.scss';
|
||||
|
||||
export default function Module({title, description, children, className}) {
|
||||
return (
|
||||
<div className={`${classes.module} ${className}`}>
|
||||
<h2 className={classes.moduleTitle}>
|
||||
{title}
|
||||
{description && <span>{description}</span>}
|
||||
</h2>
|
||||
<div>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Module.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
description: PropTypes.string,
|
||||
children: PropTypes.any.isRequired,
|
||||
className: PropTypes.string
|
||||
};
|
21
src/webui/src/components/PackageSidebar/Module/style.scss
Normal file
21
src/webui/src/components/PackageSidebar/Module/style.scss
Normal file
|
@ -0,0 +1,21 @@
|
|||
@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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import classes from './style.scss';
|
||||
|
||||
export default function ModuleContentPlaceholder({text}) {
|
||||
return <p className={classes.emptyPlaceholder}>{text}</p>;
|
||||
}
|
||||
ModuleContentPlaceholder.propTypes = {
|
||||
text: PropTypes.string.isRequired
|
||||
};
|
|
@ -0,0 +1,8 @@
|
|||
@import '../../../styles/variable';
|
||||
|
||||
.emptyPlaceholder {
|
||||
text-align: center;
|
||||
margin: 20px 0;
|
||||
font-size: 16px;
|
||||
color: $text-grey;
|
||||
}
|
|
@ -1,25 +1,20 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import _ from 'lodash';
|
||||
import LastSync from './modules/LastSync';
|
||||
import Maintainers from './modules/Maintainers';
|
||||
import Dependencies from './modules/Dependencies';
|
||||
|
||||
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: {}
|
||||
state = {};
|
||||
|
||||
static propTypes = {
|
||||
packageName: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.showAllContributors = this.showAllContributors.bind(this);
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
|
@ -44,149 +39,21 @@ export default class PackageSidebar extends React.Component {
|
|||
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: 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
|
||||
packageMeta
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
let {packageMeta} = this.state;
|
||||
|
||||
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>
|
||||
|
||||
<LastSync packageMeta={packageMeta} />
|
||||
<Maintainers packageMeta={packageMeta} />
|
||||
<Dependencies packageMeta={packageMeta} />
|
||||
{/* 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
|
||||
};
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import _ from 'lodash';
|
||||
import Module from '../../Module';
|
||||
|
||||
import classes from './style.scss';
|
||||
import {getDetailPageURL} from '../../../../../utils/url';
|
||||
import ModuleContentPlaceholder from '../../ModuleContentPlaceholder';
|
||||
|
||||
export default class Dependencies extends React.Component {
|
||||
static propTypes = {
|
||||
packageMeta: PropTypes.object
|
||||
};
|
||||
|
||||
get dependencies() {
|
||||
if (!this.props.packageMeta) return {};
|
||||
|
||||
return _.get(this, 'props.packageMeta.latest.dependencies', {});
|
||||
}
|
||||
|
||||
render() {
|
||||
let dependencies = this.dependencies;
|
||||
let dependenciesList = Object.keys(dependencies);
|
||||
|
||||
if (!dependenciesList.length) {
|
||||
return <ModuleContentPlaceholder text="Zero Dependencies!"/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Module
|
||||
title="Dependencies"
|
||||
className={classes.dependenciesModule}
|
||||
>
|
||||
<ul>
|
||||
{
|
||||
dependenciesList.map((dependenceName, index) => {
|
||||
return (
|
||||
<li key={index} title={`Depend on version: ${dependencies[dependenceName]}`}>
|
||||
<a href={getDetailPageURL(dependenceName)}>{dependenceName}</a>
|
||||
{index < dependenciesList.length - 1 && <span>, </span>}
|
||||
</li>
|
||||
);
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
</Module>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
@import '../../../../styles/variable';
|
||||
|
||||
.dependenciesModule {
|
||||
li {
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
|
||||
a {
|
||||
color: $primary-color;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Module from '../../Module';
|
||||
|
||||
import classes from './style.scss';
|
||||
|
||||
export default class LastSync extends React.Component {
|
||||
static propTypes = {
|
||||
packageMeta: PropTypes.object
|
||||
};
|
||||
|
||||
get lastUpdate() {
|
||||
if (!this.props.packageMeta) return 'Loading...';
|
||||
|
||||
let lastUpdate = 0;
|
||||
Object.keys(this.props.packageMeta._uplinks).forEach((upLinkName) => {
|
||||
const status = this.props.packageMeta._uplinks[upLinkName];
|
||||
|
||||
if (status.fetched > lastUpdate) {
|
||||
lastUpdate = status.fetched;
|
||||
}
|
||||
});
|
||||
|
||||
return lastUpdate ? (new Date(lastUpdate)).toLocaleString() : '';
|
||||
}
|
||||
|
||||
get recentReleases() {
|
||||
if (!this.props.packageMeta) return [];
|
||||
|
||||
let recentReleases = Object.keys(this.props.packageMeta.time).map((version) => {
|
||||
return {
|
||||
version,
|
||||
time: new Date(this.props.packageMeta.time[version]).toLocaleString()
|
||||
};
|
||||
});
|
||||
|
||||
return recentReleases.slice(recentReleases.length - 3, recentReleases.length).reverse();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Module
|
||||
title="Last Sync"
|
||||
description={this.lastUpdate}
|
||||
className={classes.releasesModule}
|
||||
>
|
||||
<ul>
|
||||
{this.recentReleases.map((versionInfo) => {
|
||||
return (
|
||||
<li key={versionInfo.version}>
|
||||
<span>{versionInfo.version}</span>
|
||||
<span>{versionInfo.time}</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</Module>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
.releasesModule {
|
||||
li {
|
||||
display: flex;
|
||||
font-size: 14px;
|
||||
line-height: 2;
|
||||
span:last-child {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import classes from './style.scss';
|
||||
|
||||
export default function MaintainerInfo({title, name, avatar}) {
|
||||
let avatarDescription = `${title} ${name}'s avatar`;
|
||||
return (
|
||||
<div className={classes.maintainer} title={name}>
|
||||
<img src={avatar} alt={avatarDescription} title={avatarDescription}/>
|
||||
<span>{name}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
MaintainerInfo.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
avatar: PropTypes.string.isRequired
|
||||
};
|
|
@ -0,0 +1,25 @@
|
|||
.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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import _ from 'lodash';
|
||||
import Module from '../../Module';
|
||||
|
||||
import classes from './style.scss';
|
||||
import MaintainerInfo from './MaintainerInfo';
|
||||
|
||||
export default class Maintainers extends React.Component {
|
||||
static propTypes = {
|
||||
packageMeta: PropTypes.object
|
||||
};
|
||||
|
||||
state = {};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleShowAllContributors = this.handleShowAllContributors.bind(this);
|
||||
}
|
||||
|
||||
get author() {
|
||||
return _.get(this, 'props.packageMeta.latest.author');
|
||||
}
|
||||
|
||||
get contributors() {
|
||||
let contributors = _.get(this, 'props.packageMeta.latest.contributors', {});
|
||||
return _.filter(contributors, (contributor) => {
|
||||
return (
|
||||
contributor.name !== _.get(this, 'author.name') &&
|
||||
contributor.email !== _.get(this, 'author.email')
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
get showAllContributors() {
|
||||
return this.state.showAllContributors || _.size(this.contributors) <= 5;
|
||||
}
|
||||
|
||||
get uniqueContributors() {
|
||||
if (!this.contributors) return [];
|
||||
|
||||
return _.uniqBy(this.contributors, (contributor) => contributor.name).slice(0, 5);
|
||||
}
|
||||
|
||||
handleShowAllContributors() {
|
||||
this.setState({
|
||||
showAllContributors: true
|
||||
});
|
||||
}
|
||||
|
||||
renderContributors() {
|
||||
if (!this.contributors) return null;
|
||||
|
||||
return (this.showAllContributors ? this.contributors : this.uniqueContributors)
|
||||
.map((contributor, index) => {
|
||||
return <MaintainerInfo key={index} title="Contributors" name={contributor.name} avatar={contributor.avatar}/>;
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
let author = this.author;
|
||||
|
||||
return (
|
||||
<Module
|
||||
title="Maintainers"
|
||||
className={classes.maintainersModule}
|
||||
>
|
||||
<ul>
|
||||
{author && <MaintainerInfo title="Author" name={author.name} avatar={author.avatar}/>}
|
||||
{this.renderContributors()}
|
||||
</ul>
|
||||
{!this.showAllContributors && (
|
||||
<button
|
||||
onClick={this.handleShowAllContributors}
|
||||
className={classes.showAllContributors}
|
||||
title="Current list only show the author and first 5 contributors unique by name"
|
||||
>
|
||||
Show all contributor
|
||||
</button>
|
||||
)}
|
||||
</Module>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
@import '../../../../styles/variable';
|
||||
|
||||
.maintainersModule {
|
||||
.showAllContributors {
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 14px;
|
||||
color: $primary-color;
|
||||
text-align: center;
|
||||
padding: 10px 0;
|
||||
}
|
||||
}
|
|
@ -1,90 +1 @@
|
|||
@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,4 +0,0 @@
|
|||
export default function getRegistryURL() {
|
||||
// Don't add slash if it's not a sub directory
|
||||
return `${location.origin}${location.pathname === '/' ? '' : location.pathname}`;
|
||||
}
|
12
src/webui/utils/url.js
Normal file
12
src/webui/utils/url.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
export function getRegistryURL() {
|
||||
// Don't add slash if it's not a sub directory
|
||||
return `${location.origin}${location.pathname === '/' ? '' : location.pathname}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get specified package detail page url
|
||||
* @param {string} packageName
|
||||
*/
|
||||
export function getDetailPageURL(packageName) {
|
||||
return `${getRegistryURL()}/#/detail/${packageName}`;
|
||||
}
|
Loading…
Add table
Reference in a new issue