0
Fork 0
mirror of https://github.com/verdaccio/verdaccio.git synced 2024-12-30 22:34:10 -05:00

Add new web ui, replace the old one based on jQuery by React components and webpack.

This commit is contained in:
Juan Picado @jotadeveloper 2017-06-17 10:50:49 +02:00
parent a6d3745cd4
commit e49846bb1b
No known key found for this signature in database
GPG key ID: 18AC54485952D158
35 changed files with 1348 additions and 9895 deletions

20
.babelrc Normal file
View file

@ -0,0 +1,20 @@
{
"presets": [
"react",
["env", {
"targets": {
"browsers": "last 2 versions"
},
"loose": true,
"modules": false
}]
],
"env": {
"test": {
"plugins": [
"transform-es2015-modules-commonjs"
]
}
}
}

View file

@ -1,5 +1,4 @@
node_modules
lib/web/static
lib/web/ui/
coverage/
wiki/

View file

@ -9,13 +9,22 @@
# Created to work with eslint@0.18.0
#
extends: ["eslint:recommended", "google"]
plugins: ["react"]
extends: ["eslint:recommended", "google", "plugin:react/recommended"]
env:
node: true
browser: true
es6: true
parserOptions:
sourceType: "module"
ecmaVersion: 7
ecmaFeatures:
jsx: true
rules:
# useful to have in node.js,
# if you're sure you don't need to handle error, rename it to "_err"

7
.gitignore vendored
View file

@ -20,3 +20,10 @@ coverage/
jsconfig.json
.idea/
# React
bundle.js
bundle.js.map
__tests__
__snapshots__

View file

@ -4,4 +4,5 @@ coverage/
verdaccio-*.tgz
test-storage*
/.*
scripts/
wiki/

View file

@ -1,39 +0,0 @@
module.exports = function(grunt) {
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
browserify: {
dist: {
files: {
'lib/static/main.js': ['lib/ui/js/main.js'],
},
options: {
debug: true,
transform: ['browserify-handlebars'],
},
},
},
less: {
dist: {
files: {
'lib/static/main.css': ['lib/ui/css/main.less'],
},
options: {
sourceMap: false,
},
},
},
watch: {
files: ['lib/ui/**/*'],
tasks: ['default'],
},
});
grunt.loadNpmTasks('grunt-browserify');
grunt.loadNpmTasks('grunt-contrib-watch');
grunt.loadNpmTasks('grunt-contrib-less');
grunt.registerTask('default', [
'browserify',
'less',
]);
};

View file

@ -18,8 +18,11 @@ web:
#enable: true
title: Verdaccio
# logo: logo.png
# template: custom.hbs
# tagline: "Some <b>HTML</b> enabled tagline that sits between the actual \
#header and the list of packages. You can even add <a \
#href=\"https://github.com\">links</a>!"

BIN
lib/web/static/header.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

511
lib/web/static/styles.css Normal file
View file

@ -0,0 +1,511 @@
/*! normalize.css v5.0.0 | MIT License | github.com/necolas/normalize.css */
/**
* 1. Change the default font family in all browsers (opinionated).
* 2. Correct the line height in all browsers.
* 3. Prevent adjustments of font size after orientation changes in
* IE on Windows Phone and in iOS.
*/
/* Document
========================================================================== */
html {
font-family: sans-serif; /* 1 */
line-height: 1.15; /* 2 */
-ms-text-size-adjust: 100%; /* 3 */
-webkit-text-size-adjust: 100%; /* 3 */
}
/* Sections
========================================================================== */
/**
* Remove the margin in all browsers (opinionated).
*/
body {
margin: 0;
}
/**
* Add the correct display in IE 9-.
*/
article,
aside,
footer,
header,
nav,
section {
display: block;
}
/**
* Correct the font size and margin on `h1` elements within `section` and
* `article` contexts in Chrome, Firefox, and Safari.
*/
h1 {
font-size: 2em;
margin: 0.67em 0;
}
/* Grouping content
========================================================================== */
/**
* Add the correct display in IE 9-.
* 1. Add the correct display in IE.
*/
figcaption,
figure,
main { /* 1 */
display: block;
}
/**
* Add the correct margin in IE 8.
*/
figure {
margin: 1em 40px;
}
/**
* 1. Add the correct box sizing in Firefox.
* 2. Show the overflow in Edge and IE.
*/
hr {
box-sizing: content-box; /* 1 */
height: 0; /* 1 */
overflow: visible; /* 2 */
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
pre {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
}
/* Text-level semantics
========================================================================== */
/**
* 1. Remove the gray background on active links in IE 10.
* 2. Remove gaps in links underline in iOS 8+ and Safari 8+.
*/
a {
background-color: transparent; /* 1 */
-webkit-text-decoration-skip: objects; /* 2 */
}
/**
* Remove the outline on focused links when they are also active or hovered
* in all browsers (opinionated).
*/
a:active,
a:hover {
outline-width: 0;
}
/**
* 1. Remove the bottom border in Firefox 39-.
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
*/
abbr[title] {
border-bottom: none; /* 1 */
text-decoration: underline; /* 2 */
text-decoration: underline dotted; /* 2 */
}
/**
* Prevent the duplicate application of `bolder` by the next rule in Safari 6.
*/
b,
strong {
font-weight: inherit;
}
/**
* Add the correct font weight in Chrome, Edge, and Safari.
*/
b,
strong {
font-weight: bolder;
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
}
/**
* Add the correct font style in Android 4.3-.
*/
dfn {
font-style: italic;
}
/**
* Add the correct background and color in IE 9-.
*/
mark {
background-color: #ff0;
color: #000;
}
/**
* Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/**
* Prevent `sub` and `sup` elements from affecting the line height in
* all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/* Embedded content
========================================================================== */
/**
* Add the correct display in IE 9-.
*/
audio,
video {
display: inline-block;
}
/**
* Add the correct display in iOS 4-7.
*/
audio:not([controls]) {
display: none;
height: 0;
}
/**
* Remove the border on images inside links in IE 10-.
*/
img {
border-style: none;
}
/**
* Hide the overflow in IE.
*/
svg:not(:root) {
overflow: hidden;
}
/* Forms
========================================================================== */
/**
* 1. Change the font styles in all browsers (opinionated).
* 2. Remove the margin in Firefox and Safari.
*/
button,
input,
optgroup,
select,
textarea {
font-family: sans-serif; /* 1 */
font-size: 100%; /* 1 */
line-height: 1.15; /* 1 */
margin: 0; /* 2 */
}
/**
* Show the overflow in IE.
* 1. Show the overflow in Edge.
*/
button,
input { /* 1 */
overflow: visible;
}
/**
* Remove the inheritance of text transform in Edge, Firefox, and IE.
* 1. Remove the inheritance of text transform in Firefox.
*/
button,
select { /* 1 */
text-transform: none;
}
/**
* 1. Prevent a WebKit bug where (2) destroys native `audio` and `video`
* controls in Android 4.
* 2. Correct the inability to style clickable types in iOS and Safari.
*/
button,
html [type="button"], /* 1 */
[type="reset"],
[type="submit"] {
-webkit-appearance: button; /* 2 */
}
/**
* Remove the inner border and padding in Firefox.
*/
button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
border-style: none;
padding: 0;
}
/**
* Restore the focus styles unset by the previous rule.
*/
button:-moz-focusring,
[type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
[type="submit"]:-moz-focusring {
outline: 1px dotted ButtonText;
}
/**
* Change the border, margin, and padding in all browsers (opinionated).
*/
fieldset {
border: 1px solid #c0c0c0;
margin: 0 2px;
padding: 0.35em 0.625em 0.75em;
}
/**
* 1. Correct the text wrapping in Edge and IE.
* 2. Correct the color inheritance from `fieldset` elements in IE.
* 3. Remove the padding so developers are not caught out when they zero out
* `fieldset` elements in all browsers.
*/
legend {
box-sizing: border-box; /* 1 */
color: inherit; /* 2 */
display: table; /* 1 */
max-width: 100%; /* 1 */
padding: 0; /* 3 */
white-space: normal; /* 1 */
}
/**
* 1. Add the correct display in IE 9-.
* 2. Add the correct vertical alignment in Chrome, Firefox, and Opera.
*/
progress {
display: inline-block; /* 1 */
vertical-align: baseline; /* 2 */
}
/**
* Remove the default vertical scrollbar in IE.
*/
textarea {
overflow: auto;
}
/**
* 1. Add the correct box sizing in IE 10-.
* 2. Remove the padding in IE 10-.
*/
[type="checkbox"],
[type="radio"] {
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
}
/**
* Correct the cursor style of increment and decrement buttons in Chrome.
*/
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Correct the odd appearance in Chrome and Safari.
* 2. Correct the outline style in Safari.
*/
[type="search"] {
-webkit-appearance: textfield; /* 1 */
outline-offset: -2px; /* 2 */
}
/**
* Remove the inner padding and cancel buttons in Chrome and Safari on macOS.
*/
[type="search"]::-webkit-search-cancel-button,
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* 1. Correct the inability to style clickable types in iOS and Safari.
* 2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button; /* 1 */
font: inherit; /* 2 */
}
/* Interactive
========================================================================== */
/*
* Add the correct display in IE 9-.
* 1. Add the correct display in Edge, IE, and Firefox.
*/
details, /* 1 */
menu {
display: block;
}
/*
* Add the correct display in all browsers.
*/
summary {
display: list-item;
}
/* Scripting
========================================================================== */
/**
* Add the correct display in IE 9-.
*/
canvas {
display: inline-block;
}
/**
* Add the correct display in IE.
*/
template {
display: none;
}
/* Hidden
========================================================================== */
/**
* Add the correct display in IE 10-.
*/
[hidden] {
display: none;
}
body {
font-family: "Roboto","Helvetica","Arial",sans-serif;
}
.body {
margin: 0;
padding: 0;
}
.list-container {
padding-top: 20px;
}
.wrapper {
padding-right: 15px;
padding-left: 15px;
margin-right: auto;
margin-left: auto;
}
@media (min-width: 768px) {
.wrapper {
width: 750px;
}
}
@media (max-width: 992px) {
.npm-logo {
width: 100px;
float: left;
}
.packages-header {
border-bottom: none;
}
}
@media (max-width: 768px) {
.navbar {
border-radius: 4px;
}
.wrapper {
width: 755px;
}
.list-container {
padding-top: 20px;
}
}
/*# sourceMappingURL=styles.css.map*/

Binary file not shown.

View file

@ -1,8 +0,0 @@
env:
node: true
browser: true
globals:
jQuery: true

23
lib/web/ui/.eslintrc.yml Normal file
View file

@ -0,0 +1,23 @@
# vim: syntax=yaml
## rules for react components
extends: ["eslint:recommended", "google", "plugin:react/recommended", "plugin:flowtype/recommended", "prettier", "prettier/react"]
plugins: ["flowtype", "prettier"]
parser: babel-eslint
env:
node: true
browser: true
jest: true
rules:
# jsdoc is mandatory
require-jsdoc: 0
# jsx rules
react/no-danger-with-children: 0
react/no-string-refs: 0

View file

@ -0,0 +1,93 @@
import React from 'react';
import PropTypes from 'prop-types';
import _ from 'lodash';
import injectTapEventPlugin from 'react-tap-event-plugin';
import request from 'superagent';
import getMuiTheme from 'material-ui/styles/getMuiTheme';
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
import {green100, green500, green700} from 'material-ui/styles/colors';
import Header from './Header/Header';
import Search from './Search/Search';
import List from './List/List';
injectTapEventPlugin();
if (process.env.BROWSER) {
require('./browser.css');
}
const muiTheme = getMuiTheme({
palette: {
primary1Color: green500,
primary2Color: green700,
primary3Color: green100,
},
}, {
avatar: {
borderColor: null,
},
userAgent: false,
});
class App extends React.Component {
constructor(props) {
super();
this.state = {
packages: props.packages,
frontPackages: props.packages,
req: null,
};
this.updatePackages = this.updatePackages.bind(this);
}
updatePackages(keyword) {
if (keyword !== '') {
if (this.req) {
this.req.abort();
}
this.req = request.get(`/-/search/${keyword}`)
.end((err, res) => {
if(_.isNil(err) === false) {
this.setState({
packages: [],
});
} else {
this.setState({
packages: res.body,
});
}
});
} else {
if (this.req) {
this.req.abort();
}
this.setState({
packages: this.state.frontPackages,
});
}
}
render() {
return (
<MuiThemeProvider muiTheme={muiTheme}>
<main>
<Header baseUrl={this.props.baseUrl} username={this.props.username}/>
<div className="wrapper">
<Search updatePackages={this.updatePackages}/>
<List packages={this.state.packages}/>
</div>
</main>
</MuiThemeProvider>
);
}
}
App.propTypes = {
packages: PropTypes.array.isRequired,
baseUrl: PropTypes.string.isRequired,
username: PropTypes.string,
};
export default App;

View file

@ -0,0 +1,144 @@
import React from 'react';
import PropTypes from 'prop-types';
import RaisedButton from 'material-ui/RaisedButton';
import TextField from 'material-ui/TextField';
import IconPerson from 'material-ui/svg-icons/social/person';
import Lock from 'material-ui/svg-icons/action/lock';
import {List, ListItem} from 'material-ui/List';
import Dialog from 'material-ui/Dialog';
import {HeaderNav,
MenuGroup,
MenuItem,
Code,
CodeGroup,
LogoImage,
LogoItem, Navigation} from '../styled';
let logo = '/header.png';
if (process.env.BROWSER) {
logo = require('./header.png');
}
const styles = {
flex: {
display: 'flex',
},
red: {
backgroundColor: '#cc3d33',
},
fullWidth: {
width: '100%',
},
spaceItems: {
marginRight: 20,
},
};
class Header extends React.Component {
constructor() {
super();
this.state = {
open: false,
};
this.handleRequestClose = this.handleRequestClose.bind(this);
this.handleTouchTap = this.handleTouchTap.bind(this);
this.handleLogIn = this.handleLogIn.bind(this);
}
handleRequestClose() {
this.setState({
open: false,
});
}
handleTouchTap() {
this.setState({
open: true,
});
}
handleLogIn() {
this.refs.form.submit();
}
render() {
const standardActions = process.env.BROWSER ? [
<RaisedButton key="Close"
label="Close"
style={styles.spaceItems}
onTouchTap={this.handleRequestClose}
/>,
<RaisedButton
key="LogIn"
label="Log In"
primary={true}
backgroundColor={styles.red.backgroundColor}
onTouchTap={this.handleLogIn}
/>,
] : [];
return (
<HeaderNav className="navbar">
<Navigation className="wrapper">
<MenuGroup>
<LogoItem style={{'flex': '2 0 0'}}>
<LogoImage src={`/-/static${logo}`}/>
</LogoItem>
<MenuItem style={{'flex': '10 0 0'}}>
<CodeGroup>
<div>
<Code>
{ `npm set registry ${this.props.baseUrl}` }
</Code>
</div>
<div>
<Code>
{ `npm adduser --registry ${this.props.baseUrl}` }
</Code>
</div>
</CodeGroup>
</MenuItem>
<MenuItem style={{'flex': '1 0 0'}}>
<Dialog
open={this.state.open}
title="Welcome Back"
actions={standardActions}
onRequestClose={this.handleRequestClose}>
<form method="POST" ref="form" action="/-/login" autoComplete={false}>
<List>
<ListItem disabled leftIcon={<IconPerson />}>
<TextField
name="user"
hintText="Username"
fullWidth={true}
/>
</ListItem>
<ListItem disabled leftIcon={<Lock />}>
<TextField
name="pass"
hintText="Password"
fullWidth={true}
type="password"
/>
</ListItem>
</List>
</form>
</Dialog>
<RaisedButton
label="Login"
onTouchTap={this.handleTouchTap}
/>
</MenuItem>
</MenuGroup>
</Navigation>
</HeaderNav>
);
}
}
Header.propTypes = {
baseUrl: PropTypes.string.isRequired,
};
export default Header;

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View file

@ -0,0 +1,143 @@
import React from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import request from 'superagent';
import _ from 'lodash';
import ArrowRight from 'material-ui/svg-icons/hardware/keyboard-arrow-right';
import ArrowDown from 'material-ui/svg-icons/hardware/keyboard-arrow-down';
import Readme from '../Readme/Readme';
const ItemWrap = styled.li`
padding: 9px 10px;
border-bottom: 1px solid #E7E7E7;
list-style-type: none;
&:nth-child(even) {
background: #f3f3f3;
}
`;
const Description = styled.p`
margin: 0 0 0 18px;
font-size: 13px;
`;
const Group = styled.div`
display: flex;
align-items: center;
line-height: 10px;
`;
const GroupTitle = styled.div`
margin: 0 5px;
flex-basis: 85%;
`;
const Author = styled.div`
flex-basis: 15%;
text-align: center;
padding: 5px 0;
`;
const Small = styled.small`
color: #666;
`;
const Title = styled.h4`
margin: 0px;
margin-right: 10px;
`;
const Link = styled.a`
color: #cc3d33;
fill: currentColor;
text-decoration: none;
`;
class Item extends React.Component {
constructor() {
super();
this.state = {
open: false,
};
this.displayReadme = this.displayReadme.bind(this);
}
/**
*
* @param {*} event
*/
displayReadme(event) {
event.preventDefault();
if (!this.state.open) {
this.req = request
.get(this._buildUrl())
.set('Content-Type', 'text/html; charset=utf8')
.end((err, res) => {
if (_.isNil(err) === false) {
this.setState({
open: true,
readme: 'No readme available',
});
} else {
this.setState({
open: true,
readme: res.text,
});
}
});
} else {
this.setState({
open: false,
});
}
}
_buildUrl() {
return `/-/readme/${encodeURIComponent(this.props.pkg.name)}/${encodeURIComponent(this.props.pkg.version)}`;
}
render() {
const author = this.props.pkg.author ? this.props.pkg.author.name : '';
return (
<ItemWrap>
<div>
<Group>
<GroupTitle>
<Link href={'#'}>
<Group>
<span>
{ this.state.open ? <ArrowDown/> : <ArrowRight/> }
</span>
<Title onClick={this.displayReadme}>
{this.props.pkg.name}
</Title>
<Small>
{ `v${this.props.pkg.version}` }
</Small>
</Group>
</Link>
</GroupTitle>
<Author>
<Small>
{ `By: ${_.isNil(author) ? 'Not available' : author}` }
</Small>
</Author>
</Group>
</div>
<Description>
{this.props.pkg.description}
</Description>
{ this.state.open ? <Readme html={ this.state.readme }/> : '' }
</ItemWrap>
);
}
}
Item.propTypes = {
pkg: PropTypes.object.isRequired,
};
export default Item;

View file

@ -0,0 +1,29 @@
import React from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import Item from '../Item/Item';
const ListItems = styled.ul`
margin: 0px;
padding: 0px;
`;
class List extends React.Component {
render() {
return (
<ListItems className="list-container">
{ this.props.packages.map((item)=> {
return (<Item key={item.name} pkg={item}/>);
})}
</ListItems>
);
}
}
List.propTypes = {
packages: PropTypes.array.isRequired,
};
export default List;

View file

@ -0,0 +1,30 @@
import React from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
const ReadmeStyle = styled.div`
margin-top: 10px;
background: #ffffff;
padding: 10px 12px;
border-radius: 3px;
border: 1px solid #dadada;
color: #333;
overflow: hidden;
font-family: "Helvetica Neue", Helvetica, "Segoe UI", Arial, freesans, sans-serif;
font-size: 16px;
line-height: 1.6;
word-wrap: break-word;
`;
const Readme = (props) => {
return (<ReadmeStyle className="readme"
dangerouslySetInnerHTML={{__html: props.html}}>
</ReadmeStyle>
);
};
Readme.propTypes = {
html: PropTypes.string.isRequired,
};
export default Readme;

View file

@ -0,0 +1,57 @@
import React from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import TextField from 'material-ui/TextField';
const SearchContainer = styled.div`
margin-top: 20px;
`;
const styles = {
flex: {
display: 'flex',
},
fullWidth: {
width: '100%',
},
};
class Search extends React.Component {
constructor() {
super();
this.state = {
value: '',
};
this.handleChange = this.handleChange.bind(this);
}
handleChange(event) {
const keyword = event.target.value.trim();
this.setState({
value: keyword,
});
this.props.updatePackages(keyword);
}
render() {
return (
<SearchContainer className="search-box">
<TextField
style={styles.fullWidth}
hintText="Search for packages"
fullWidth={true}
value={this.state.value}
onChange={this.handleChange}
/>
</SearchContainer>
);
}
}
Search.propTypes = {
updatePackages: PropTypes.func.isRequired,
};
export default Search;

View file

@ -0,0 +1,50 @@
@import '../../../../node_modules/normalize.css/normalize.css';
body {
font-family: "Roboto","Helvetica","Arial",sans-serif;
}
.body {
margin: 0;
padding: 0;
}
.list-container {
padding-top: 20px;
}
.wrapper {
padding-right: 15px;
padding-left: 15px;
margin-right: auto;
margin-left: auto;
}
@media (min-width: 768px) {
.wrapper {
width: 750px;
}
}
@media (max-width: 992px) {
.npm-logo {
width: 100px;
float: left;
}
.packages-header {
border-bottom: none;
}
}
@media (max-width: 768px) {
.navbar {
border-radius: 4px;
}
.wrapper {
width: 755px;
}
.list-container {
padding-top: 20px;
}
}

View file

@ -0,0 +1,13 @@
import React from 'react';
class Detail extends React.Component {
render() {
return (
<header>
<h1>DETAIL</h1>
</header>
);
}
}
export default Detail;

View file

@ -0,0 +1,49 @@
import styled from 'styled-components';
const MenuItem = styled.li``;
const HeaderNav = styled.header`
background: #cc3d33;
margin: 0px;
`;
const Navigation = styled.nav`
margin-bottom: 0px;
border-radius: 0px;
position: relative;
min-height: 50px;
font-size: 14px;
border: none;
color: white;
`;
const MenuGroup = styled.ul`
display: flex;
margin: 0;
align-items: center;
padding: 0;
list-style-type: none;
`;
const LogoItem = styled(MenuItem)`
width: 100px;
height: 50px;
padding: 5px;
`;
const Code = styled.code`
background: none;
color: white;
`;
const LogoImage = styled.img`
padding-top: 5px;
width: 85%;
height: 85%;
`;
const CodeGroup = styled.div`
line-height: 1.5em;
`;
export { Code, LogoImage, CodeGroup, MenuGroup, Navigation, HeaderNav, MenuItem, LogoItem }

View file

@ -3,132 +3,15 @@
<head>
<meta charset="utf-8">
<title>{{ name }}</title>
<link rel="icon" type="image/png" href="{{ baseUrl }}/-/static/favicon.png"/>
<link rel="stylesheet" type="text/css" href="{{ baseUrl }}/-/static/main.css">
<link rel="stylesheet" type="text/css" href="{{ baseUrl }}/-/static/styles.css">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body class="body">
<header class="main-header">
<nav class="navbar navbar-default red-bg white no-border no-rnd-cnr" role="navigation">
<div class="container">
<div class="navbar-header clearfix">
<div class="npm-logo brand">
<a href="{{ baseUrl }}"></a>
</div>
<!-- login/logout for small devices -->
<div class="pull-right visible-xs pad-right-10">
<div>
{{#if username}}
<p class="white no-bg navbar-text pad-right-10 inline-block">Hi {{username}}</p>
<button type="submit" class="btn btn-danger inline-block js-userLogoutBtn">Logout</button>
{{else}}
<p class="white no-bg navbar-text pad-right-10 inline-block">&nbsp;</p>
<button type="submit" class="btn btn-danger inline-block" data-toggle="modal" data-target="#login-form" onclick="return false">Login</button>
{{/if}}
</div>
</div>
</div>
<div class="navbar-left hidden-xs">&nbsp;&nbsp;</div>
<div class="navbar-left setup hidden-xs">
<code class="white no-bg">npm set registry {{ baseUrl }}</code><br>
<code class="white no-bg">npm adduser --registry {{ baseUrl }}</code>
</div>
<!-- login/logout for large devices -->
<div class="navbar-collapse collapse">
<div class="navbar-right">
<form class="navbar-form navbar-right">
{{#if username}}
<p class="white no-bg pad-right-10 inline-block">Hi {{username}}</p>
<button type="submit" class="btn btn-danger inline-block js-userLogoutBtn">Logout</button>
{{else}}
<button type="submit" class="btn btn-danger inline-block" data-toggle="modal" data-target="#login-form" onclick="return false">Login</button>
{{/if}}
</form>
</div>
</div>
</div>
</nav>
<header class="sm-registry-info light-red-bg center hidden-sm hidden-lg hidden-md">
<code class="white no-bg">{{ baseUrl }}</code><br>
</header>
<header class="packages-header container">
{{#if tagline}}
<div class="row">
<div class="col-md-12">
{{{tagline}}}
</div>
</div>
{{/if}}
<div class="row">
<div class="col-md-5 hidden-xs hidden-sm">
<h2 class="title">Available Packages</h2>
</div>
<div class="col-md-4 col-md-offset-3 col-sm-12">
<form id='search-form'>
<div class="input-group input-group-lg search-container">
<input type="text" class="form-control" placeholder="Search for packages">
<span class="input-group-btn">
<button class="btn btn-default search-icon js-search-btn"><i class="icon-search"></i></button>
</span>
</div>
</form>
</div>
</div>
</header>
</header>
<section class="content container packages-container" id="all-packages">
{{#each packages}}
{{> entry}}
{{/each}}
{{#unless packages.length}}
<div class='no-results'>
<big>No Packages</big><br>
Use <code>npm publish</code>
</div>
{{/unless}}
</section>
<section class="content container pkg-search-container" id="search-results"></section>
<div class="modal fade" id="login-form" tabindex="-1" role="dialog" aria-labelledby="login-form-label" aria-hidden="true">
<div class="modal-dialog modal-sm">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">&times;</span><span class="sr-only">Close</span></button>
<h5 class="modal-title" id="login-form-label">Welcome back</h5>
</div>
<form role="form" action="{{ baseUrl }}/-/login" method="post" id="login-form" autocomplete="off">
<div class="modal-body">
<div class="form-group">
<label for="user" class="sr-only">Email</label>
<input name="user" id="user" class="form-control" placeholder="Username" type="text">
</div>
<div class="form-group">
<label for="pass" class="sr-only">Password</label>
<input name="pass" id="pass" class="form-control" placeholder="Password" type="password">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
<button type="submit" class="btn btn-primary">Log in</button>
</div>
</form>
</div>
</div>
</div>
<form action="{{ baseUrl }}/-/logout" method="post" class="hide" id="userLogoutForm"></form>
<script src="{{ baseUrl }}/-/static/jquery.min.js"></script>
<script type='text/javascript' src='{{ baseUrl }}/-/static/main.js'></script>
<div id="root"></div>
<script>
window.__INITIAL_STATE = JSON.parse('{{{data}}}');
</script>
<script type='text/javascript' src='{{ baseUrl }}/-/static/bundle.js'></script>
</body>
</html>

8
lib/web/ui/index.js Normal file
View file

@ -0,0 +1,8 @@
import React from 'react';
import ReactDOM from 'react-dom';
import App from './components/App.js';
import './style.css';
const state = window.__INITIAL_STATE;
ReactDOM.render(<App {...state} />, document.getElementById('root'));

View file

@ -1,278 +0,0 @@
/* ========================================================================
* Bootstrap: modal.js v3.3.0
* http://getbootstrap.com/javascript/#modals
* ========================================================================
* Copyright 2011-2014 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* ======================================================================== */
+function($) {
'use strict';
// MODAL CLASS DEFINITION
// ======================
let Modal = function(element, options) {
this.options = options;
this.$body = $(document.body);
this.$element = $(element);
this.$backdrop =
this.isShown = null;
this.scrollbarWidth = 0;
if (this.options.remote) {
this.$element
.find('.modal-content')
.load(this.options.remote, $.proxy(function() {
this.$element.trigger('loaded.bs.modal');
}, this));
}
};
Modal.VERSION = '3.3.0';
Modal.TRANSITION_DURATION = 300;
Modal.BACKDROP_TRANSITION_DURATION = 150;
Modal.DEFAULTS = {
backdrop: true,
keyboard: true,
show: true,
};
Modal.prototype.toggle = function(_relatedTarget) {
return this.isShown ? this.hide() : this.show(_relatedTarget);
};
Modal.prototype.show = function(_relatedTarget) {
let that = this;
let e = $.Event('show.bs.modal', {relatedTarget: _relatedTarget});
this.$element.trigger(e);
if (this.isShown || e.isDefaultPrevented()) return;
this.isShown = true;
this.checkScrollbar();
this.$body.addClass('modal-open');
this.setScrollbar();
this.escape();
this.$element.on('click.dismiss.bs.modal', '[data-dismiss="modal"]', $.proxy(this.hide, this));
this.backdrop(function() {
let transition = $.support.transition && that.$element.hasClass('fade');
if (!that.$element.parent().length) {
that.$element.appendTo(that.$body); // don't move modals dom position
}
that.$element
.show()
.scrollTop(0);
if (transition) {
that.$element[0].offsetWidth; // force reflow
}
that.$element
.addClass('in')
.attr('aria-hidden', false);
that.enforceFocus();
let e = $.Event('shown.bs.modal', {relatedTarget: _relatedTarget});
transition ?
that.$element.find('.modal-dialog') // wait for modal to slide in
.one('bsTransitionEnd', function() {
that.$element.trigger('focus').trigger(e);
})
.emulateTransitionEnd(Modal.TRANSITION_DURATION) :
that.$element.trigger('focus').trigger(e);
});
};
Modal.prototype.hide = function(e) {
if (e) e.preventDefault();
e = $.Event('hide.bs.modal');
this.$element.trigger(e);
if (!this.isShown || e.isDefaultPrevented()) return;
this.isShown = false;
this.escape();
$(document).off('focusin.bs.modal');
this.$element
.removeClass('in')
.attr('aria-hidden', true)
.off('click.dismiss.bs.modal');
$.support.transition && this.$element.hasClass('fade') ?
this.$element
.one('bsTransitionEnd', $.proxy(this.hideModal, this))
.emulateTransitionEnd(Modal.TRANSITION_DURATION) :
this.hideModal();
};
Modal.prototype.enforceFocus = function() {
$(document)
.off('focusin.bs.modal') // guard against infinite focus loop
.on('focusin.bs.modal', $.proxy(function(e) {
if (this.$element[0] !== e.target && !this.$element.has(e.target).length) {
this.$element.trigger('focus');
}
}, this));
};
Modal.prototype.escape = function() {
if (this.isShown && this.options.keyboard) {
this.$element.on('keydown.dismiss.bs.modal', $.proxy(function(e) {
e.which == 27 && this.hide();
}, this));
} else if (!this.isShown) {
this.$element.off('keydown.dismiss.bs.modal');
}
};
Modal.prototype.hideModal = function() {
let that = this;
this.$element.hide();
this.backdrop(function() {
that.$body.removeClass('modal-open');
that.resetScrollbar();
that.$element.trigger('hidden.bs.modal');
});
};
Modal.prototype.removeBackdrop = function() {
this.$backdrop && this.$backdrop.remove();
this.$backdrop = null;
};
Modal.prototype.backdrop = function(callback) {
let that = this;
let animate = this.$element.hasClass('fade') ? 'fade' : '';
if (this.isShown && this.options.backdrop) {
let doAnimate = $.support.transition && animate;
this.$backdrop = $('<div class="modal-backdrop ' + animate + '" />')
.prependTo(this.$element)
.on('click.dismiss.bs.modal', $.proxy(function(e) {
if (e.target !== e.currentTarget) return;
this.options.backdrop == 'static'
? this.$element[0].focus.call(this.$element[0])
: this.hide.call(this);
}, this));
if (doAnimate) this.$backdrop[0].offsetWidth; // force reflow
this.$backdrop.addClass('in');
if (!callback) return;
doAnimate ?
this.$backdrop
.one('bsTransitionEnd', callback)
.emulateTransitionEnd(Modal.BACKDROP_TRANSITION_DURATION) :
callback();
} else if (!this.isShown && this.$backdrop) {
this.$backdrop.removeClass('in');
let callbackRemove = function() {
that.removeBackdrop();
callback && callback();
};
$.support.transition && this.$element.hasClass('fade') ?
this.$backdrop
.one('bsTransitionEnd', callbackRemove)
.emulateTransitionEnd(Modal.BACKDROP_TRANSITION_DURATION) :
callbackRemove();
} else if (callback) {
callback();
}
};
Modal.prototype.checkScrollbar = function() {
this.scrollbarWidth = this.measureScrollbar();
};
Modal.prototype.setScrollbar = function() {
let bodyPad = parseInt((this.$body.css('padding-right') || 0), 10);
if (this.scrollbarWidth) this.$body.css('padding-right', bodyPad + this.scrollbarWidth);
};
Modal.prototype.resetScrollbar = function() {
this.$body.css('padding-right', '');
};
Modal.prototype.measureScrollbar = function() { // thx walsh
if (document.body.clientWidth >= window.innerWidth) return 0;
let scrollDiv = document.createElement('div');
scrollDiv.className = 'modal-scrollbar-measure';
this.$body.append(scrollDiv);
let scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth;
this.$body[0].removeChild(scrollDiv);
return scrollbarWidth;
};
// MODAL PLUGIN DEFINITION
// =======================
function Plugin(option, _relatedTarget) {
return this.each(function() {
let $this = $(this);
let data = $this.data('bs.modal');
let options = $.extend({}, Modal.DEFAULTS, $this.data(), typeof option == 'object' && option);
if (!data) $this.data('bs.modal', (data = new Modal(this, options)));
if (typeof option == 'string') data[option](_relatedTarget);
else if (options.show) data.show(_relatedTarget);
});
}
let old = $.fn.modal;
$.fn.modal = Plugin;
$.fn.modal.Constructor = Modal;
// MODAL NO CONFLICT
// =================
$.fn.modal.noConflict = function() {
$.fn.modal = old;
return this;
};
// MODAL DATA-API
// ==============
$(document).on('click.bs.modal.data-api', '[data-toggle="modal"]', function(e) {
let $this = $(this);
let href = $this.attr('href');
let $target = $($this.attr('data-target') || (href && href.replace(/.*(?=#[^\s]+$)/, ''))); // strip for ie7
let option = $target.data('bs.modal') ? 'toggle' : $.extend({remote: !/#/.test(href) && href}, $target.data(), $this.data());
if ($this.is('a')) e.preventDefault();
$target.one('show.bs.modal', function(showEvent) {
if (showEvent.isDefaultPrevented()) return; // only register focus restorer if modal will actually get shown
$target.one('hidden.bs.modal', function() {
$this.is(':visible') && $this.trigger('focus');
});
});
Plugin.call($target, option, this);
});
}(jQuery);

View file

@ -1,72 +0,0 @@
let $ = require('unopinionate').selector;
let onClick = require('onclick');
let transitionComplete = require('transition-complete');
$(function() {
onClick('.entry .name', function() {
let $this = $(this);
let $entry = $this.closest('.entry');
if ($entry.hasClass('open')) {
// Close entry
$entry
.height($entry.outerHeight())
.removeClass('open');
setTimeout(function() {
$entry.css('height', $entry.attr('data-height') + 'px');
}, 0);
transitionComplete(function() {
$entry.find('.readme').remove();
$entry.css('height', 'auto');
});
} else {
// Open entry
$('.entry.open').each(function() {
// Close open entries
let $entry = $(this);
$entry
.height($entry.outerHeight())
.removeClass('open');
setTimeout(function() {
$entry.css('height', $entry.attr('data-height') + 'px');
}, 0);
transitionComplete(function() {
$entry.find('.readme').remove();
$entry.css('height', 'auto');
});
});
// Add the open class
$entry.addClass('open');
// Explicitly set heights for transitions
let height = $entry.outerHeight();
$entry
.attr('data-height', height)
.css('height', height);
// Get the data
$.ajax({
url: '-/readme/'
+ encodeURIComponent($entry.attr('data-name')) + '/'
+ encodeURIComponent($entry.attr('data-version')),
dataType: 'text',
success: function(html) {
let $readme = $('<div class=\'readme\'>')
.html(html)
.appendTo($entry);
$entry.height(height + $readme.outerHeight());
transitionComplete(function() {
$entry.css('height', 'auto');
});
},
});
}
});
});

View file

@ -1,13 +0,0 @@
// twitter bootstrap stuff;
// not in static 'cause I want it to be bundled with the rest of javascripts
require('./bootstrap-modal');
// our own files
require('./search');
require('./entry');
let $ = require('unopinionate').selector;
$(document).on('click', '.js-userLogoutBtn', function() {
$('#userLogoutForm').submit();
return false;
});

View file

@ -1,77 +0,0 @@
let $ = require('unopinionate').selector;
let template = require('../entry.hbs');
$(function() {
;(function(window, document) {
var $form = $('#search-form')
var $input = $form.find('input')
var $searchResults = $('#search-results')
var $pkgListing = $('#all-packages')
var $searchBtn = $('.js-search-btn')
var request
var lastQuery = ''
var toggle = function(validQuery) {
$searchResults.toggleClass('show', validQuery)
$pkgListing.toggleClass('hide', validQuery)
$searchBtn.find('i').toggleClass('icon-cancel', validQuery)
$searchBtn.find('i').toggleClass('icon-search', !validQuery)
}
$form.bind('submit keyup', function(e) {
var query, isValidQuery
e.preventDefault();
query = $input.val()
isValidQuery = (query !== '')
toggle(isValidQuery)
if (!isValidQuery) {
if (request && typeof request.abort === 'function') {
request.abort();
}
$searchResults.html('')
return;
}
if (request && typeof request.abort === 'function') {
request.abort();
}
if (query !== lastQuery) {
lastQuery = query
$searchResults.html(
'<img class=\'search-ajax\' src=\'-/static/ajax.gif\' alt=\'Spinner\'/>');
}
request = $.getJSON('-/search/' + query, function( results ) {
if (results.length > 0) {
let html = '';
$.each(results, function(i, entry) {
html += template(entry);
});
$searchResults.html(html);
} else {
$searchResults.html(
'<div class=\'no-results\'><big>No Results</big></div>');
}
}).fail(function () {
$searchResults.html(
"<div class='no-results'><big>No Results</big></div>")
})
})
$(document).on('click', '.icon-cancel', function(e) {
e.preventDefault();
$input.val('');
$form.keyup();
});
})(window, window.document);
});

0
lib/web/ui/style.css Normal file
View file

View file

@ -23,64 +23,123 @@
"bunyan": "^1.8.0",
"chalk": "^1.1.3",
"commander": "^2.9.0",
"compression": "^1.6.1",
"compression": "1.6.2",
"cookies": "^0.6.1",
"express": "^4.13.4",
"cors": "2.8.3",
"express": "4.15.3",
"global": "^4.3.2",
"handlebars": "^4.0.5",
"highlight.js": "^9.3.0",
"http-errors": "^1.4.0",
"jju": "^1.3.0",
"js-string-escape": "1.0.1",
"js-yaml": "^3.6.0",
"lockfile": "^1.0.1",
"lodash": "^4.17.4",
"lunr": "^0.7.0",
"marked": "0.3.6",
"minimatch": "^3.0.2",
"mkdirp": "^0.5.1",
"pkginfo": "^0.4.0",
"render-readme": "^1.3.1",
"prop-types": "^15.5.10",
"request": "^2.72.0",
"semver": "^5.1.0",
"unix-crypt-td-js": "^1.0.0"
},
"devDependencies": {
"browserify": "^13.0.0",
"browserify-handlebars": "^1.0.0",
"codecov": "^2.2.0",
"eslint": "^3.19.0",
"eslint-config-google": "^0.7.1",
"grunt": "^1.0.1",
"grunt-browserify": "^5.0.0",
"grunt-cli": "^1.2.0",
"grunt-contrib-less": "^1.3.0",
"grunt-contrib-watch": "^1.0.0",
"mocha": "^3.2.0",
"mocha-lcov-reporter": "^1.3.0",
"nyc": "^10.1.2",
"onclick": "^0.1.0",
"rimraf": "^2.5.2",
"transition-complete": "^0.0.2",
"unopinionate": "^0.0.4"
"babel-cli": "6.22.2",
"babel-core": "6.22.1",
"babel-eslint": "7.2.3",
"babel-loader": "6.2.4",
"babel-plugin-dynamic-import-node": "1.0.2",
"babel-plugin-dynamic-import-webpack": "1.0.1",
"babel-plugin-syntax-dynamic-import": "6.18.0",
"babel-plugin-transform-class-properties": "6.24.1",
"babel-plugin-transform-decorators-legacy": "1.3.4",
"babel-plugin-transform-es2015-modules-commonjs": "6.24.1",
"babel-polyfill": "6.23.0",
"babel-preset-env": "1.5.1",
"babel-preset-es2015": "6.22.0",
"babel-preset-es2016": "6.22.0",
"babel-preset-es2017": "6.22.0",
"babel-preset-react": "6.24.1",
"babel-register": "6.24.1",
"codacy-coverage": "2.0.2",
"codecov": "2.2.0",
"coveralls": "2.13.0",
"css-loader": "0.23.1",
"enzyme": "2.8.2",
"eslint": "3.19.0",
"eslint-config-google": "0.7.1",
"eslint-config-prettier": "2.1.1",
"eslint-loader": "1.7.1",
"eslint-plugin-flow": "2.29.1",
"eslint-plugin-flowtype": "2.33.0",
"eslint-plugin-import": "2.3.0",
"eslint-plugin-jsx-a11y": "5.0.3",
"eslint-plugin-prettier": "2.1.1",
"eslint-plugin-react": "7.0.1",
"extract-text-webpack-plugin": "2.1.2",
"file-loader": "0.10.1",
"flow-bin": "0.47.0",
"in-publish": "2.0.0",
"jest": "20.0.4",
"jest-serializer-enzyme": "1.0.0",
"material-ui": "0.17.1",
"mocha": "3.2.0",
"mocha-lcov-reporter": "1.3.0",
"normalize.css": "5.0.0",
"nyc": "10.1.2",
"onclick": "0.1.0",
"prettier": "1.3.1",
"react": "15.6.0",
"react-dom": "15.6.0",
"react-hot-loader": "3.0.0-beta.6",
"react-router": "3.0.2",
"react-tap-event-plugin": "2.0.1",
"react-test-renderer": "15.5.4",
"rimraf": "2.5.2",
"sinon": "^2.3.4",
"style-loader": "0.13.1",
"styled-components": "1.4.6",
"styled-theme": "0.3.0",
"styled-tools": "0.1.2",
"superagent": "2.0.0",
"transition-complete": "0.0.2",
"unopinionate": "0.0.4",
"url-loader": "0.5.8",
"webpack": "2.6.1"
},
"keywords": [
"private",
"package",
"repository",
"registry",
"enterprise",
"modules",
"proxy",
"server"
],
"scripts": {
"test": "npm run lint && mocha ./test/functional ./test/unit",
"test": "npm run lint && mocha ./test/functional --reporter=spec --full-trace",
"test:coverage": "nyc mocha -R spec ./test/functional ./test/unit",
"test:ui": "NODE_ENV=test jest",
"test:ui:update": "NODE_ENV=test jest -u",
"coverage:html": "nyc report --reporter=html",
"coverage:codecov": "nyc report --reporter=lcov | codecov",
"test-travis": "npm run lint && npm run test:coverage",
"test-only": "mocha ./test/functional ./test/unit",
"lint": "eslint .",
"build-docker": "docker build -t verdaccio .",
"build:webpack": "webpack --config tools/webpack.config.js",
"prepublish": "in-publish && thing-I-dont-want-on-dev-install || not-in-publish",
"build-docker:rpi": "docker build -f Dockerfile.rpi -t verdaccio:rpi ."
},
"jest": {
"snapshotSerializers": [
"jest-serializer-enzyme"
]
},
"engines": {
"node": ">=4.6.1",
"npm": ">=2.15.9"

15
tools/.eslintrc.yml Normal file
View file

@ -0,0 +1,15 @@
# vim: syntax=yaml
## rules for react components
env:
node: true
browser: true
rules:
# jsdoc is mandatory
require-jsdoc: 0
comma-dangle: 0

56
tools/webpack.config.js Normal file
View file

@ -0,0 +1,56 @@
const path = require('path');
const webpack = require('webpack');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
module.exports = {
entry: './lib/web/ui/index.js',
devtool: 'source-map',
output: {
path: path.resolve(__dirname, '../lib/web/static'),
filename: 'bundle.js'
},
module: {
rules: [
{
enforce: 'pre',
test: /\.js$/,
exclude: /node_modules/,
loader: 'eslint-loader',
options: {
failOnError: true,
}
},
{
test: /\.(js)$/,
exclude: /node_modules/,
use: ['babel-loader']
},
{
test: /\.(jpe?g|png|gif|svg)$/i,
use: 'file-loader?name=/[name].[ext]'
},
{
test: /\.(ttf|eot|woff|woff2|svg)$/,
loader: 'url-loader?limit=50000&name=fonts/[hash].[ext]'
},
{
test: /\.css$/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: 'css-loader'
})
}
]
},
plugins: [
new ExtractTextPlugin('styles.css'),
new webpack.DefinePlugin({
'process.env': {
BROWSER: JSON.stringify(true)
}
})
],
resolve: {
extensions: ['.js', '.css']
}
};

BIN
yarn.lock

Binary file not shown.