mirror of
https://github.com/verdaccio/verdaccio.git
synced 2025-02-21 07:29:37 +01:00
Merge branch '4.x' into fix-1163
This commit is contained in:
commit
28231841f6
@ -1,7 +1,7 @@
|
||||
FROM node:10.14.1-alpine as builder
|
||||
FROM node:10.15.1-alpine as builder
|
||||
|
||||
ENV NODE_ENV=production \
|
||||
VERDACCIO_BUILD_REGISTRY=https://registry.verdaccio.org
|
||||
VERDACCIO_BUILD_REGISTRY=https://registry.npmjs.org
|
||||
|
||||
RUN apk --no-cache add openssl ca-certificates wget && \
|
||||
apk --no-cache add g++ gcc libgcc libstdc++ linux-headers make python && \
|
||||
@ -13,12 +13,12 @@ WORKDIR /opt/verdaccio-build
|
||||
COPY . .
|
||||
|
||||
RUN yarn config set registry $VERDACCIO_BUILD_REGISTRY && \
|
||||
yarn install --production=false && \
|
||||
yarn install --production=false --no-lockfile && \
|
||||
yarn lint && \
|
||||
yarn code:docker-build && \
|
||||
yarn build:webui && \
|
||||
yarn cache clean && \
|
||||
yarn install --production=true --pure-lockfile
|
||||
yarn install --production=true --no-lockfile
|
||||
|
||||
|
||||
|
||||
|
43
package.json
43
package.json
@ -18,45 +18,45 @@
|
||||
"@verdaccio/file-locking": "0.0.8",
|
||||
"@verdaccio/local-storage": "2.0.0-beta.1",
|
||||
"@verdaccio/streams": "2.0.0-beta.0",
|
||||
"JSONStream": "1.3.4",
|
||||
"JSONStream": "1.3.5",
|
||||
"async": "3.0.1-0",
|
||||
"body-parser": "1.18.3",
|
||||
"bunyan": "1.8.12",
|
||||
"chalk": "2.4.1",
|
||||
"commander": "2.18.0",
|
||||
"chalk": "2.4.2",
|
||||
"commander": "2.19.0",
|
||||
"compression": "1.7.3",
|
||||
"cookies": "0.7.2",
|
||||
"cors": "2.8.4",
|
||||
"date-fns": "1.29.0",
|
||||
"express": "4.16.3",
|
||||
"cookies": "0.7.3",
|
||||
"cors": "2.8.5",
|
||||
"date-fns": "1.30.1",
|
||||
"express": "4.16.4",
|
||||
"global": "4.3.2",
|
||||
"handlebars": "4.0.12",
|
||||
"http-errors": "1.7.1",
|
||||
"js-base64": "2.4.9",
|
||||
"js-base64": "2.5.1",
|
||||
"js-string-escape": "1.0.1",
|
||||
"js-yaml": "3.12.0",
|
||||
"jsonwebtoken": "8.3.0",
|
||||
"js-yaml": "3.12.1",
|
||||
"jsonwebtoken": "8.4.0",
|
||||
"lockfile": "1.0.4",
|
||||
"lodash": "4.17.11",
|
||||
"lunr-mutable-indexes": "2.3.1",
|
||||
"marked": "0.5.1",
|
||||
"mime": "2.3.1",
|
||||
"lunr-mutable-indexes": "2.3.2",
|
||||
"marked": "0.6.0",
|
||||
"mime": "2.4.0",
|
||||
"minimatch": "3.0.4",
|
||||
"mkdirp": "0.5.1",
|
||||
"mv": "2.1.1",
|
||||
"pkginfo": "0.4.1",
|
||||
"request": "2.88.0",
|
||||
"semver": "5.5.1",
|
||||
"semver": "5.6.0",
|
||||
"verdaccio-audit": "1.1.0",
|
||||
"verdaccio-htpasswd": "1.0.1",
|
||||
"verdaccio-htpasswd": "2.0.0-beta.0",
|
||||
"verror": "1.10.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "7.2.1",
|
||||
"@commitlint/config-conventional": "7.1.2",
|
||||
"@material-ui/core": "3.1.0",
|
||||
"@material-ui/icons": "3.0.1",
|
||||
"@verdaccio/babel-preset": "0.0.3",
|
||||
"@material-ui/core": "3.9.0",
|
||||
"@material-ui/icons": "3.0.2",
|
||||
"@verdaccio/babel-preset": "0.0.4",
|
||||
"@verdaccio/types": "4.1.4",
|
||||
"autosuggest-highlight": "3.1.1",
|
||||
"bundlesize": "0.17.0",
|
||||
@ -102,9 +102,9 @@
|
||||
"prettier": "1.14.3",
|
||||
"prop-types": "15.6.2",
|
||||
"puppeteer": "1.8.0",
|
||||
"react": "16.4.2",
|
||||
"react": "16.7.0",
|
||||
"react-autosuggest": "9.4.2",
|
||||
"react-dom": "16.4.2",
|
||||
"react-dom": "16.7.0",
|
||||
"react-emotion": "9.2.12",
|
||||
"react-hot-loader": "4.2.0",
|
||||
"react-router": "4.3.1",
|
||||
@ -132,7 +132,8 @@
|
||||
"webpack-cli": "3.1.1",
|
||||
"webpack-dev-server": "3.1.14",
|
||||
"webpack-merge": "4.1.4",
|
||||
"whatwg-fetch": "3.0.0"
|
||||
"whatwg-fetch": "3.0.0",
|
||||
"xss": "1.0.3"
|
||||
},
|
||||
"keywords": [
|
||||
"private",
|
||||
|
@ -12,6 +12,7 @@ import express from 'express';
|
||||
import { combineBaseUrl, getWebProtocol } from '../../lib/utils';
|
||||
import Search from '../../lib/search';
|
||||
import { HEADERS, HTTP_STATUS, WEB_TITLE } from '../../lib/constants';
|
||||
import { spliceURL } from '../../utils/string';
|
||||
|
||||
const { securityIframe } = require('../middleware');
|
||||
/* eslint new-cap:off */
|
||||
@ -52,9 +53,8 @@ module.exports = function(config, auth, storage) {
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/', function(req, res) {
|
||||
const installPath = _.get(config, 'url_prefix', '');
|
||||
const base = combineBaseUrl(getWebProtocol(req.get(HEADERS.FORWARDED_PROTO), req.protocol), req.get('host'), installPath);
|
||||
function renderHTML(req, res) {
|
||||
const base = combineBaseUrl(getWebProtocol(req.get(HEADERS.FORWARDED_PROTO), req.protocol), req.get('host'), config.url_prefix);
|
||||
const webPage = template
|
||||
.replace(/ToReplaceByVerdaccio/g, base)
|
||||
.replace(/ToReplaceByTitle/g, _.get(config, 'web.title') ? config.web.title : WEB_TITLE)
|
||||
@ -64,6 +64,20 @@ module.exports = function(config, auth, storage) {
|
||||
res.setHeader('Content-Type', 'text/html');
|
||||
|
||||
res.send(webPage);
|
||||
}
|
||||
|
||||
router.get('/-/web/:pkg', function(req, res) {
|
||||
renderHTML(req, res);
|
||||
});
|
||||
|
||||
router.get('/-/verdaccio/logo', function(req, res) {
|
||||
const installPath = _.get(config, 'url_prefix', '');
|
||||
|
||||
res.send(_.get(config, 'web.logo') || spliceURL(installPath, '/-/static/logo.png'));
|
||||
});
|
||||
|
||||
router.get('/', function(req, res) {
|
||||
renderHTML(req, res);
|
||||
});
|
||||
|
||||
return router;
|
||||
|
@ -4,21 +4,26 @@ import isNil from 'lodash/isNil';
|
||||
import storage from './utils/storage';
|
||||
import { makeLogin, isTokenExpire } from './utils/login';
|
||||
|
||||
import Footer from './components/Footer';
|
||||
import Loading from './components/Loading';
|
||||
import LoginModal from './components/Login';
|
||||
import Header from './components/Header';
|
||||
import { Container, Content } from './components/Layout';
|
||||
import Route from './router';
|
||||
import RouterApp from './router';
|
||||
import API from './utils/api';
|
||||
import './styles/typeface-roboto.scss';
|
||||
import './styles/main.scss';
|
||||
import 'normalize.css';
|
||||
import Footer from './components/Footer';
|
||||
|
||||
export const AppContext = React.createContext();
|
||||
|
||||
export const AppContextProvider = AppContext.Provider;
|
||||
export const AppContextConsumer = AppContext.Consumer;
|
||||
|
||||
export default class App extends Component {
|
||||
state = {
|
||||
error: {},
|
||||
logoUrl: '',
|
||||
logoUrl: window.VERDACCIO_LOGO,
|
||||
user: {},
|
||||
scope: (window.VERDACCIO_SCOPE) ? `${window.VERDACCIO_SCOPE}:` : '',
|
||||
showLoginModal: false,
|
||||
@ -28,26 +33,18 @@ export default class App extends Component {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.loadLogo();
|
||||
this.isUserAlreadyLoggedIn();
|
||||
this.loadPackages();
|
||||
this.loadOnHandler();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
componentDidUpdate(_, prevState) {
|
||||
const { isUserLoggedIn } = this.state;
|
||||
if (prevState.isUserLoggedIn !== isUserLoggedIn) {
|
||||
this.loadPackages();
|
||||
this.loadOnHandler();
|
||||
}
|
||||
}
|
||||
|
||||
loadLogo = () => {
|
||||
const logoUrl = window.VERDACCIO_LOGO;
|
||||
this.setState({
|
||||
logoUrl,
|
||||
});
|
||||
}
|
||||
|
||||
isUserAlreadyLoggedIn = () => {
|
||||
// checks for token validity
|
||||
const token = storage.getItem('token');
|
||||
@ -62,7 +59,7 @@ export default class App extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
loadPackages = async () => {
|
||||
loadOnHandler = async () => {
|
||||
try {
|
||||
this.req = await API.request('packages', 'GET');
|
||||
this.setState({
|
||||
@ -70,7 +67,8 @@ export default class App extends Component {
|
||||
isLoading: false,
|
||||
});
|
||||
} catch (error) {
|
||||
this.handleShowAlertDialog({
|
||||
// FIXME: add dialog
|
||||
console.error({
|
||||
title: 'Warning',
|
||||
message: `Unable to load package list: ${error.message}`,
|
||||
});
|
||||
@ -143,18 +141,16 @@ export default class App extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isLoading, isUserLoggedIn, packages } = this.state;
|
||||
const { isLoading, isUserLoggedIn, packages, logoUrl, user, scope } = this.state;
|
||||
return (
|
||||
<Container isLoading={isLoading}>
|
||||
{isLoading ? (
|
||||
<Loading />
|
||||
) : (
|
||||
<Fragment>
|
||||
{this.renderHeader()}
|
||||
<Content>
|
||||
<Route isUserLoggedIn={isUserLoggedIn} packages={packages} />
|
||||
</Content>
|
||||
<Footer />
|
||||
<AppContextProvider value={{isUserLoggedIn, packages, logoUrl, user, scope}}>
|
||||
{this.renderContent()}
|
||||
</AppContextProvider>
|
||||
</Fragment>
|
||||
)}
|
||||
{this.renderLoginModal()}
|
||||
@ -175,8 +171,24 @@ export default class App extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
renderContent = () => {
|
||||
return (
|
||||
<Fragment>
|
||||
<Content>
|
||||
<RouterApp
|
||||
onLogout={this.handleLogout}
|
||||
onToggleLoginModal={this.handleToggleLoginModal}>
|
||||
{this.renderHeader()}
|
||||
</RouterApp>
|
||||
</Content>
|
||||
<Footer />
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
renderHeader = () => {
|
||||
const { logoUrl, user, scope } = this.state;
|
||||
|
||||
return (
|
||||
<Header
|
||||
logo={logoUrl}
|
||||
|
109
src/webui/components/ActionBar/index.js
Normal file
109
src/webui/components/ActionBar/index.js
Normal file
@ -0,0 +1,109 @@
|
||||
/* eslint-disable react/jsx-max-depth */
|
||||
/**
|
||||
* @prettier
|
||||
*/
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { DetailContextConsumer } from '../../pages/version/index';
|
||||
import List from '@material-ui/core/List/index';
|
||||
|
||||
import DownloadIcon from '@material-ui/icons/CloudDownload';
|
||||
import BugReportIcon from '@material-ui/icons/BugReport';
|
||||
import HomeIcon from '@material-ui/icons/Home';
|
||||
import Tooltip from '@material-ui/core/Tooltip/index';
|
||||
|
||||
import { Fab, ActionListItem } from './styles';
|
||||
|
||||
const ACTIONS = {
|
||||
homepage: {
|
||||
icon: <HomeIcon />,
|
||||
title: 'Visit homepage',
|
||||
},
|
||||
issue: {
|
||||
icon: <BugReportIcon />,
|
||||
title: 'Open an issue',
|
||||
},
|
||||
tarball: {
|
||||
icon: <DownloadIcon />,
|
||||
title: 'Download tarball',
|
||||
},
|
||||
};
|
||||
|
||||
class ActionBar extends Component<any, any> {
|
||||
render() {
|
||||
return (
|
||||
<DetailContextConsumer>
|
||||
{context => {
|
||||
return this.renderActionBar(context);
|
||||
}}
|
||||
</DetailContextConsumer>
|
||||
);
|
||||
}
|
||||
|
||||
renderIconsWithLink(link, component) {
|
||||
if (!link) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<a href={link} target={"_blank"}>
|
||||
{component}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
renderActionBarListItems = (packageMeta) => {
|
||||
const {
|
||||
latest: {
|
||||
bugs: {
|
||||
url: issue,
|
||||
} = {},
|
||||
homepage,
|
||||
dist: {
|
||||
tarball,
|
||||
} = {},
|
||||
} = {},
|
||||
} = packageMeta;
|
||||
|
||||
const actionsMap = {
|
||||
homepage,
|
||||
issue,
|
||||
tarball,
|
||||
};
|
||||
|
||||
const renderList = Object.keys(actionsMap).reduce((component, value, key) => {
|
||||
const link = actionsMap[value];
|
||||
if (link) {
|
||||
const fab = (
|
||||
<Fab size={'small'}>
|
||||
{ACTIONS[value]['icon']}
|
||||
</Fab>
|
||||
);
|
||||
component.push(
|
||||
<Tooltip key={key} title={ACTIONS[value]['title']}>
|
||||
{this.renderIconsWithLink(link, fab)}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
return component;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ActionListItem alignItems={'flex-start'}>
|
||||
{renderList}
|
||||
</ActionListItem>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
renderActionBar = ({ packageMeta = {} }) => {
|
||||
return (
|
||||
<List>
|
||||
{this.renderActionBarListItems(packageMeta)}
|
||||
</List>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default ActionBar;
|
24
src/webui/components/ActionBar/styles.js
Normal file
24
src/webui/components/ActionBar/styles.js
Normal file
@ -0,0 +1,24 @@
|
||||
/**
|
||||
* @prettier
|
||||
*/
|
||||
import styled from 'react-emotion';
|
||||
import { default as MuiFab } from '@material-ui/core/Fab';
|
||||
import ListItem from '@material-ui/core/ListItem/index';
|
||||
|
||||
import colors from '../../utils/styles/colors';
|
||||
|
||||
export const ActionListItem = styled(ListItem)`
|
||||
&& {
|
||||
padding-top: 0;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Fab = styled(MuiFab)`
|
||||
&& {
|
||||
background-color: ${colors.primary};
|
||||
color: ${colors.white};
|
||||
margin-right: 10px;
|
||||
}
|
||||
`;
|
54
src/webui/components/Author/index.js
Normal file
54
src/webui/components/Author/index.js
Normal file
@ -0,0 +1,54 @@
|
||||
/**
|
||||
* @prettier
|
||||
*/
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { DetailContextConsumer } from '../../pages/version/index';
|
||||
import Avatar from '@material-ui/core/Avatar/index';
|
||||
import List from '@material-ui/core/List/index';
|
||||
import ListItemText from '@material-ui/core/ListItemText/index';
|
||||
|
||||
import { Heading, AuthorListItem } from './styles';
|
||||
class Authors extends Component<any, any> {
|
||||
render() {
|
||||
return (
|
||||
<DetailContextConsumer>
|
||||
{context => {
|
||||
return this.renderAuthor(context);
|
||||
}}
|
||||
</DetailContextConsumer>
|
||||
);
|
||||
}
|
||||
|
||||
renderLinkForMail(email, avatarComponent) {
|
||||
if (!email) {
|
||||
return avatarComponent;
|
||||
}
|
||||
return (
|
||||
<a href={`mailto:${email}`} target={'_top'}>
|
||||
{avatarComponent}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
renderAuthor = ({ packageMeta }) => {
|
||||
const { author } = packageMeta.latest;
|
||||
|
||||
if (!author) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const avatarComponent = <Avatar alt={author.name} src={author.avatar} />;
|
||||
return (
|
||||
<List subheader={<Heading variant={'subheading'}>{'Author'}</Heading>}>
|
||||
<AuthorListItem>
|
||||
{this.renderLinkForMail(author.email, avatarComponent)}
|
||||
<ListItemText primary={author.name} />
|
||||
</AuthorListItem>
|
||||
</List>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default Authors;
|
21
src/webui/components/Author/styles.js
Normal file
21
src/webui/components/Author/styles.js
Normal file
@ -0,0 +1,21 @@
|
||||
/**
|
||||
* @prettier
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import styled from 'react-emotion';
|
||||
import ListItem from '@material-ui/core/ListItem/index';
|
||||
import Typography from '@material-ui/core/Typography/index';
|
||||
|
||||
export const Heading = styled(Typography)`
|
||||
&& {
|
||||
font-weight: 700;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
`;
|
||||
export const AuthorListItem = styled(ListItem)`
|
||||
&& {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
`;
|
@ -14,7 +14,7 @@ import { ClipBoardCopy, ClipBoardCopyText, CopyIcon } from './styles';
|
||||
import { copyToClipBoardUtility } from '../../utils/cli-utils';
|
||||
import { TEXT } from '../../utils/constants';
|
||||
|
||||
const CopyToClipBoard = ({ text }: IProps): Node => {
|
||||
const CopyToClipBoard = ({ text, children }: IProps): Node => {
|
||||
const renderToolTipFileCopy = () => (
|
||||
<Tooltip disableFocusListener={true} title={TEXT.CLIPBOARD_COPY}>
|
||||
<CopyIcon onClick={copyToClipBoardUtility(text)}>
|
||||
@ -22,9 +22,17 @@ const CopyToClipBoard = ({ text }: IProps): Node => {
|
||||
</CopyIcon>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
const renderText = children => {
|
||||
if (children) {
|
||||
return <ClipBoardCopyText>{children}</ClipBoardCopyText>;
|
||||
}
|
||||
|
||||
return <ClipBoardCopyText>{text}</ClipBoardCopyText>;
|
||||
};
|
||||
return (
|
||||
<ClipBoardCopy>
|
||||
<ClipBoardCopyText>{text}</ClipBoardCopyText>
|
||||
{renderText(children)}
|
||||
{renderToolTipFileCopy()}
|
||||
</ClipBoardCopy>
|
||||
);
|
||||
|
@ -5,4 +5,5 @@
|
||||
|
||||
export interface IProps {
|
||||
text: string;
|
||||
children?: any;
|
||||
}
|
||||
|
114
src/webui/components/Dependencies/index.js
Normal file
114
src/webui/components/Dependencies/index.js
Normal file
@ -0,0 +1,114 @@
|
||||
/**
|
||||
* @prettier
|
||||
* @flow
|
||||
*/
|
||||
|
||||
/* eslint react/jsx-max-depth: 0 */
|
||||
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import CardContent from '@material-ui/core/CardContent/index';
|
||||
|
||||
import { DetailContextConsumer } from '../../pages/version';
|
||||
|
||||
import { Content, CardWrap, Heading, Tags, Tag } from './styles';
|
||||
import NoItems from '../NoItems';
|
||||
|
||||
class DepDetail extends Component<any, any> {
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
const { name, version } = this.props;
|
||||
|
||||
this.state = {
|
||||
name,
|
||||
version,
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const { name, version } = this.state;
|
||||
const tagText = `${name}@${version}`;
|
||||
return <Tag clickable={true} component={'div'} label={tagText} onClick={this.handleOnClick} />;
|
||||
}
|
||||
|
||||
handleOnClick = () => {
|
||||
const { name } = this.state;
|
||||
const { onLoading, history } = this.props;
|
||||
|
||||
onLoading();
|
||||
history.push(`/-/web/detail/${name}`);
|
||||
};
|
||||
}
|
||||
|
||||
const WrappDepDetail = withRouter(DepDetail);
|
||||
|
||||
class DependencyBlock extends Component<any, any> {
|
||||
render() {
|
||||
const { dependencies, title } = this.props;
|
||||
const deps = Object.entries(dependencies);
|
||||
|
||||
return (
|
||||
// $FlowFixMe
|
||||
<DetailContextConsumer>
|
||||
{({ enableLoading }) => {
|
||||
return (
|
||||
<CardWrap>
|
||||
<CardContent>
|
||||
<Heading variant={'subheading'}>{`${title} (${deps.length})`}</Heading>
|
||||
<Tags>{this.renderTags(deps, enableLoading)}</Tags>
|
||||
</CardContent>
|
||||
</CardWrap>
|
||||
);
|
||||
}}
|
||||
</DetailContextConsumer>
|
||||
);
|
||||
}
|
||||
|
||||
renderTags = (deps: any, enableLoading: any) =>
|
||||
deps.map(dep => {
|
||||
const [name, version] = dep;
|
||||
|
||||
return <WrappDepDetail key={name} name={name} onLoading={enableLoading} version={version} />;
|
||||
});
|
||||
}
|
||||
|
||||
class Dependencies extends Component<any, any> {
|
||||
state = {
|
||||
tabPosition: 0,
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<DetailContextConsumer>
|
||||
{packageMeta => {
|
||||
return this.renderDependencies(packageMeta);
|
||||
}}
|
||||
</DetailContextConsumer>
|
||||
);
|
||||
}
|
||||
|
||||
// $FlowFixMe
|
||||
renderDependencies({ packageMeta }) {
|
||||
const { latest } = packageMeta;
|
||||
const { dependencies, devDependencies, peerDependencies, name } = latest;
|
||||
|
||||
if (dependencies || devDependencies || peerDependencies) {
|
||||
return (
|
||||
<Content>
|
||||
<Fragment>
|
||||
{dependencies && <DependencyBlock dependencies={dependencies} title={'Dependencies'} />}
|
||||
{devDependencies && <DependencyBlock dependencies={devDependencies} title={'DevDependencies'} />}
|
||||
{peerDependencies && <DependencyBlock dependencies={peerDependencies} title={'PeerDependencies'} />}
|
||||
</Fragment>
|
||||
</Content>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Content>
|
||||
<NoItems text={`${name} has no dependencies.`} />
|
||||
</Content>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Dependencies;
|
42
src/webui/components/Dependencies/styles.js
Normal file
42
src/webui/components/Dependencies/styles.js
Normal file
@ -0,0 +1,42 @@
|
||||
/**
|
||||
* @prettier
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import styled from 'react-emotion';
|
||||
import Card from '@material-ui/core/Card/index';
|
||||
import Typography from '@material-ui/core/Typography/index';
|
||||
import Chip from '@material-ui/core/Chip/index';
|
||||
|
||||
export const Content = styled.div`
|
||||
&& {
|
||||
padding: 20px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const CardWrap = styled(Card)`
|
||||
&& {
|
||||
margin: 0 0 25px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Heading = styled(Typography)`
|
||||
&& {
|
||||
font-weight: 700;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Tags = styled('div')`
|
||||
&& {
|
||||
display: flex;
|
||||
justify-content: start;
|
||||
flex-wrap: wrap;
|
||||
margin: 0 -5px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Tag = styled(Chip)`
|
||||
&& {
|
||||
margin: 5px;
|
||||
}
|
||||
`;
|
12
src/webui/components/Dependencies/types.js
Normal file
12
src/webui/components/Dependencies/types.js
Normal file
@ -0,0 +1,12 @@
|
||||
/**
|
||||
* @prettier
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import type { Node } from 'react';
|
||||
|
||||
export interface IProps {
|
||||
children: Node;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
67
src/webui/components/DetailContainer/index.js
Normal file
67
src/webui/components/DetailContainer/index.js
Normal file
@ -0,0 +1,67 @@
|
||||
/**
|
||||
* @prettier
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { DetailContextConsumer } from '../../pages/version/index';
|
||||
import Readme from '../Readme';
|
||||
import Versions from '../Versions';
|
||||
import { preventXSS } from '../../utils/sec-utils';
|
||||
import Tabs from '@material-ui/core/Tabs/index';
|
||||
import Tab from '@material-ui/core/Tab/index';
|
||||
import { Content } from './styles';
|
||||
import Dependencies from '../Dependencies';
|
||||
import UpLinks from '../UpLinks';
|
||||
|
||||
class DetailContainer extends Component<any, any> {
|
||||
state = {
|
||||
tabPosition: 0,
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<DetailContextConsumer>
|
||||
{context => {
|
||||
return this.renderTabs(context);
|
||||
}}
|
||||
</DetailContextConsumer>
|
||||
);
|
||||
}
|
||||
|
||||
// $FlowFixMe
|
||||
renderTabs = ({ readMe }) => {
|
||||
const { tabPosition } = this.state;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Tabs indicatorColor={'primary'} onChange={this.handleChange} textColor={'primary'} value={tabPosition} variant={'fullWidth'}>
|
||||
<Tab label={'Readme'} />
|
||||
<Tab label={'Dependencies'} />
|
||||
<Tab label={'Versions'} />
|
||||
<Tab label={'Uplinks'} />
|
||||
</Tabs>
|
||||
<Content>
|
||||
{tabPosition === 0 && this.renderReadme(readMe)}
|
||||
{tabPosition === 1 && <Dependencies />}
|
||||
{tabPosition === 2 && <Versions />}
|
||||
{tabPosition === 3 && <UpLinks />}
|
||||
</Content>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
renderReadme = (readMe: string) => {
|
||||
const encodedReadme = preventXSS(readMe);
|
||||
|
||||
return <Readme description={encodedReadme} />;
|
||||
};
|
||||
|
||||
handleChange = (event: any, tabPosition: number) => {
|
||||
event.preventDefault();
|
||||
this.setState({ tabPosition });
|
||||
};
|
||||
}
|
||||
|
||||
export default DetailContainer;
|
@ -5,8 +5,8 @@
|
||||
|
||||
import styled from 'react-emotion';
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
export const Content = styled.div`
|
||||
&& {
|
||||
margin: 5em 0;
|
||||
padding: 15px;
|
||||
}
|
||||
`;
|
12
src/webui/components/DetailContainer/types.js
Normal file
12
src/webui/components/DetailContainer/types.js
Normal file
@ -0,0 +1,12 @@
|
||||
/**
|
||||
* @prettier
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import type { Node } from 'react';
|
||||
|
||||
export interface IProps {
|
||||
children: Node;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
96
src/webui/components/DetailSidebar/index.js
Normal file
96
src/webui/components/DetailSidebar/index.js
Normal file
@ -0,0 +1,96 @@
|
||||
import React, {Component} from 'react';
|
||||
|
||||
import Card from '@material-ui/core/Card/index';
|
||||
import CardContent from '@material-ui/core/CardContent/index';
|
||||
import List from '@material-ui/core/List/index';
|
||||
import ListItemText from '@material-ui/core/ListItemText/index';
|
||||
|
||||
import ActtionBar from '../ActionBar';
|
||||
import Author from '../Author';
|
||||
import Developers from '../Developers';
|
||||
import Dist from '../Dist';
|
||||
import Engine from '../Engines';
|
||||
import Install from '../Install';
|
||||
import Repository from '../Repository';
|
||||
|
||||
|
||||
import { DetailContextConsumer } from '../../pages/version/index';
|
||||
|
||||
import { TitleListItem } from './styles';
|
||||
|
||||
class DetailSidebar extends Component {
|
||||
render() {
|
||||
return (
|
||||
<DetailContextConsumer>
|
||||
{(context) => this.renderSideBar(context)}
|
||||
</DetailContextConsumer>
|
||||
);
|
||||
};
|
||||
|
||||
renderSideBar = ({packageName, packageMeta}) => {
|
||||
return (
|
||||
<div className={'sidebar-info'}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
{this.renderTitle(packageName, packageMeta)}
|
||||
{this.renderActionBar()}
|
||||
{this.renderCopyCLI()}
|
||||
{this.renderRepository()}
|
||||
{this.renderEngine()}
|
||||
{this.renderDist()}
|
||||
{this.renderAuthor()}
|
||||
{this.renderMaintainers()}
|
||||
{this.renderContributors()}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderTitle = (packageName, packageMeta) => {
|
||||
return (
|
||||
<List className={'detail-info'}>
|
||||
<TitleListItem alignItems={"flex-start"}>
|
||||
<ListItemText
|
||||
primary={<b>{packageName}</b>}
|
||||
secondary={packageMeta.latest.description}
|
||||
/>
|
||||
</TitleListItem>
|
||||
</List>
|
||||
);
|
||||
}
|
||||
|
||||
renderCopyCLI = () => {
|
||||
return <Install />;
|
||||
}
|
||||
|
||||
renderMaintainers = () => {
|
||||
return <Developers type={'maintainers'} />;
|
||||
}
|
||||
|
||||
renderContributors = () => {
|
||||
return <Developers type={'contributors'} />;
|
||||
}
|
||||
|
||||
renderRepository = () => {
|
||||
return <Repository />;
|
||||
}
|
||||
|
||||
renderAuthor = () => {
|
||||
return <Author />;
|
||||
}
|
||||
|
||||
renderEngine = () => {
|
||||
return <Engine />;
|
||||
}
|
||||
|
||||
renderDist = () => {
|
||||
return <Dist />;
|
||||
}
|
||||
|
||||
renderActionBar = () => {
|
||||
return <ActtionBar />;
|
||||
}
|
||||
}
|
||||
|
||||
export default DetailSidebar;
|
26
src/webui/components/DetailSidebar/styles.js
Normal file
26
src/webui/components/DetailSidebar/styles.js
Normal file
@ -0,0 +1,26 @@
|
||||
/**
|
||||
* @prettier
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import styled from 'react-emotion';
|
||||
import Avatar from '@material-ui/core/Avatar/index';
|
||||
import ListItem from '@material-ui/core/ListItem/index';
|
||||
|
||||
import colors from '../../utils/styles/colors';
|
||||
|
||||
export const TitleListItem = styled(ListItem)`
|
||||
&& {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
export const TitleAvatar = styled(Avatar)`
|
||||
&& {
|
||||
color: ${colors.greySuperLight};
|
||||
background-color: ${colors.primary};
|
||||
text-transform: capitalize;
|
||||
}
|
||||
`;
|
12
src/webui/components/DetailSidebar/types.js
Normal file
12
src/webui/components/DetailSidebar/types.js
Normal file
@ -0,0 +1,12 @@
|
||||
/**
|
||||
* @prettier
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import type { Node } from 'react';
|
||||
|
||||
export interface IProps {
|
||||
children: Node;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
78
src/webui/components/Developers/index.js
Normal file
78
src/webui/components/Developers/index.js
Normal file
@ -0,0 +1,78 @@
|
||||
import React, {Component} from 'react';
|
||||
|
||||
import Avatar from '@material-ui/core/Avatar';
|
||||
import Tooltip from '@material-ui/core/Tooltip';
|
||||
import Add from '@material-ui/icons/Add';
|
||||
|
||||
import { DetailContextConsumer } from '../../pages/version';
|
||||
|
||||
import { Details, Heading, Content, Fab } from './styles';
|
||||
|
||||
interface Props {
|
||||
type: 'contributors' | 'maintainers'
|
||||
}
|
||||
|
||||
class Developers extends Component<Props, any> {
|
||||
state = {
|
||||
visibleDevs: 6,
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<DetailContextConsumer>
|
||||
{({ packageMeta }) => {
|
||||
const { type } = this.props;
|
||||
const developerType = packageMeta.latest[type];
|
||||
if (!developerType || developerType.length === 0) return null;
|
||||
return this.renderDevelopers(developerType);
|
||||
}}
|
||||
</DetailContextConsumer>
|
||||
);
|
||||
};
|
||||
|
||||
handleLoadMore = () => {
|
||||
this.setState((prev) => ({ visibleDevs: prev.visibleDevs + 6 }));
|
||||
}
|
||||
|
||||
renderDevelopers = (developers) => {
|
||||
const { type } = this.props;
|
||||
const { visibleDevs } = this.state;
|
||||
return (
|
||||
<>
|
||||
<Heading variant={'subheading'}>{type}</Heading>
|
||||
<Content>
|
||||
{developers.slice(0, visibleDevs).map(developer => (
|
||||
<Details key={developer.email}>{this.renderDeveloperDetails(developer)}</Details>
|
||||
))}
|
||||
{visibleDevs < developers.length &&
|
||||
<Fab onClick={this.handleLoadMore} size={'small'}><Add /></Fab>
|
||||
}
|
||||
</Content>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
renderLinkForMail(email, avatar) {
|
||||
if(!email) {
|
||||
return avatar;
|
||||
}
|
||||
return (
|
||||
<a href={`mailto:${email}`} target={"_top"}>
|
||||
{avatar}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
renderDeveloperDetails = ({ name, avatar, email }) => {
|
||||
const avatarComponent = <Avatar aria-label={name} src={avatar} />;
|
||||
return (
|
||||
<Tooltip title={name}>
|
||||
{this.renderLinkForMail(email, avatarComponent)}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
export default Developers;
|
38
src/webui/components/Developers/styles.js
Normal file
38
src/webui/components/Developers/styles.js
Normal file
@ -0,0 +1,38 @@
|
||||
/**
|
||||
* @prettier
|
||||
*/
|
||||
|
||||
import styled from 'react-emotion';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import { default as MuiFab } from '@material-ui/core/Fab';
|
||||
import colors from '../../utils/styles/colors';
|
||||
|
||||
export const Details = styled('span')`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
export const Content = styled('div')`
|
||||
margin: 10px 0 10px 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
> * {
|
||||
margin: 5px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Heading = styled(Typography)`
|
||||
&& {
|
||||
font-weight: 700;
|
||||
margin-bottom: 10px;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Fab = styled(MuiFab)`
|
||||
&& {
|
||||
background-color: ${colors.primary};
|
||||
color: ${colors.white};
|
||||
}
|
||||
`;
|
59
src/webui/components/Dist/index.js
Normal file
59
src/webui/components/Dist/index.js
Normal file
@ -0,0 +1,59 @@
|
||||
/**
|
||||
* @prettier
|
||||
*/
|
||||
|
||||
/* eslint-disable */
|
||||
import React, { Component } from 'react';
|
||||
import List from '@material-ui/core/List/index';
|
||||
|
||||
import { DetailContextConsumer } from '../../pages/version/index';
|
||||
|
||||
import { Heading, DistListItem, DistChips, DownloadButton } from './styles';
|
||||
import fileSizeSI from '../../utils/file-size';
|
||||
|
||||
class Dist extends Component<any, any> {
|
||||
render() {
|
||||
return (
|
||||
<DetailContextConsumer>
|
||||
{context => {
|
||||
return this.renderDist(context);
|
||||
}}
|
||||
</DetailContextConsumer>
|
||||
);
|
||||
}
|
||||
|
||||
renderChips(dist, license) {
|
||||
const distDict = {
|
||||
'file-count': dist.fileCount,
|
||||
size: dist.unpackedSize && fileSizeSI(dist.unpackedSize),
|
||||
license,
|
||||
};
|
||||
|
||||
const chipsList = Object.keys(distDict).reduce((componentList, title, key) => {
|
||||
const value = distDict[title];
|
||||
if (value) {
|
||||
const label = (
|
||||
<span>
|
||||
<b>{title.split('-').join(' ')}</b>: {value}
|
||||
</span>
|
||||
);
|
||||
componentList.push(<DistChips label={label} key={key} />);
|
||||
}
|
||||
return componentList;
|
||||
}, []);
|
||||
|
||||
return chipsList;
|
||||
}
|
||||
|
||||
renderDist = ({ packageMeta }) => {
|
||||
const { dist = {}, license } = packageMeta.latest;
|
||||
|
||||
return (
|
||||
<List subheader={<Heading variant={'subheading'}>{'Latest Distribution'}</Heading>}>
|
||||
<DistListItem>{this.renderChips(dist, license)}</DistListItem>
|
||||
</List>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default Dist;
|
39
src/webui/components/Dist/styles.js
Normal file
39
src/webui/components/Dist/styles.js
Normal file
@ -0,0 +1,39 @@
|
||||
/**
|
||||
* @prettier
|
||||
*/
|
||||
|
||||
import styled from 'react-emotion';
|
||||
import { default as MuiFab } from '@material-ui/core/Fab/index';
|
||||
import Chip from '@material-ui/core/Chip/index';
|
||||
import ListItem from '@material-ui/core/ListItem/index';
|
||||
import Typography from '@material-ui/core/Typography/index';
|
||||
|
||||
import colors from '../../utils/styles/colors';
|
||||
|
||||
export const Heading = styled(Typography)`
|
||||
&& {
|
||||
font-weight: 700;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
`;
|
||||
|
||||
export const DistListItem = styled(ListItem)`
|
||||
&& {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
export const DistChips = styled(Chip)`
|
||||
&& {
|
||||
margin-right: 5px;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
`;
|
||||
|
||||
export const DownloadButton = styled(MuiFab)`
|
||||
&& {
|
||||
background-color: ${colors.primary};
|
||||
color: ${colors.white};
|
||||
}
|
||||
`;
|
BIN
src/webui/components/Engines/img/node.png
Normal file
BIN
src/webui/components/Engines/img/node.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 27 KiB |
78
src/webui/components/Engines/index.js
Normal file
78
src/webui/components/Engines/index.js
Normal file
@ -0,0 +1,78 @@
|
||||
/* eslint-disable */
|
||||
import React, {Component} from 'react';
|
||||
|
||||
import Avatar from '@material-ui/core/Avatar/index';
|
||||
import Grid from '@material-ui/core/Grid/index';
|
||||
import List from '@material-ui/core/List/index';
|
||||
import ListItemText from '@material-ui/core/ListItemText/index';
|
||||
|
||||
import { DetailContextConsumer } from '../../pages/version/index';
|
||||
|
||||
import { Heading, EngineListItem } from './styles';
|
||||
import node from './img/node.png';
|
||||
import npm from '../Install/img/npm.svg'
|
||||
|
||||
|
||||
const ICONS = {
|
||||
'node-JS': <Avatar src={node} />,
|
||||
'NPM-version': <Avatar src={npm} />,
|
||||
}
|
||||
|
||||
class Engine extends Component {
|
||||
render() {
|
||||
return (
|
||||
<DetailContextConsumer>
|
||||
{(context) => {
|
||||
return this.renderEngine(context);
|
||||
}}
|
||||
</DetailContextConsumer>
|
||||
);
|
||||
};
|
||||
|
||||
renderEngine = ({packageMeta}) => {
|
||||
const { engines } = packageMeta.latest;
|
||||
if (!engines) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const engineDict = {
|
||||
'node-JS': engines.node,
|
||||
'NPM-version': engines.npm
|
||||
}
|
||||
|
||||
const items = Object.keys(engineDict).reduce((markup, text, key) => {
|
||||
const heading = engineDict[text]
|
||||
if (heading){
|
||||
markup.push(
|
||||
<Grid item={true} xs={6} key={key}>
|
||||
{this.renderListItems(heading, text)}
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
return markup;
|
||||
}, []);
|
||||
|
||||
if (items.length < 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Grid container={true}>
|
||||
{items}
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
renderListItems = (heading, text) => {
|
||||
return (
|
||||
<List subheader={<Heading variant={"subheading"}>{text.split('-').join(' ')}</Heading>}>
|
||||
<EngineListItem>
|
||||
{ ICONS[text] }
|
||||
<ListItemText primary={heading} />
|
||||
</EngineListItem>
|
||||
</List>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Engine;
|
21
src/webui/components/Engines/styles.js
Normal file
21
src/webui/components/Engines/styles.js
Normal file
@ -0,0 +1,21 @@
|
||||
/**
|
||||
* @prettier
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import styled from 'react-emotion';
|
||||
import ListItem from '@material-ui/core/ListItem/index';
|
||||
import Typography from '@material-ui/core/Typography/index';
|
||||
|
||||
export const Heading = styled(Typography)`
|
||||
&& {
|
||||
font-weight: 700;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
`;
|
||||
|
||||
export const EngineListItem = styled(ListItem)`
|
||||
&& {
|
||||
padding-left: 0;
|
||||
}
|
||||
`;
|
@ -5,6 +5,7 @@
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import type { Node } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import Button from '@material-ui/core/Button/index';
|
||||
import IconButton from '@material-ui/core/IconButton/index';
|
||||
@ -17,7 +18,7 @@ import AccountCircle from '@material-ui/icons/AccountCircle';
|
||||
import { default as IconSearch } from '@material-ui/icons/Search';
|
||||
|
||||
import { getRegistryURL } from '../../utils/url';
|
||||
import Link from '../Link';
|
||||
import ExternalLink from '../Link';
|
||||
import Logo from '../Logo';
|
||||
import RegistryInfoDialog from '../RegistryInfoDialog';
|
||||
import Label from '../Label';
|
||||
@ -138,7 +139,7 @@ class Header extends Component<IProps, IState> {
|
||||
switch (type) {
|
||||
case 'help':
|
||||
content = (
|
||||
<IconButton blank={true} color={'inherit'} component={Link} to={'https://verdaccio.org/docs/en/installation'}>
|
||||
<IconButton blank={true} color={'inherit'} component={ExternalLink} to={'https://verdaccio.org/docs/en/installation'}>
|
||||
<Help />
|
||||
</IconButton>
|
||||
);
|
||||
|
15
src/webui/components/Install/img/npm.svg
Normal file
15
src/webui/components/Install/img/npm.svg
Normal file
@ -0,0 +1,15 @@
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 256 256" enable-background="new 0 0 256 256" xml:space="preserve">
|
||||
<polygon fill="#CC0000" points="23,65.6 130,3.3 237,65.6 237,190.1 130,252.4 23,190.1 "/>
|
||||
<polygon fill="#FFFFFF" points="133,127.8 232.5,70.5 236.5,186 133,248.5 "/>
|
||||
<g>
|
||||
<path fill="#CC0000" d="M234,67l-0.3,122.4l-103.8,60.2l-0.5-120.3L234,67z M146.7,139.3l0.3,80.4l34.5-20.1l-0.1-60.6l17.4-10.3
|
||||
l0,60.8l17.4-10.2l0.1-81.4L146.7,139.3z"/>
|
||||
</g>
|
||||
<path fill="#910505" d="M136.8,4.2c-4.8-2.7-12.5-2.7-17.3,0L24.7,58.7c-4.8,2.7-8.6,9.5-8.6,14.9v109c0,5.5,3.9,12.2,8.6,14.9
|
||||
l94.8,54.5c4.8,2.7,12.5,2.7,17.3,0l94.8-54.5c4.8-2.7,8.6-9.5,8.6-14.9v-109c0-5.5-3.9-12.2-8.6-14.9L136.8,4.2z M220.9,61.2
|
||||
c4.8,2.7,4.8,7.2,0,9.9l-83,47.7c-4.8,2.7-12.5,2.7-17.3,0L36.4,70.4c-4.8-2.7-4.8-7.2,0-9.9l83-47.7c4.8-2.7,12.5-2.7,17.3,0
|
||||
L220.9,61.2z M23.5,81.5c0-5.5,3.9-7.7,8.6-5l84.9,48.8c4.8,2.7,8.6,9.5,8.6,14.9V237c0,5.5-3.9,7.7-8.6,5l-84.9-48.8
|
||||
c-4.8-2.7-8.6-9.5-8.6-14.9V81.5z M141.8,240.5c-4.8,2.7-8.6,0.5-8.6-5v-95.3c0-5.5,3.9-12.2,8.6-14.9L224.2,78
|
||||
c4.8-2.7,8.6-0.5,8.6,5v95.3c0,5.5-3.9,12.2-8.6,14.9L141.8,240.5z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
1
src/webui/components/Install/img/pnpm.svg
Normal file
1
src/webui/components/Install/img/pnpm.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid meet" viewBox="66.09157809474142 33.5 184.5 184.49999999999997" width="180" height="180"><defs><path d="M67.59 35L247.59 35L247.59 215L67.59 215L67.59 35Z" id="b2JZZcA3fT"></path><path d="M237.6 95L187.6 95L187.6 45L237.6 45L237.6 95Z" id="bj0tb0Y8q"></path><path d="M182.59 95L132.59 95L132.59 45L182.59 45L182.59 95Z" id="dkDSTzPj3"></path><path d="M127.59 95L77.59 95L77.59 45L127.59 45L127.59 95Z" id="a4vNdcNLpF"></path><path d="M237.6 150L187.6 150L187.6 100L237.6 100L237.6 150Z" id="h2t4Zj1jSU"></path><path d="M182.59 150L132.59 150L132.59 100L182.59 100L182.59 150Z" id="b4t5pngwvT"></path><path d="M182.59 205L132.59 205L132.59 155L182.59 155L182.59 205Z" id="b9s1gd5m2"></path><path d="M237.6 205L187.6 205L187.6 155L237.6 155L237.6 205Z" id="cmt9WLvz7"></path><path d="M127.59 205L77.59 205L77.59 155L127.59 155L127.59 205Z" id="bJUNqgFSg"></path></defs><g><g><use xlink:href="#b2JZZcA3fT" opacity="1" fill="#ffffff" fill-opacity="1"></use></g><g><use xlink:href="#bj0tb0Y8q" opacity="1" fill="#f9ad00" fill-opacity="1"></use></g><g><use xlink:href="#dkDSTzPj3" opacity="1" fill="#f9ad00" fill-opacity="1"></use></g><g><use xlink:href="#a4vNdcNLpF" opacity="1" fill="#f9ad00" fill-opacity="1"></use></g><g><use xlink:href="#h2t4Zj1jSU" opacity="1" fill="#f9ad00" fill-opacity="1"></use></g><g><use xlink:href="#b4t5pngwvT" opacity="1" fill="#4e4e4e" fill-opacity="1"></use></g><g><use xlink:href="#b9s1gd5m2" opacity="1" fill="#4e4e4e" fill-opacity="1"></use></g><g><use xlink:href="#cmt9WLvz7" opacity="1" fill="#4e4e4e" fill-opacity="1"></use></g><g><use xlink:href="#bJUNqgFSg" opacity="1" fill="#4e4e4e" fill-opacity="1"></use></g></g></svg>
|
After Width: | Height: | Size: 1.7 KiB |
1
src/webui/components/Install/img/yarn.svg
Normal file
1
src/webui/components/Install/img/yarn.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 518 518"><style>.st0{fill:#2c8ebb}.st1{fill:#fff}</style><path class="st0" d="M259 0c143 0 259 116 259 259S402 518 259 518 0 402 0 259 116 0 259 0z"/><path class="st1" d="M435.2 337.5c-1.8-14.2-13.8-24-29.2-23.8-23 .3-42.3 12.2-55.1 20.1-5 3.1-9.3 5.4-13 7.1.8-11.6.1-26.8-5.9-43.5-7.3-20-17.1-32.3-24.1-39.4 8.1-11.8 19.2-29 24.4-55.6 4.5-22.7 3.1-58-7.2-77.8-2.1-4-5.6-6.9-10-8.1-1.8-.5-5.2-1.5-11.9.4C293.1 96 289.6 93.8 286.9 92c-5.6-3.6-12.2-4.4-18.4-2.1-8.3 3-15.4 11-22.1 25.2-1 2.1-1.9 4.1-2.7 6.1-12.7.9-32.7 5.5-49.6 23.8-2.1 2.3-6.2 4-10.5 5.6h.1c-8.8 3.1-12.8 10.3-17.7 23.3-6.8 18.2.2 36.1 7.1 47.7-9.4 8.4-21.9 21.8-28.5 37.5-8.2 19.4-9.1 38.4-8.8 48.7-7 7.4-17.8 21.3-19 36.9-1.6 21.8 6.3 36.6 9.8 42 1 1.6 2.1 2.9 3.3 4.2-.4 2.7-.5 5.6.1 8.6 1.3 7 5.7 12.7 12.4 16.3 13.2 7 31.6 10 45.8 2.9 5.1 5.4 14.4 10.6 31.3 10.6h1c4.3 0 58.9-2.9 74.8-6.8 7.1-1.7 12-4.7 15.2-7.4 10.2-3.2 38.4-12.8 65-30 18.8-12.2 25.3-14.8 39.3-18.2 13.6-3.3 22.1-15.7 20.4-29.4zm-23.8 14.7c-16 3.8-24.1 7.3-43.9 20.2-30.9 20-64.7 29.3-64.7 29.3s-2.8 4.2-10.9 6.1c-14 3.4-66.7 6.3-71.5 6.4-12.9.1-20.8-3.3-23-8.6-6.7-16 9.6-23 9.6-23s-3.6-2.2-5.7-4.2c-1.9-1.9-3.9-5.7-4.5-4.3-2.5 6.1-3.8 21-10.5 27.7-9.2 9.3-26.6 6.2-36.9.8-11.3-6 .8-20.1.8-20.1s-6.1 3.6-11-3.8c-4.4-6.8-8.5-18.4-7.4-32.7 1.2-16.3 19.4-32.1 19.4-32.1s-3.2-24.1 7.3-48.8c9.5-22.5 35.1-40.6 35.1-40.6s-21.5-23.8-13.5-45.2c5.2-14 7.3-13.9 9-14.5 6-2.3 11.8-4.8 16.1-9.5 21.5-23.2 48.9-18.8 48.9-18.8s13-39.5 25-31.8c3.7 2.4 17 32 17 32s14.2-8.3 15.8-5.2c8.6 16.7 9.6 48.6 5.8 68-6.4 32-22.4 49.2-28.8 60-1.5 2.5 17.2 10.4 29 43.1 10.9 29.9 1.2 55 2.9 57.8.3.5.4.7.4.7s12.5 1 37.6-14.5c13.4-8.3 29.3-17.6 47.4-17.8 17.5-.3 18.4 20.2 5.2 23.4z"/></svg>
|
After Width: | Height: | Size: 1.7 KiB |
58
src/webui/components/Install/index.js
Normal file
58
src/webui/components/Install/index.js
Normal file
@ -0,0 +1,58 @@
|
||||
/**
|
||||
* @prettier
|
||||
*/
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import List from '@material-ui/core/List/index';
|
||||
import ListItemText from '@material-ui/core/ListItemText/index';
|
||||
|
||||
import { DetailContextConsumer } from '../../pages/version/index';
|
||||
import CopyToClipBoard from '../CopyToClipBoard';
|
||||
|
||||
import { Heading, InstallItem, PackageMangerAvatar } from './styles';
|
||||
// logos of package managers
|
||||
import npm from './img/npm.svg';
|
||||
import pnpm from './img/pnpm.svg';
|
||||
import yarn from './img/yarn.svg';
|
||||
|
||||
class Install extends Component {
|
||||
render() {
|
||||
return (
|
||||
<DetailContextConsumer>
|
||||
{context => {
|
||||
return this.renderCopyCLI(context);
|
||||
}}
|
||||
</DetailContextConsumer>
|
||||
);
|
||||
}
|
||||
|
||||
renderCopyCLI = ({ packageName }) => {
|
||||
return (
|
||||
<>
|
||||
<List subheader={<Heading variant={'subheading'}>{'Installation'}</Heading>}>{this.renderListItems(packageName)}</List>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
renderListItems = packageName => {
|
||||
return (
|
||||
<>
|
||||
<InstallItem>
|
||||
<PackageMangerAvatar alt={'npm logo'} src={npm} />
|
||||
<ListItemText primary={<CopyToClipBoard text={`npm install ${packageName}`} />} secondary={'Install using NPM'} />
|
||||
</InstallItem>
|
||||
<InstallItem>
|
||||
<PackageMangerAvatar alt={'yarn logo'} src={yarn} />
|
||||
<ListItemText primary={<CopyToClipBoard text={`yarn add ${packageName}`} />} secondary={'Install using Yarn'} />
|
||||
</InstallItem>
|
||||
<InstallItem>
|
||||
<PackageMangerAvatar alt={'pnpm logo'} src={pnpm} />
|
||||
<ListItemText primary={<CopyToClipBoard text={`pnpm install ${packageName}`} />} secondary={'Install using PNPM'} />
|
||||
</InstallItem>
|
||||
</>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default Install;
|
28
src/webui/components/Install/styles.js
Normal file
28
src/webui/components/Install/styles.js
Normal file
@ -0,0 +1,28 @@
|
||||
/**
|
||||
* @prettier
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import styled from 'react-emotion';
|
||||
import Typography from '@material-ui/core/Typography/index';
|
||||
import ListItem from '@material-ui/core/ListItem/index';
|
||||
import Avatar from '@material-ui/core/Avatar/index';
|
||||
|
||||
export const Heading = styled(Typography)`
|
||||
&& {
|
||||
font-weight: 700;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
`;
|
||||
|
||||
export const InstallItem = styled(ListItem)`
|
||||
&& {
|
||||
padding: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
export const PackageMangerAvatar = styled(Avatar)`
|
||||
&& {
|
||||
border-radius: 0px;
|
||||
}
|
||||
`;
|
@ -7,9 +7,11 @@ import styled, { css } from 'react-emotion';
|
||||
|
||||
export const Content = styled.div`
|
||||
&& {
|
||||
background-color: #fff;
|
||||
background-color: #ffffff;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -4,13 +4,14 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import Typography from '@material-ui/core/Typography/index';
|
||||
|
||||
import { IProps } from './types';
|
||||
import { Wrapper } from './styles';
|
||||
|
||||
const NoItems = ({ text }: IProps) => (
|
||||
<Wrapper>
|
||||
<h2>{text}</h2>
|
||||
</Wrapper>
|
||||
<Typography gutterBottom={true} variant={'subtitle1'}>
|
||||
{text}
|
||||
</Typography>
|
||||
);
|
||||
|
||||
export default NoItems;
|
||||
|
1
src/webui/components/NotFound/img/package.svg
Normal file
1
src/webui/components/NotFound/img/package.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 495.2 495.2"><path fill="#d38c0d" d="M325.6 224.4L495.2 126 325.6 28.4l-168.8 99.2z"/><g fill="#efbb67"><path d="M170.4 224.4l168-97.6-168-98.4L0 126.8z"/><path d="M416 368.4l-168 98.4-168-98.4v-196L248 74l168 98.4z"/></g><path fill="#d38c0d" d="M248 74l168 98.4v196l-168 98.4"/><path fill="#efbb67" d="M326.4 314.8L495.2 218l-169.6-98.4L156 218z"/><path fill="#d38c0d" d="M170.4 316.4l168.8-99.2-168.8-97.6L0 218z"/><path fill="#704a0e" d="M248.8 270.8L416 172.4 248.8 74 78.4 172.4z"/><path fill="#513307" d="M248.8 270.8L416 172.4 248.8 74"/><path fill="#2d1c05" d="M248.8 270.8l36-21.6-36-20.8-36 20.8z"/><g fill="#0dd396"><path d="M368 379.6l40-23.2v-12.8l-40 23.2zM368 356.4l40-23.2v-12.8l-40 23.2zM368 333.2l40-23.2v-13.6l-40 24z"/></g></svg>
|
After Width: | Height: | Size: 802 B |
@ -1,21 +1,53 @@
|
||||
/**
|
||||
* @prettier
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Wrapper } from './styles';
|
||||
import { IProps } from './types';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import withWidth, { isWidthUp } from '@material-ui/core/withWidth/index';
|
||||
import ListItem from '@material-ui/core/ListItem/index';
|
||||
import Typography from '@material-ui/core/Typography/index';
|
||||
import { Wrapper, Inner, EmptyPackage, Heading, Card, List } from './styles';
|
||||
import PackageImg from './img/package.svg';
|
||||
|
||||
const NotFound = ({ pkg }: IProps) => (
|
||||
<Wrapper>
|
||||
<h1>
|
||||
{'Error 404 - '}
|
||||
{pkg}
|
||||
</h1>
|
||||
<hr />
|
||||
<p>{'Oops, The package you are trying to access does not exist.'}</p>
|
||||
</Wrapper>
|
||||
// eslint-disable-next-line react/prop-types
|
||||
const NotFound = ({ history, width }) => {
|
||||
const handleGoTo = to => () => {
|
||||
history.push(to);
|
||||
};
|
||||
|
||||
const handleGoBack = () => () => {
|
||||
history.goBack();
|
||||
};
|
||||
|
||||
const renderList = () => (
|
||||
<List>
|
||||
<ListItem button={true} divider={true} onClick={handleGoTo('/')}>
|
||||
{'Home'}
|
||||
</ListItem>
|
||||
<ListItem button={true} divider={true} onClick={handleGoBack()}>
|
||||
{'Back'}
|
||||
</ListItem>
|
||||
</List>
|
||||
);
|
||||
|
||||
export default NotFound;
|
||||
const renderSubTitle = () => (
|
||||
<Typography variant={'subtitle1'}>
|
||||
<div>{"The page you're looking for doesn't exist."}</div>
|
||||
<div>{'Perhaps these links will help find what you are looking for:'}</div>
|
||||
</Typography>
|
||||
);
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<Inner>
|
||||
<EmptyPackage alt={'404 - Page not found'} src={PackageImg} />
|
||||
<Heading variant={isWidthUp('sm', width) ? 'h2' : 'h4'}>{"Sorry, we couldn't find it..."}</Heading>
|
||||
{renderSubTitle()}
|
||||
<Card>{renderList()}</Card>
|
||||
</Inner>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default withRouter(withWidth()(NotFound));
|
||||
|
@ -4,16 +4,43 @@
|
||||
*/
|
||||
|
||||
import styled from 'react-emotion';
|
||||
import Typography from '@material-ui/core/Typography/index';
|
||||
import { default as MuiList } from '@material-ui/core/List/index';
|
||||
import { default as MuiCard } from '@material-ui/core/Card/index';
|
||||
|
||||
import { fontSize, lineHeight } from '../../utils/styles/sizes';
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
&& {
|
||||
font-size: ${fontSize.md};
|
||||
line-height: ${lineHeight.xl};
|
||||
border: none;
|
||||
outline: none;
|
||||
flex-direction: column;
|
||||
export const Wrapper = styled('div')`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
padding: 16px;
|
||||
`;
|
||||
|
||||
export const Inner = styled('div')`
|
||||
max-width: 650px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
export const EmptyPackage = styled('img')`
|
||||
width: 150px;
|
||||
margin: 0 auto;
|
||||
`;
|
||||
|
||||
export const Heading = styled(Typography)`
|
||||
&& {
|
||||
color: #4b5e40;
|
||||
}
|
||||
`;
|
||||
|
||||
export const List = styled(MuiList)`
|
||||
&& {
|
||||
padding: 0;
|
||||
color: #4b5e40;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Card = styled(MuiCard)`
|
||||
margin-top: 24px;
|
||||
`;
|
||||
|
@ -1,8 +0,0 @@
|
||||
/**
|
||||
* @prettier
|
||||
* @flow
|
||||
*/
|
||||
|
||||
export interface IProps {
|
||||
pkg: string;
|
||||
}
|
@ -12,7 +12,7 @@ import { formatDate, formatDateDistance } from '../../utils/package';
|
||||
|
||||
import { IProps } from './types';
|
||||
import {
|
||||
Wrapper,
|
||||
WrapperLink,
|
||||
Header,
|
||||
MainInfo,
|
||||
Name,
|
||||
@ -37,7 +37,7 @@ const getInitialsName = (name: string) =>
|
||||
.reduce((accumulator, currentValue) => accumulator.charAt(0) + currentValue.charAt(0), '')
|
||||
.toUpperCase();
|
||||
|
||||
const Package = ({ name: label, version, time, author: { name, avatar }, description, license, keywords = [] }: IProps): Element<Wrapper> => {
|
||||
const Package = ({ name: label, version, time, author: { name, avatar }, description, license, keywords = [] }: IProps): Element<WrapperLink> => {
|
||||
const renderMainInfo = () => (
|
||||
<MainInfo>
|
||||
<Name>{label}</Name>
|
||||
@ -80,7 +80,7 @@ const Package = ({ name: label, version, time, author: { name, avatar }, descrip
|
||||
);
|
||||
|
||||
return (
|
||||
<Wrapper className={'package'} to={`detail/${label}`}>
|
||||
<WrapperLink className={'package'} to={`/-/web/detail/${label}`}>
|
||||
<Header>
|
||||
{renderMainInfo()}
|
||||
<Overview>
|
||||
@ -99,7 +99,7 @@ const Package = ({ name: label, version, time, author: { name, avatar }, descrip
|
||||
))}
|
||||
</Footer>
|
||||
)}
|
||||
</Wrapper>
|
||||
</WrapperLink>
|
||||
);
|
||||
};
|
||||
export default Package;
|
||||
|
@ -25,7 +25,7 @@ export const Header = styled.div`
|
||||
|
||||
export const Name = styled.span`
|
||||
&& {
|
||||
${ellipsis('50%')};
|
||||
color: ${colors.primary};
|
||||
}
|
||||
`;
|
||||
|
||||
@ -159,7 +159,7 @@ export const Footer = styled.div`
|
||||
`;
|
||||
|
||||
// Container
|
||||
export const Wrapper = styled(Link)`
|
||||
export const WrapperLink = styled(Link)`
|
||||
&& {
|
||||
font-size: 12px;
|
||||
background-color: white;
|
||||
|
@ -1,31 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import isNil from 'lodash/isNil';
|
||||
|
||||
import Readme from '../Readme';
|
||||
|
||||
import classes from './packageDetail.scss';
|
||||
|
||||
const displayState = (description) => {
|
||||
return !isNil(description) ? <Readme description={description} /> : '';
|
||||
};
|
||||
|
||||
const PackageDetail = ({packageName, readMe}) => {
|
||||
return (
|
||||
<div className={classes.pkgDetail}>
|
||||
<h1 className={classes.title}>
|
||||
{packageName}
|
||||
</h1>
|
||||
<div className={classes.readme}>
|
||||
{displayState(readMe)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
PackageDetail.propTypes = {
|
||||
readMe: PropTypes.string,
|
||||
packageName: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default PackageDetail;
|
@ -1,16 +0,0 @@
|
||||
@import '../../styles/variables';
|
||||
@import '../../styles/mixins';
|
||||
|
||||
.pkgDetail {
|
||||
.title {
|
||||
font-size: $font-size-xxl;
|
||||
font-weight: $font-weight-semibold;
|
||||
margin: 0 0 40px;
|
||||
padding-bottom: 5px;
|
||||
@include border-bottom-default($greyGainsboro);
|
||||
}
|
||||
|
||||
.readme {
|
||||
margin-bottom: 5em;
|
||||
}
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
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,
|
||||
};
|
@ -1,24 +0,0 @@
|
||||
@import '../../../styles/variables';
|
||||
@import '../../../styles/mixins';
|
||||
|
||||
.module {
|
||||
|
||||
margin-bottom: 10px;
|
||||
|
||||
.moduleTitle {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
font-size: $font-size-lg;
|
||||
margin: 0 0 10px;
|
||||
padding: 5px 0;
|
||||
font-weight: $font-weight-semibold;
|
||||
@include border-bottom-default($greyGainsboro);
|
||||
|
||||
span { // description
|
||||
font-size: $font-size-sm;
|
||||
color: $greyChateau;
|
||||
margin-left: auto;
|
||||
font-weight: $font-weight-light;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
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,
|
||||
};
|
@ -1,8 +0,0 @@
|
||||
@import '../../../styles/variables';
|
||||
|
||||
.emptyPlaceholder {
|
||||
text-align: center;
|
||||
margin: 20px 0;
|
||||
font-size: $font-size-base;
|
||||
color: $greyChateau;
|
||||
}
|
@ -1,104 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import get from 'lodash/get';
|
||||
import LastSync from './modules/LastSync';
|
||||
import DistTags from './modules/DistTags';
|
||||
import Maintainers from './modules/Maintainers';
|
||||
import Dependencies from './modules/Dependencies';
|
||||
import PeerDependencies from './modules/PeerDependencies';
|
||||
import Infos from './modules/Infos';
|
||||
|
||||
import {
|
||||
formatLicense,
|
||||
formatRepository,
|
||||
getLastUpdatedPackageTime,
|
||||
getRecentReleases,
|
||||
} from '../../utils/package';
|
||||
import API from '../../utils/api';
|
||||
import {DIST_TAGS} from '../../../lib/constants';
|
||||
|
||||
export default class PackageSidebar extends React.Component {
|
||||
state = {};
|
||||
|
||||
static propTypes = {
|
||||
packageName: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.loadPackageData = this.loadPackageData.bind(this);
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
const { packageName } = this.props;
|
||||
await this.loadPackageData(packageName);
|
||||
}
|
||||
|
||||
async loadPackageData(packageName) {
|
||||
let packageMeta;
|
||||
|
||||
try {
|
||||
packageMeta = await API.request(`sidebar/${packageName}`, 'GET');
|
||||
} catch (err) {
|
||||
this.setState({
|
||||
failed: true,
|
||||
});
|
||||
}
|
||||
|
||||
this.setState({
|
||||
packageMeta,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { packageMeta } = this.state;
|
||||
|
||||
if (packageMeta) {
|
||||
const {time, _uplinks} = packageMeta;
|
||||
|
||||
// Infos component
|
||||
const license = formatLicense(get(packageMeta, 'latest.license', null));
|
||||
const repository = formatRepository(
|
||||
get(packageMeta, 'latest.repository', null)
|
||||
);
|
||||
const homepage = get(packageMeta, 'latest.homepage', null);
|
||||
|
||||
// dist-tags
|
||||
const distTags = packageMeta[DIST_TAGS];
|
||||
|
||||
// Lastsync component
|
||||
const recentReleases = getRecentReleases(time);
|
||||
const lastUpdated = getLastUpdatedPackageTime(_uplinks);
|
||||
|
||||
// Dependencies component
|
||||
const dependencies = get(packageMeta, 'latest.dependencies', {});
|
||||
const peerDependencies = get(packageMeta, 'latest.peerDependencies', {});
|
||||
|
||||
// Maintainers component
|
||||
return (
|
||||
<aside className={'sidebar-info'}>
|
||||
{time && (
|
||||
<LastSync
|
||||
lastUpdated={lastUpdated}
|
||||
recentReleases={recentReleases}
|
||||
/>
|
||||
)}
|
||||
<DistTags distTags={distTags} />
|
||||
<Infos
|
||||
homepage={homepage}
|
||||
license={license}
|
||||
repository={repository}
|
||||
/>
|
||||
{/* TODO: Refacor later, when we decide to show only maintainers/authors */}
|
||||
<Maintainers packageMeta={packageMeta} />
|
||||
<Dependencies dependencies={dependencies} />
|
||||
<PeerDependencies dependencies={peerDependencies} />
|
||||
{/* Package management module? Help us implement it! */}
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<aside className={'sidebar-loading'}>{'Loading package information...'}</aside>
|
||||
);
|
||||
}
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Module from '../../Module';
|
||||
|
||||
import {getDetailPageURL} from '../../../../utils/url';
|
||||
import ModuleContentPlaceholder from '../../ModuleContentPlaceholder';
|
||||
|
||||
import classes from './style.scss';
|
||||
|
||||
export const NO_DEPENDENCIES = 'Zero Dependencies!';
|
||||
export const DEP_ITEM_CLASS = 'dependency-item';
|
||||
|
||||
const renderDependenciesList = (dependencies, dependenciesList) => {
|
||||
return (
|
||||
<ul>
|
||||
{dependenciesList.map((dependenceName, index) => {
|
||||
return (
|
||||
<li
|
||||
className={DEP_ITEM_CLASS}
|
||||
key={index}
|
||||
title={`Depend on version: ${dependencies[dependenceName]}`}
|
||||
>
|
||||
<a href={getDetailPageURL(dependenceName)}>{dependenceName}</a>
|
||||
{index < dependenciesList.length - 1 && <span>{', '}</span>}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
|
||||
const Dependencies = ({dependencies = {}, title = 'Dependencies'}) => {
|
||||
const dependenciesList = Object.keys(dependencies);
|
||||
return (
|
||||
<Module className={classes.dependenciesModule} title={title}>
|
||||
{dependenciesList.length > 0 ? (
|
||||
renderDependenciesList(dependencies, dependenciesList)
|
||||
) : (
|
||||
<ModuleContentPlaceholder text={NO_DEPENDENCIES} />
|
||||
)}
|
||||
</Module>
|
||||
);
|
||||
};
|
||||
|
||||
Dependencies.propTypes = {
|
||||
dependencies: PropTypes.object,
|
||||
title: PropTypes.string,
|
||||
};
|
||||
|
||||
export default Dependencies;
|
@ -1,13 +0,0 @@
|
||||
@import '../../../../styles/variables';
|
||||
|
||||
.dependenciesModule {
|
||||
li {
|
||||
display: inline-block;
|
||||
font-size: $font-size-sm;
|
||||
line-height: $line-height-xxs;
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
import React from 'react';
|
||||
import propTypes from 'prop-types';
|
||||
import Module from '../../Module';
|
||||
import ModuleContentPlaceholder from '../../ModuleContentPlaceholder';
|
||||
|
||||
import classes from './style.scss';
|
||||
|
||||
const renderDistTags = (distTags) => {
|
||||
|
||||
const tags = Object.entries(distTags);
|
||||
|
||||
return (
|
||||
<ul>
|
||||
{tags.map((tagItem) => {
|
||||
const [tag, version] = tagItem;
|
||||
|
||||
return (
|
||||
<li className={'dist-tag-item'} key={tag}>
|
||||
<span>{tag}</span>
|
||||
<span>{version}</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
|
||||
const DistTags = ({distTags = {}}) => {
|
||||
const hasTags = Object.keys(distTags).length > 0;
|
||||
|
||||
return (
|
||||
<Module
|
||||
className={classes.releasesModule}
|
||||
description={''}
|
||||
title={'Dist-Tags'}
|
||||
>
|
||||
{hasTags ? (
|
||||
renderDistTags(distTags)
|
||||
) : (
|
||||
<ModuleContentPlaceholder text={'Not Available!'} />
|
||||
)}
|
||||
</Module>
|
||||
);
|
||||
};
|
||||
|
||||
DistTags.propTypes = {
|
||||
distTags: propTypes.object,
|
||||
};
|
||||
|
||||
export default DistTags;
|
@ -1,13 +0,0 @@
|
||||
@import '../../../../styles/variables';
|
||||
|
||||
.releasesModule {
|
||||
li {
|
||||
display: flex;
|
||||
font-size: $font-size-sm;
|
||||
line-height: $line-height-xs;
|
||||
|
||||
span:last-child {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Module from '../../Module';
|
||||
import ModuleContentPlaceholder from '../../ModuleContentPlaceholder';
|
||||
|
||||
import classes from './style.scss';
|
||||
|
||||
const renderSection = (title, url) => (
|
||||
<li>
|
||||
<span>{title}</span>
|
||||
<a href={url} rel={'noopener noreferrer'} target={'_blank'}>
|
||||
{url}
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
|
||||
const Infos = ({homepage, repository, license}) => {
|
||||
const showInfo = homepage || repository || license;
|
||||
return (
|
||||
<Module className={classes.infosModule} title={'Infos'}>
|
||||
{showInfo ? (
|
||||
<ul>
|
||||
{homepage && renderSection('Homepage', homepage)}
|
||||
{repository && renderSection('Repository', repository)}
|
||||
{license && (
|
||||
<li>
|
||||
<span>{'License'}</span>
|
||||
<span>{license}</span>
|
||||
</li>)}
|
||||
</ul>) : <ModuleContentPlaceholder text={'Not Available!'} />}
|
||||
</Module>);
|
||||
};
|
||||
|
||||
Infos.propTypes = {
|
||||
homepage: PropTypes.string,
|
||||
repository: PropTypes.string,
|
||||
license: PropTypes.string,
|
||||
};
|
||||
|
||||
export default Infos;
|
@ -1,21 +0,0 @@
|
||||
@import '../../../../styles/variables';
|
||||
@import '../../../../styles/mixins';
|
||||
|
||||
.infosModule {
|
||||
li {
|
||||
display: flex;
|
||||
font-size: $font-size-sm;
|
||||
line-height: $line-height-xs;
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
max-width: 150px;
|
||||
@include ellipsis;
|
||||
}
|
||||
|
||||
a:last-child,
|
||||
span:last-child {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
import React from 'react';
|
||||
import propTypes from 'prop-types';
|
||||
import Module from '../../Module';
|
||||
import ModuleContentPlaceholder from '../../ModuleContentPlaceholder';
|
||||
|
||||
import classes from './style.scss';
|
||||
|
||||
const renderRecentReleases = (recentReleases) => (
|
||||
<ul>
|
||||
{recentReleases.map((versionInfo) => {
|
||||
const {version, time} = versionInfo;
|
||||
return (
|
||||
<li className={'last-sync-item'} key={version}>
|
||||
<span>{version}</span>
|
||||
<span>{time}</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
);
|
||||
|
||||
const LastSync = ({recentReleases = [], lastUpdated = ''}) => {
|
||||
return (
|
||||
<Module
|
||||
className={classes.releasesModule}
|
||||
description={lastUpdated}
|
||||
title={'Last Sync'}
|
||||
>
|
||||
{recentReleases.length ? (
|
||||
renderRecentReleases(recentReleases)
|
||||
) : (
|
||||
<ModuleContentPlaceholder text={'Not Available!'} />
|
||||
)}
|
||||
</Module>
|
||||
);
|
||||
};
|
||||
|
||||
LastSync.propTypes = {
|
||||
recentReleases: propTypes.array,
|
||||
lastUpdated: propTypes.string,
|
||||
};
|
||||
|
||||
export default LastSync;
|
@ -1,13 +0,0 @@
|
||||
@import '../../../../styles/variables';
|
||||
|
||||
.releasesModule {
|
||||
li {
|
||||
display: flex;
|
||||
font-size: $font-size-sm;
|
||||
line-height: $line-height-xs;
|
||||
|
||||
span:last-child {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import classes from './style.scss';
|
||||
|
||||
const MaintainerInfo = ({title, name, avatar}) => {
|
||||
const avatarDescription = `${title} ${name}'s avatar`;
|
||||
return (
|
||||
<div className={classes.maintainer} title={name}>
|
||||
<img alt={avatarDescription} src={avatar} title={avatarDescription} />
|
||||
<span className={'maintainer-name'}>{name}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
MaintainerInfo.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
avatar: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default MaintainerInfo;
|
@ -1,26 +0,0 @@
|
||||
@import '../../../../../styles/variables';
|
||||
@import '../../../../../styles/mixins';
|
||||
|
||||
.maintainer {
|
||||
display: flex;
|
||||
line-height: $line-height-xl;
|
||||
cursor: default;
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
margin-right: 10px;
|
||||
border-radius: 100%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: $font-size-sm;
|
||||
flex-shrink: 1;
|
||||
@include ellipsis;
|
||||
}
|
||||
}
|
@ -1,122 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import get from 'lodash/get';
|
||||
import filter from 'lodash/filter';
|
||||
import size from 'lodash/size';
|
||||
import has from 'lodash/has';
|
||||
import uniqBy from 'lodash/uniqBy';
|
||||
|
||||
import Module from '../../Module';
|
||||
import MaintainerInfo from './MaintainerInfo';
|
||||
import ModuleContentPlaceholder from '../../ModuleContentPlaceholder';
|
||||
|
||||
import classes from './style.scss';
|
||||
|
||||
const CONTRIBUTORS_TO_SHOW = 5;
|
||||
|
||||
export default class Maintainers extends React.Component {
|
||||
static propTypes = {
|
||||
packageMeta: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
state = {};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleShowAllContributors = this.handleShowAllContributors.bind(this);
|
||||
}
|
||||
|
||||
get author() {
|
||||
return get(this, 'props.packageMeta.latest.author');
|
||||
}
|
||||
|
||||
get contributors() {
|
||||
const 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() {
|
||||
const { showAllContributors } = this.state;
|
||||
return showAllContributors || size(this.contributors) <= 5;
|
||||
}
|
||||
|
||||
get uniqueContributors() {
|
||||
if (!this.contributors) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return uniqBy(this.contributors, (contributor) => contributor.name).slice(
|
||||
0,
|
||||
CONTRIBUTORS_TO_SHOW
|
||||
);
|
||||
}
|
||||
|
||||
handleShowAllContributors() {
|
||||
this.setState({
|
||||
showAllContributors: true,
|
||||
});
|
||||
}
|
||||
|
||||
renderContributors() {
|
||||
if (!this.contributors) return null;
|
||||
|
||||
return (this.showAllContributors
|
||||
? this.contributors
|
||||
: this.uniqueContributors
|
||||
).map((contributor, index) => {
|
||||
return (
|
||||
<MaintainerInfo
|
||||
avatar={contributor.avatar}
|
||||
key={index}
|
||||
name={contributor.name}
|
||||
title={'Contributors'}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
renderAuthorAndContributors(author) {
|
||||
return (
|
||||
<div>
|
||||
<ul className={'maintainer-author'}>
|
||||
{author &&
|
||||
author.name && (
|
||||
<MaintainerInfo
|
||||
avatar={author.avatar}
|
||||
name={author.name}
|
||||
title={'Author'}
|
||||
/>
|
||||
)}
|
||||
{this.renderContributors()}
|
||||
</ul>
|
||||
{!this.showAllContributors && (
|
||||
<button
|
||||
className={classes.showAllContributors}
|
||||
onClick={this.handleShowAllContributors}
|
||||
title={'Current list only show the author and first 5 contributors unique by name'}
|
||||
>
|
||||
{'Show all contributor'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const contributors = this.renderContributors();
|
||||
return (
|
||||
<Module className={classes.maintainersModule} title={'Maintainers'}>
|
||||
{contributors.length || has(this.author, 'name') ? (
|
||||
this.renderAuthorAndContributors(this.author)
|
||||
) : (
|
||||
<ModuleContentPlaceholder text={'Not Available!'} />
|
||||
)}
|
||||
</Module>
|
||||
);
|
||||
}
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
@import '../../../../styles/variables';
|
||||
|
||||
.maintainersModule {
|
||||
.showAllContributors {
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: $font-size-sm;
|
||||
text-align: center;
|
||||
padding: 10px 0;
|
||||
}
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Dependencies from '../Dependencies';
|
||||
|
||||
export const TITLE = 'Peer Dependencies';
|
||||
|
||||
const PeerDependencies = ({dependencies = {}, title = TITLE}) => {
|
||||
return (
|
||||
<Dependencies dependencies={dependencies} title={title} />
|
||||
);
|
||||
};
|
||||
|
||||
PeerDependencies.propTypes = {
|
||||
dependencies: PropTypes.object,
|
||||
title: PropTypes.string,
|
||||
};
|
||||
|
||||
export default PeerDependencies;
|
BIN
src/webui/components/Repository/img/git.png
Normal file
BIN
src/webui/components/Repository/img/git.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.3 KiB |
59
src/webui/components/Repository/index.js
Normal file
59
src/webui/components/Repository/index.js
Normal file
@ -0,0 +1,59 @@
|
||||
/* eslint react/jsx-max-depth: 0 */
|
||||
|
||||
import React, {Component} from 'react';
|
||||
import Avatar from '@material-ui/core/Avatar';
|
||||
import List from '@material-ui/core/List';
|
||||
import ListItemText from '@material-ui/core/ListItemText';
|
||||
|
||||
import { DetailContextConsumer } from '../../pages/version/index';
|
||||
import CopyToClipBoard from '../CopyToClipBoard';
|
||||
|
||||
import { Heading, GithubLink, RepositoryListItem } from './styles';
|
||||
import git from './img/git.png';
|
||||
|
||||
class Repository extends Component<any, any> {
|
||||
render() {
|
||||
return (
|
||||
<DetailContextConsumer>
|
||||
{(context) => {
|
||||
return this.renderRepository(context);
|
||||
}}
|
||||
</DetailContextConsumer>
|
||||
);
|
||||
};
|
||||
|
||||
renderRepositoryText(url) {
|
||||
return (<GithubLink href={url} target={"_blank"}>{url}</GithubLink>);
|
||||
}
|
||||
|
||||
renderRepository = ({packageMeta}) => {
|
||||
const { repository, homepage } = packageMeta.latest;
|
||||
if (!repository) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// we prefer homepage first, because it's more cleaner
|
||||
const url = homepage || repository.url;
|
||||
return (
|
||||
<>
|
||||
<List dense={true} subheader={<Heading variant={"subheading"}>{'Repository'}</Heading>}>
|
||||
<RepositoryListItem>
|
||||
<Avatar src={git} />
|
||||
<ListItemText primary={this.renderContent(url)} />
|
||||
</RepositoryListItem>
|
||||
</List>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
renderContent(url) {
|
||||
return (
|
||||
<CopyToClipBoard text={url}>
|
||||
{this.renderRepositoryText(url)}
|
||||
</CopyToClipBoard>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default Repository;
|
46
src/webui/components/Repository/styles.js
Normal file
46
src/webui/components/Repository/styles.js
Normal file
@ -0,0 +1,46 @@
|
||||
/**
|
||||
* @prettier
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import styled from 'react-emotion';
|
||||
import Grid from '@material-ui/core/Grid/index';
|
||||
import ListItem from '@material-ui/core/ListItem/index';
|
||||
import Typography from '@material-ui/core/Typography/index';
|
||||
|
||||
import Github from '../../icons/GitHub';
|
||||
import colors from '../../utils/styles/colors';
|
||||
|
||||
export const Heading = styled(Typography)`
|
||||
&& {
|
||||
font-weight: 700;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
`;
|
||||
|
||||
export const GridRepo = styled(Grid)`
|
||||
&& {
|
||||
align-items: center;
|
||||
}
|
||||
`;
|
||||
|
||||
export const GithubLink = styled('a')`
|
||||
&& {
|
||||
color: ${colors.primary};
|
||||
}
|
||||
`;
|
||||
|
||||
export const GithubLogo = styled(Github)`
|
||||
&& {
|
||||
font-size: 40px;
|
||||
color: ${colors.primary};
|
||||
background-color: ${colors.greySuperLight};
|
||||
}
|
||||
`;
|
||||
|
||||
export const RepositoryListItem = styled(ListItem)`
|
||||
&& {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
`;
|
@ -5,6 +5,7 @@
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import type { Node } from 'react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
|
||||
import { default as IconSearch } from '@material-ui/icons/Search';
|
||||
import InputAdornment from '@material-ui/core/InputAdornment';
|
||||
@ -13,7 +14,6 @@ import debounce from 'lodash/debounce';
|
||||
import API from '../../utils/api';
|
||||
import AutoComplete from '../AutoComplete';
|
||||
import colors from '../../utils/styles/colors';
|
||||
import { getDetailPageURL } from '../../utils/url';
|
||||
|
||||
import { IProps, IState } from './types';
|
||||
import type { cancelAllSearchRequests, handlePackagesClearRequested, handleSearch, handleClickSearch, handleFetchPackages, onBlur } from './types';
|
||||
@ -24,7 +24,7 @@ const CONSTANTS = {
|
||||
ABORT_ERROR: 'AbortError',
|
||||
};
|
||||
|
||||
class Search extends Component<IProps, IState> {
|
||||
export class Search extends Component<IProps, IState> {
|
||||
requestList: Array<any>;
|
||||
|
||||
constructor(props: IProps) {
|
||||
@ -92,13 +92,15 @@ class Search extends Component<IProps, IState> {
|
||||
* When an user select any package by clicking or pressing return key.
|
||||
*/
|
||||
handleClickSearch: handleClickSearch = (event, { suggestionValue, method }) => {
|
||||
const { history } = this.props;
|
||||
// stops event bubbling
|
||||
event.stopPropagation();
|
||||
switch (method) {
|
||||
case 'click':
|
||||
case 'enter':
|
||||
this.setState({ search: '' });
|
||||
window.location.assign(getDetailPageURL(suggestionValue));
|
||||
// $FlowFixMe
|
||||
history.push(`/-/web/detail/${suggestionValue}`);
|
||||
break;
|
||||
}
|
||||
};
|
||||
@ -181,4 +183,4 @@ class Search extends Component<IProps, IState> {
|
||||
}
|
||||
}
|
||||
|
||||
export default Search;
|
||||
export default withRouter(Search);
|
||||
|
@ -3,7 +3,9 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
export interface IProps {}
|
||||
export interface IProps {
|
||||
history?: any;
|
||||
}
|
||||
|
||||
export interface IState {
|
||||
search: string;
|
||||
|
56
src/webui/components/UpLinks/index.js
Normal file
56
src/webui/components/UpLinks/index.js
Normal file
@ -0,0 +1,56 @@
|
||||
/**
|
||||
* @prettier
|
||||
*/
|
||||
import React from 'react';
|
||||
import List from '@material-ui/core/List/index';
|
||||
import ListItem from '@material-ui/core/ListItem/index';
|
||||
|
||||
import { DetailContextConsumer } from '../../pages/version/index';
|
||||
import NoItems from '../NoItems';
|
||||
import { formatDateDistance } from '../../utils/package';
|
||||
|
||||
import { Heading, Spacer, ListItemText } from './styles';
|
||||
|
||||
class UpLinks extends React.PureComponent<any> {
|
||||
render() {
|
||||
return (
|
||||
// $FlowFixMe
|
||||
<DetailContextConsumer>
|
||||
{({ packageMeta }) => {
|
||||
return this.renderContent(packageMeta._uplinks, packageMeta.latest);
|
||||
}}
|
||||
</DetailContextConsumer>
|
||||
);
|
||||
}
|
||||
|
||||
renderUpLinksList = uplinks => (
|
||||
<List>
|
||||
{Object.keys(uplinks)
|
||||
.reverse()
|
||||
.map(name => (
|
||||
<ListItem key={name}>
|
||||
<ListItemText>{name}</ListItemText>
|
||||
<Spacer />
|
||||
<ListItemText>{`${formatDateDistance(uplinks[name].fetched)} ago`}</ListItemText>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
);
|
||||
|
||||
renderContent(uplinks, { name }) {
|
||||
console.log(uplinks);
|
||||
if (Object.keys(uplinks).length > 0) {
|
||||
return (
|
||||
uplinks && (
|
||||
<>
|
||||
<Heading variant={'subheading'}>{'Uplinks'}</Heading>
|
||||
{this.renderUpLinksList(uplinks)}
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
return <NoItems text={`${name} has no uplinks.`} />;
|
||||
}
|
||||
}
|
||||
|
||||
export default UpLinks;
|
25
src/webui/components/UpLinks/styles.js
Normal file
25
src/webui/components/UpLinks/styles.js
Normal file
@ -0,0 +1,25 @@
|
||||
import styled from 'react-emotion';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import { default as MuiListItemText } from '@material-ui/core/ListItemText';
|
||||
|
||||
export const Heading = styled(Typography)`
|
||||
&& {
|
||||
font-weight: 700;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Spacer = styled('div')`
|
||||
flex: 1 1 auto;
|
||||
border-bottom: 1px dotted rgba(0, 0, 0, .2);
|
||||
white-space: nowrap;
|
||||
height: 0.5em;
|
||||
`;
|
||||
|
||||
export const ListItemText = styled(MuiListItemText)`
|
||||
&& {
|
||||
flex: none;
|
||||
color: black;
|
||||
opacity: .6;
|
||||
}
|
||||
`;
|
||||
|
6
src/webui/components/UpLinks/types.js
Normal file
6
src/webui/components/UpLinks/types.js
Normal file
@ -0,0 +1,6 @@
|
||||
/**
|
||||
* @prettier
|
||||
* @flow
|
||||
*/
|
||||
|
||||
export interface IProps {}
|
61
src/webui/components/Versions/index.js
Normal file
61
src/webui/components/Versions/index.js
Normal file
@ -0,0 +1,61 @@
|
||||
/**
|
||||
* @prettier
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import { DetailContextConsumer } from '../../pages/version/index';
|
||||
import { formatDateDistance } from '../../utils/package';
|
||||
import { Heading, Spacer, ListItemText } from './styles';
|
||||
import List from '@material-ui/core/List/index';
|
||||
import ListItem from '@material-ui/core/ListItem/index';
|
||||
import React from 'react';
|
||||
import { DIST_TAGS } from '../../../lib/constants';
|
||||
|
||||
class Versions extends React.PureComponent<any> {
|
||||
render() {
|
||||
return (
|
||||
// $FlowFixMe
|
||||
<DetailContextConsumer>
|
||||
{({ packageMeta }) => {
|
||||
return this.renderContent(packageMeta[DIST_TAGS], packageMeta.versions);
|
||||
}}
|
||||
</DetailContextConsumer>
|
||||
);
|
||||
}
|
||||
|
||||
renderPackageList = (packages: any, isVersion: boolean = false) => (
|
||||
<List>
|
||||
{Object.keys(packages)
|
||||
.reverse()
|
||||
.map(version => (
|
||||
<ListItem key={version}>
|
||||
<ListItemText>{version}</ListItemText>
|
||||
<Spacer />
|
||||
<ListItemText>{isVersion ? `${formatDateDistance('2017-10-26T09:03:15.044Z')} ago` : packages[version]}</ListItemText>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
);
|
||||
|
||||
// $FlowFixMe
|
||||
renderContent(distTags: object, versions: object) {
|
||||
return (
|
||||
<>
|
||||
{distTags && (
|
||||
<>
|
||||
<Heading variant={'subheading'}>{'Current Tags'}</Heading>
|
||||
{this.renderPackageList(distTags)}
|
||||
</>
|
||||
)}
|
||||
{versions && (
|
||||
<>
|
||||
<Heading variant={'subheading'}>{'Version History'}</Heading>
|
||||
{this.renderPackageList(versions, true)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Versions;
|
25
src/webui/components/Versions/styles.js
Normal file
25
src/webui/components/Versions/styles.js
Normal file
@ -0,0 +1,25 @@
|
||||
import styled from 'react-emotion';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import { default as MuiListItemText } from '@material-ui/core/ListItemText';
|
||||
|
||||
export const Heading = styled(Typography)`
|
||||
&& {
|
||||
font-weight: 700;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Spacer = styled('div')`
|
||||
flex: 1 1 auto;
|
||||
border-bottom: 1px dotted rgba(0, 0, 0, .2);
|
||||
white-space: nowrap;
|
||||
height: 0.5em;
|
||||
`;
|
||||
|
||||
export const ListItemText = styled(MuiListItemText)`
|
||||
&& {
|
||||
flex: none;
|
||||
color: black;
|
||||
opacity: .6;
|
||||
}
|
||||
`;
|
||||
|
6
src/webui/components/Versions/types.js
Normal file
6
src/webui/components/Versions/types.js
Normal file
@ -0,0 +1,6 @@
|
||||
/**
|
||||
* @prettier
|
||||
* @flow
|
||||
*/
|
||||
|
||||
export interface IProps {}
|
9
src/webui/history.js
Normal file
9
src/webui/history.js
Normal file
@ -0,0 +1,9 @@
|
||||
/**
|
||||
* @prettier
|
||||
*/
|
||||
|
||||
import { createBrowserHistory } from 'history';
|
||||
|
||||
const history = createBrowserHistory();
|
||||
|
||||
export default history;
|
16
src/webui/icons/GitHub.js
Normal file
16
src/webui/icons/GitHub.js
Normal file
@ -0,0 +1,16 @@
|
||||
// @flow
|
||||
/* eslint-disable max-len */
|
||||
/* eslint-disable react/jsx-curly-brace-presence */
|
||||
|
||||
import React from 'react';
|
||||
import SvgIcon from '@material-ui/core/SvgIcon/index';
|
||||
|
||||
function GitHub(props: Object) {
|
||||
return (
|
||||
<SvgIcon {...props}>
|
||||
<path d="M12.007 0C6.12 0 1.1 4.27.157 10.08c-.944 5.813 2.468 11.45 8.054 13.312.19.064.397.033.555-.084.16-.117.25-.304.244-.5v-2.042c-3.33.735-4.037-1.56-4.037-1.56-.22-.726-.694-1.35-1.334-1.756-1.096-.75.074-.735.074-.735.773.103 1.454.557 1.846 1.23.694 1.21 2.23 1.638 3.45.96.056-.61.327-1.178.766-1.605-2.67-.3-5.462-1.335-5.462-6.002-.02-1.193.42-2.35 1.23-3.226-.327-1.015-.27-2.116.166-3.09 0 0 1.006-.33 3.3 1.23 1.966-.538 4.04-.538 6.003 0 2.295-1.5 3.3-1.23 3.3-1.23.445 1.006.49 2.144.12 3.18.81.877 1.25 2.033 1.23 3.226 0 4.607-2.805 5.627-5.476 5.927.578.583.88 1.386.825 2.206v3.29c-.005.2.092.393.26.507.164.115.377.14.565.063 5.568-1.88 8.956-7.514 8.007-13.313C22.892 4.267 17.884.007 12.008 0z" />
|
||||
</SvgIcon>
|
||||
);
|
||||
}
|
||||
|
||||
export default GitHub;
|
@ -1,25 +0,0 @@
|
||||
@import '../../styles/variables';
|
||||
@import '../../styles/mixins';
|
||||
|
||||
.twoColumn {
|
||||
@include container-size;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,87 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
|
||||
import PackageDetail from '../../components/PackageDetail';
|
||||
import NotFound from '../../components/NotFound';
|
||||
import Spinner from '../../components/Spinner';
|
||||
import API from '../../utils/api';
|
||||
|
||||
import classes from './detail.scss';
|
||||
import PackageSidebar from '../../components/PackageSidebar/index';
|
||||
|
||||
export default class Detail extends Component {
|
||||
static propTypes = {
|
||||
match: PropTypes.object,
|
||||
isUserLoggedIn: PropTypes.bool,
|
||||
};
|
||||
|
||||
state = {
|
||||
readMe: '',
|
||||
notFound: false,
|
||||
};
|
||||
|
||||
getPackageName(props = this.props) {
|
||||
const params = props.match.params;
|
||||
return `${(params.scope && '@' + params.scope + '/') || ''}${
|
||||
params.package
|
||||
}`;
|
||||
}
|
||||
|
||||
get packageName() {
|
||||
return this.getPackageName();
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
await this.loadPackageInfo(this.packageName);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const { isUserLoggedIn, match } = this.props;
|
||||
const condition1 = prevProps.isUserLoggedIn !== isUserLoggedIn;
|
||||
const condition2 =
|
||||
prevProps.match.params.package !== match.params.package;
|
||||
if (condition1 || condition2) {
|
||||
const packageName = this.getPackageName(this.props);
|
||||
this.loadPackageInfo(packageName);
|
||||
}
|
||||
}
|
||||
|
||||
async loadPackageInfo(packageName) {
|
||||
this.setState({
|
||||
readMe: '',
|
||||
});
|
||||
|
||||
try {
|
||||
const resp = await API.request(`package/readme/${packageName}`, 'GET');
|
||||
this.setState({
|
||||
readMe: resp,
|
||||
notFound: false,
|
||||
});
|
||||
} catch (err) {
|
||||
this.setState({
|
||||
notFound: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { notFound, readMe } = this.state;
|
||||
|
||||
if (notFound) {
|
||||
return (
|
||||
<div className={'container content'}>
|
||||
<NotFound pkg={this.packageName} />
|
||||
</div>
|
||||
);
|
||||
} else if (isEmpty(readMe)) {
|
||||
return <Spinner centered={true} />;
|
||||
}
|
||||
return (
|
||||
<div className={`container content ${classes.twoColumn}`}>
|
||||
<PackageDetail packageName={this.packageName} readMe={readMe} />
|
||||
<PackageSidebar packageName={this.packageName} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
135
src/webui/pages/version/index.js
Normal file
135
src/webui/pages/version/index.js
Normal file
@ -0,0 +1,135 @@
|
||||
/**
|
||||
* @prettier
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import Grid from '@material-ui/core/Grid/index';
|
||||
import Loading from '../../components/Loading';
|
||||
import DetailContainer from '../../components/DetailContainer';
|
||||
import DetailSidebar from '../../components/DetailSidebar';
|
||||
import { callDetailPage } from '../../utils/calls';
|
||||
import { getRouterPackageName } from '../../utils/package';
|
||||
|
||||
export const DetailContext = React.createContext();
|
||||
|
||||
export const DetailContextProvider = DetailContext.Provider;
|
||||
export const DetailContextConsumer = DetailContext.Consumer;
|
||||
|
||||
class VersionPage extends Component<any, any> {
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
readMe: '',
|
||||
packageName: getRouterPackageName(props.match),
|
||||
packageMeta: null,
|
||||
isLoading: true,
|
||||
notFound: false,
|
||||
};
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
await this.loadPackageInfo();
|
||||
}
|
||||
|
||||
/* eslint no-unused-vars: 0 */
|
||||
async componentDidUpdate(nextProps: any, prevState: any) {
|
||||
const { packageName } = this.state;
|
||||
if (packageName !== prevState.packageName) {
|
||||
const { readMe, packageMeta } = await callDetailPage(packageName);
|
||||
this.setState({
|
||||
readMe,
|
||||
packageMeta,
|
||||
packageName,
|
||||
notFound: false,
|
||||
isLoading: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(nextProps: any, prevState: any) {
|
||||
const { match } = nextProps;
|
||||
const packageName = getRouterPackageName(match);
|
||||
|
||||
if (packageName !== prevState.packageName) {
|
||||
try {
|
||||
return {
|
||||
packageName,
|
||||
isLoading: false,
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
notFound: true,
|
||||
isLoading: false,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async loadPackageInfo() {
|
||||
const { packageName } = this.state;
|
||||
// FIXME: use utility
|
||||
document.title = `Verdaccio - ${packageName}`;
|
||||
|
||||
this.setState({
|
||||
readMe: '',
|
||||
});
|
||||
|
||||
try {
|
||||
const { readMe, packageMeta } = await callDetailPage(packageName);
|
||||
this.setState({
|
||||
readMe,
|
||||
packageMeta,
|
||||
packageName,
|
||||
notFound: false,
|
||||
isLoading: false,
|
||||
});
|
||||
} catch (err) {
|
||||
this.setState({
|
||||
notFound: true,
|
||||
packageName,
|
||||
isLoading: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
enableLoading = () => {
|
||||
this.setState({
|
||||
isLoading: true,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { isLoading, packageMeta, readMe, packageName } = this.state;
|
||||
|
||||
if (isLoading === false) {
|
||||
return (
|
||||
<DetailContextProvider value={{ packageMeta, readMe, packageName, enableLoading: this.enableLoading }}>
|
||||
<Grid className={'container content'} container={true} spacing={0}>
|
||||
<Grid item={true} xs={8}>
|
||||
{this.renderDetail()}
|
||||
</Grid>
|
||||
<Grid item={true} xs={4}>
|
||||
{this.renderSidebar()}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</DetailContextProvider>
|
||||
);
|
||||
} else {
|
||||
return <Loading />;
|
||||
}
|
||||
}
|
||||
|
||||
renderDetail() {
|
||||
return <DetailContainer />;
|
||||
}
|
||||
|
||||
renderSidebar() {
|
||||
return <DetailSidebar />;
|
||||
}
|
||||
}
|
||||
|
||||
export default VersionPage;
|
17
src/webui/pages/version/styles.js
Normal file
17
src/webui/pages/version/styles.js
Normal file
@ -0,0 +1,17 @@
|
||||
/**
|
||||
* @prettier
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import styled from 'react-emotion';
|
||||
import DialogTitle from '@material-ui/core/DialogTitle/index';
|
||||
import colors from '../../utils/styles/colors';
|
||||
import { fontSize } from '../../utils/styles/sizes';
|
||||
|
||||
export const Title = styled(DialogTitle)`
|
||||
&& {
|
||||
background-color: ${colors.primary};
|
||||
color: ${colors.white};
|
||||
font-size: ${fontSize.lg};
|
||||
}
|
||||
`;
|
0
src/webui/pages/version/types.js
Normal file
0
src/webui/pages/version/types.js
Normal file
@ -3,44 +3,67 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { HashRouter as Router, Route, Switch } from 'react-router-dom';
|
||||
/* eslint react/jsx-max-depth:0 */
|
||||
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import { Router, Route, Switch } from 'react-router-dom';
|
||||
import { AppContextConsumer } from './app';
|
||||
|
||||
import { asyncComponent } from './utils/asyncComponent';
|
||||
import history from './history';
|
||||
import Header from './components/Header';
|
||||
|
||||
const DetailPackage = asyncComponent(() => import('./pages/detail'));
|
||||
const NotFound = asyncComponent(() => import('./components/NotFound'));
|
||||
const VersionPackage = asyncComponent(() => import('./pages/version'));
|
||||
const HomePage = asyncComponent(() => import('./pages/home'));
|
||||
|
||||
interface IProps {
|
||||
isUserLoggedIn: boolean;
|
||||
packages: Array<Object>;
|
||||
}
|
||||
|
||||
interface IState {}
|
||||
|
||||
class RouterApp extends Component<IProps, IState> {
|
||||
class RouterApp extends Component<any, any> {
|
||||
render() {
|
||||
return (
|
||||
<Router>
|
||||
<Router history={history}>
|
||||
<Fragment>
|
||||
{this.renderHeader()}
|
||||
<Switch>
|
||||
<Route exact={true} path={'/'} render={this.renderHomePage} />
|
||||
<Route exact={true} path={'/detail/@:scope/:package'} render={this.renderDetailPage} />
|
||||
<Route exact={true} path={'/detail/:package'} render={this.renderDetailPage} />
|
||||
<Route exact={true} path={'/-/web/detail/@:scope/:package'} render={this.renderVersionPage} />
|
||||
<Route exact={true} path={'/-/web/detail/:package'} render={this.renderVersionPage} />
|
||||
<Route component={NotFound} />
|
||||
</Switch>
|
||||
</Fragment>
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
|
||||
renderHomePage = () => {
|
||||
const { isUserLoggedIn, packages } = this.props;
|
||||
renderHeader = () => {
|
||||
const { onLogout, onToggleLoginModal } = this.props;
|
||||
|
||||
return <HomePage isUserLoggedIn={isUserLoggedIn} packages={packages} />;
|
||||
return (
|
||||
<AppContextConsumer>
|
||||
{function renderConsumerVersionPage({ logoUrl, scope, user }) {
|
||||
return <Header logo={logoUrl} onLogout={onLogout} onToggleLoginModal={onToggleLoginModal} scope={scope} username={user.username} />;
|
||||
}}
|
||||
</AppContextConsumer>
|
||||
);
|
||||
};
|
||||
|
||||
renderDetailPage = (routerProps: any) => {
|
||||
const { isUserLoggedIn } = this.props;
|
||||
renderHomePage = () => {
|
||||
return (
|
||||
<AppContextConsumer>
|
||||
{function renderConsumerVersionPage({ isUserLoggedIn, packages }) {
|
||||
return <HomePage isUserLoggedIn={isUserLoggedIn} packages={packages} />;
|
||||
}}
|
||||
</AppContextConsumer>
|
||||
);
|
||||
};
|
||||
|
||||
return <DetailPackage {...routerProps} isUserLoggedIn={isUserLoggedIn} />;
|
||||
renderVersionPage = (routerProps: any) => {
|
||||
return (
|
||||
<AppContextConsumer>
|
||||
{function renderConsumerVersionPage({ isUserLoggedIn }) {
|
||||
return <VersionPackage {...routerProps} isUserLoggedIn={isUserLoggedIn} />;
|
||||
}}
|
||||
</AppContextConsumer>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -37,9 +37,10 @@ @mixin fullSize {
|
||||
}
|
||||
|
||||
@mixin container-size {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
@media screen and (min-width: $break-lg) {
|
||||
max-width: $break-lg;
|
||||
width: 100%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
|
12
src/webui/utils/calls.js
Normal file
12
src/webui/utils/calls.js
Normal file
@ -0,0 +1,12 @@
|
||||
import API from './api';
|
||||
|
||||
/**
|
||||
* @prettier
|
||||
*/
|
||||
|
||||
export async function callDetailPage(packageName) {
|
||||
const readMe = await API.request(`package/readme/${packageName}`, 'GET');
|
||||
const packageMeta = await API.request(`sidebar/${packageName}`, 'GET');
|
||||
|
||||
return {readMe, packageMeta};
|
||||
}
|
5
src/webui/utils/file-size.js
Normal file
5
src/webui/utils/file-size.js
Normal file
@ -0,0 +1,5 @@
|
||||
export default function fileSizeSI(a, b, c, d, e) {
|
||||
return (b = Math, c = b.log, d = 1e3, e = c(a) / c(d) | 0, a / b.pow(d, e)).toFixed(2)
|
||||
+ ' ' + (e ? 'kMGTPEZY'[--e] + 'B' : 'Bytes');
|
||||
};
|
||||
|
@ -106,3 +106,13 @@ export function formatDate(lastUpdate) {
|
||||
export function formatDateDistance(lastUpdate) {
|
||||
return distanceInWordsToNow(new Date(lastUpdate));
|
||||
}
|
||||
|
||||
export function getRouterPackageName(match) {
|
||||
const packageName = match.params.package;
|
||||
const scope = match.params.scope;
|
||||
if (scope) {
|
||||
return `@${scope}/${packageName}`;
|
||||
}
|
||||
|
||||
return packageName;
|
||||
}
|
||||
|
12
src/webui/utils/sec-utils.js
Normal file
12
src/webui/utils/sec-utils.js
Normal file
@ -0,0 +1,12 @@
|
||||
/**
|
||||
* @prettier
|
||||
* @flow
|
||||
*/
|
||||
// $FlowFixMe
|
||||
import parseXSS from 'xss';
|
||||
|
||||
export function preventXSS(text: string) {
|
||||
const encodedText = parseXSS(text);
|
||||
|
||||
return encodedText;
|
||||
}
|
@ -8,7 +8,7 @@ const colors = {
|
||||
white: '#fff',
|
||||
red: '#d32f2f',
|
||||
grey: '#808080',
|
||||
|
||||
greySuperLight: '#f5f5f5',
|
||||
greyLight: '#d3d3d3',
|
||||
greyDark: '#a9a9a9',
|
||||
greyChateau: '#95989a',
|
||||
|
@ -2,11 +2,3 @@ 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}`;
|
||||
}
|
||||
|
@ -139,8 +139,8 @@ describe('/ (Verdaccio Page)', () => {
|
||||
});
|
||||
|
||||
test('should contains last sync information', async () => {
|
||||
const versionList = await page.$$('.sidebar-info .last-sync-item');
|
||||
expect(versionList).toHaveLength(3);
|
||||
const versionList = await page.$$('.sidebar-info .detail-info');
|
||||
expect(versionList).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('should publish a protected package', async () => {
|
||||
|
File diff suppressed because one or more lines are too long
@ -1,11 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<PackageSidebar /> : <Infos /> should load the Info component with homepage only 1`] = `"<div class=\\"module infosModule\\"><h2 class=\\"moduleTitle\\">Infos</h2><div><ul><li><span>Homepage</span><a href=\\"https://www.verdaccio.org\\" rel=\\"noopener noreferrer\\" target=\\"_blank\\">https://www.verdaccio.org</a></li></ul></div></div>"`;
|
||||
|
||||
exports[`<PackageSidebar /> : <Infos /> should load the Info component with license only 1`] = `"<div class=\\"module infosModule\\"><h2 class=\\"moduleTitle\\">Infos</h2><div><ul><li><span>License</span><span>MIT</span></li></ul></div></div>"`;
|
||||
|
||||
exports[`<PackageSidebar /> : <Infos /> should load the Info component with props 1`] = `"<div class=\\"module infosModule\\"><h2 class=\\"moduleTitle\\">Infos</h2><div><ul><li><span>Homepage</span><a href=\\"https://www.verdaccio.org\\" rel=\\"noopener noreferrer\\" target=\\"_blank\\">https://www.verdaccio.org</a></li><li><span>Repository</span><a href=\\"https://github.com/verdaccio/verdaccio\\" rel=\\"noopener noreferrer\\" target=\\"_blank\\">https://github.com/verdaccio/verdaccio</a></li><li><span>License</span><span>MIT</span></li></ul></div></div>"`;
|
||||
|
||||
exports[`<PackageSidebar /> : <Infos /> should load the Info component with repository only 1`] = `"<div class=\\"module infosModule\\"><h2 class=\\"moduleTitle\\">Infos</h2><div><ul><li><span>Repository</span><a href=\\"https://github.com/verdaccio/verdaccio\\" rel=\\"noopener noreferrer\\" target=\\"_blank\\">https://github.com/verdaccio/verdaccio</a></li></ul></div></div>"`;
|
||||
|
||||
exports[`<PackageSidebar /> : <Infos /> should load the component without props 1`] = `"<div class=\\"module infosModule\\"><h2 class=\\"moduleTitle\\">Infos</h2><div><p class=\\"emptyPlaceholder\\">Not Available!</p></div></div>"`;
|
@ -1,5 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<PackageSidebar /> : <LastSync /> should check the default props condition 1`] = `"<div class=\\"module releasesModule\\"><h2 class=\\"moduleTitle\\">Last Sync</h2><div><p class=\\"emptyPlaceholder\\">Not Available!</p></div></div>"`;
|
||||
|
||||
exports[`<PackageSidebar /> : <LastSync /> should load the LastSync component and match snapshot 1`] = `"<div class=\\"module releasesModule\\"><h2 class=\\"moduleTitle\\">Last Sync<span>2017/12/14, 15:43:52</span></h2><div><ul><li class=\\"last-sync-item\\"><span>2.7.1</span><span>2017/12/14, 15:43:27</span></li><li class=\\"last-sync-item\\"><span>2.7.0</span><span>2017/12/05, 23:25:06</span></li><li class=\\"last-sync-item\\"><span>2.6.6</span><span>2017/11/08, 22:47:16</span></li></ul></div></div>"`;
|
@ -1,3 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<PackageSidebar /> : <Maintainers /> <MaintainerInfo /> should load the component and match with snapshot 1`] = `"<div class=\\"maintainer\\" title=\\"test\\"><img alt=\\"test-title test's avatar\\" src=\\"http://xyz.com/profile.jpg\\" title=\\"test-title test's avatar\\"/><span class=\\"maintainer-name\\">test</span></div>"`;
|
@ -1,3 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<PackageSidebar /> : <Maintainers /> should match with the props 1`] = `"<div class=\\"module maintainersModule\\"><h2 class=\\"moduleTitle\\">Maintainers</h2><div><div><ul class=\\"maintainer-author\\"><div class=\\"maintainer\\" title=\\"User NPM\\"><img alt=\\"Author User NPM's avatar\\" src=\\"https://www.gravatar.com/avatar/a5a236ba477ee98908600c40cda74f4a\\" title=\\"Author User NPM's avatar\\"><span class=\\"maintainer-name\\">User NPM</span></div><div class=\\"maintainer\\" title=\\"030\\"><img alt=\\"Contributors 030's avatar\\" src=\\"https://www.gravatar.com/avatar/4ef03c2bf8d8689527903212d96fb45b\\" title=\\"Contributors 030's avatar\\"><span class=\\"maintainer-name\\">030</span></div><div class=\\"maintainer\\" title=\\"Alex Vernacchia\\"><img alt=\\"Contributors Alex Vernacchia's avatar\\" src=\\"https://www.gravatar.com/avatar/06975001f7f2be7052bcf978700c6112\\" title=\\"Contributors Alex Vernacchia's avatar\\"><span class=\\"maintainer-name\\">Alex Vernacchia</span></div><div class=\\"maintainer\\" title=\\"Alexander Makarenko\\"><img alt=\\"Contributors Alexander Makarenko's avatar\\" src=\\"https://www.gravatar.com/avatar/d9acfc4ed4e49a436738ff26a722dce4\\" title=\\"Contributors Alexander Makarenko's avatar\\"><span class=\\"maintainer-name\\">Alexander Makarenko</span></div><div class=\\"maintainer\\" title=\\"Alexandre-io\\"><img alt=\\"Contributors Alexandre-io's avatar\\" src=\\"https://www.gravatar.com/avatar/2e095c7cfd278f72825d0fed6e12e3b1\\" title=\\"Contributors Alexandre-io's avatar\\"><span class=\\"maintainer-name\\">Alexandre-io</span></div><div class=\\"maintainer\\" title=\\"Aram Drevekenin\\"><img alt=\\"Contributors Aram Drevekenin's avatar\\" src=\\"https://www.gravatar.com/avatar/371edff6d79c39bb9e36bde39d41a4b0\\" title=\\"Contributors Aram Drevekenin's avatar\\"><span class=\\"maintainer-name\\">Aram Drevekenin</span></div></ul><button class=\\"showAllContributors\\" title=\\"Current list only show the author and first 5 contributors unique by name\\">Show all contributor</button></div></div></div>"`;
|
@ -1,3 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<PackageSidebar /> : <Module /> should load module component 1`] = `"<div class=\\"module module-component\\"><h2 class=\\"moduleTitle\\">Test title<span>Test description</span></h2><div><p>test children</p></div></div>"`;
|
@ -1,3 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<PackageSidebar /> : <ModuleContentPlaceholder /> should load module component 1`] = `"<p class=\\"emptyPlaceholder\\">Test text</p>"`;
|
@ -1,3 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<PackageSidebar /> component should load the packageMeta 1`] = `"<aside class=\\"sidebar-loading\\">Loading package information...</aside>"`;
|
File diff suppressed because one or more lines are too long
@ -1,67 +0,0 @@
|
||||
/**
|
||||
* Dependencies component
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { mount, shallow } from 'enzyme';
|
||||
import Dependencies, {
|
||||
NO_DEPENDENCIES,
|
||||
DEP_ITEM_CLASS
|
||||
} from '../../../../../src/webui/components/PackageSidebar/modules/Dependencies/index';
|
||||
import ModuleContentPlaceholder from '../../../../../src/webui/components/PackageSidebar/ModuleContentPlaceholder';
|
||||
|
||||
|
||||
describe('<PackageSidebar /> : <Dependencies />', () => {
|
||||
test('should load dependencies', () => {
|
||||
const dependencies = {
|
||||
'@verdaccio/file-locking': '0.0.3',
|
||||
'@verdaccio/streams': '0.0.2',
|
||||
JSONStream: '^1.1.1',
|
||||
'apache-md5': '^1.1.2',
|
||||
async: '^2.0.1',
|
||||
'body-parser': '^1.15.0',
|
||||
bunyan: '^1.8.0',
|
||||
chalk: '^2.0.1',
|
||||
commander: '^2.11.0',
|
||||
compression: '1.6.2',
|
||||
cookies: '^0.7.0',
|
||||
cors: '^2.8.3',
|
||||
express: '4.15.3',
|
||||
global: '^4.3.2',
|
||||
handlebars: '4.0.5',
|
||||
'http-errors': '^1.4.0',
|
||||
'js-string-escape': '1.0.1',
|
||||
'js-yaml': '^3.6.0',
|
||||
jsonwebtoken: '^7.4.1',
|
||||
lockfile: '^1.0.1',
|
||||
lodash: '4.17.4',
|
||||
lunr: '^0.7.0',
|
||||
marked: '0.3.6',
|
||||
mime: '^1.3.6',
|
||||
minimatch: '^3.0.2',
|
||||
mkdirp: '^0.5.1',
|
||||
pkginfo: '^0.4.0',
|
||||
request: '^2.72.0',
|
||||
semver: '^5.1.0',
|
||||
'unix-crypt-td-js': '^1.0.0'
|
||||
};
|
||||
const wrapper = shallow(<Dependencies dependencies={dependencies} />);
|
||||
|
||||
expect(wrapper.find(`.${DEP_ITEM_CLASS}`)).toHaveLength(Object.keys(dependencies).length);
|
||||
expect(wrapper.html()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should load the package without dependencies', () => {
|
||||
const wrapper = shallow(<Dependencies />);
|
||||
|
||||
expect(wrapper.find(ModuleContentPlaceholder).props().text).toBe(NO_DEPENDENCIES);
|
||||
expect(wrapper.html()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should permit overriding title', () => {
|
||||
const wrapper = mount(<Dependencies title={"Package dependencies"} />);
|
||||
|
||||
expect(wrapper.find('h2').text()).toEqual('Package dependencies');
|
||||
expect(wrapper.html()).toMatchSnapshot();
|
||||
});
|
||||
});
|
@ -1,46 +0,0 @@
|
||||
/**
|
||||
* Infos component
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import Infos from '../../../../../src/webui/components/PackageSidebar/modules/Infos/index';
|
||||
|
||||
describe('<PackageSidebar /> : <Infos />', () => {
|
||||
test('should load the component without props', () => {
|
||||
const wrapper = shallow(<Infos />);
|
||||
expect(wrapper.html()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should load the Info component with props', () => {
|
||||
const props = {
|
||||
homepage: 'https://www.verdaccio.org',
|
||||
license: 'MIT',
|
||||
repository: 'https://github.com/verdaccio/verdaccio'
|
||||
}
|
||||
const wrapper = shallow(<Infos {...props} />);
|
||||
expect(wrapper.html()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should load the Info component with homepage only', () => {
|
||||
const props = {
|
||||
homepage: 'https://www.verdaccio.org'
|
||||
}
|
||||
const wrapper = shallow(<Infos {...props} />);
|
||||
expect(wrapper.html()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should load the Info component with license only', () => {
|
||||
const props = {
|
||||
license: 'MIT',
|
||||
}
|
||||
const wrapper = shallow(<Infos {...props} />);
|
||||
expect(wrapper.html()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should load the Info component with repository only', () => {
|
||||
const props = { repository: 'https://github.com/verdaccio/verdaccio' };
|
||||
const wrapper = shallow(<Infos {...props} />);
|
||||
expect(wrapper.html()).toMatchSnapshot();
|
||||
});
|
||||
});
|
@ -1,27 +0,0 @@
|
||||
/**
|
||||
* LastSync component
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import LastSync from '../../../../../src/webui/components/PackageSidebar/modules/LastSync/index';
|
||||
|
||||
describe('<PackageSidebar /> : <LastSync />', () => {
|
||||
test('should check the default props condition', () => {
|
||||
const wrapper = shallow(<LastSync />);
|
||||
expect(wrapper.html()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should load the LastSync component and match snapshot', () => {
|
||||
const props = {
|
||||
lastUpdated: '2017/12/14, 15:43:52',
|
||||
recentReleases: [
|
||||
{ time: '2017/12/14, 15:43:27', version: '2.7.1' },
|
||||
{ time: '2017/12/05, 23:25:06', version: '2.7.0' },
|
||||
{ time: '2017/11/08, 22:47:16', version: '2.6.6' }
|
||||
]
|
||||
};
|
||||
const wrapper = shallow(<LastSync {...props} />);
|
||||
expect(wrapper.html()).toMatchSnapshot();
|
||||
});
|
||||
});
|
@ -1,30 +0,0 @@
|
||||
/**
|
||||
* MaintainerInfo component
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import MaintainerInfo from '../../../../../src/webui/components/PackageSidebar/modules/Maintainers/MaintainerInfo/index';
|
||||
|
||||
console.error = jest.fn();
|
||||
|
||||
describe('<PackageSidebar /> : <Maintainers /> <MaintainerInfo />', () => {
|
||||
test('should throw error for required props', () => {
|
||||
shallow(<MaintainerInfo />);
|
||||
expect(console.error).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should load the component and match with snapshot', () => {
|
||||
const props = {
|
||||
title: 'test-title',
|
||||
name: 'test',
|
||||
avatar: 'http://xyz.com/profile.jpg'
|
||||
};
|
||||
const wrapper = shallow(<MaintainerInfo {...props} />);
|
||||
expect(wrapper.find('.maintainer').prop('title')).toEqual('test');
|
||||
expect(wrapper.find('img').prop('src')).toEqual(
|
||||
'http://xyz.com/profile.jpg'
|
||||
);
|
||||
expect(wrapper.html()).toMatchSnapshot();
|
||||
});
|
||||
});
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user