diff --git a/flow-typed/npm/@material-ui/core/InputAdornment_vx.x.x.js b/flow-typed/npm/@material-ui/core/InputAdornment_vx.x.x.js new file mode 100644 index 000000000..4ea20a10c --- /dev/null +++ b/flow-typed/npm/@material-ui/core/InputAdornment_vx.x.x.js @@ -0,0 +1,38 @@ +// flow-typed signature: bc8a4aaaeb1e738c5006d06072a9b064 +// flow-typed version: <>/@material-ui/core/InputAdornment_v3.1.0/flow_v0.81.0 + +/** + * This is an autogenerated libdef stub for: + * + * '@material-ui/core/InputAdornment' + * + * Fill this stub out by replacing all the `any` types. + * + * Once filled out, we encourage you to share your work with the + * community by sending a pull request to: + * https://github.com/flowtype/flow-typed + */ + +declare module '@material-ui/core/InputAdornment' { + declare module.exports: any; +} + +/** + * We include stubs for each file inside this npm package in case you need to + * require those files directly. Feel free to delete any files that aren't + * needed. + */ +declare module '@material-ui/core/InputAdornment/InputAdornment' { + declare module.exports: any; +} + +// Filename aliases +declare module '@material-ui/core/InputAdornment/index' { + declare module.exports: $Exports<'@material-ui/core/InputAdornment'>; +} +declare module '@material-ui/core/InputAdornment/index.js' { + declare module.exports: $Exports<'@material-ui/core/InputAdornment'>; +} +declare module '@material-ui/core/InputAdornment/InputAdornment.js' { + declare module.exports: $Exports<'@material-ui/core/InputAdornment/InputAdornment'>; +} diff --git a/flow-typed/npm/@material-ui/core/MenuItem_vx.x.x.js b/flow-typed/npm/@material-ui/core/MenuItem_vx.x.x.js new file mode 100644 index 000000000..8deea954d --- /dev/null +++ b/flow-typed/npm/@material-ui/core/MenuItem_vx.x.x.js @@ -0,0 +1,38 @@ +// flow-typed signature: fd8dc668544eb744d5267a667187804b +// flow-typed version: <>/@material-ui/core/MenuItem_v3.1.0/flow_v0.81.0 + +/** + * This is an autogenerated libdef stub for: + * + * '@material-ui/core/MenuItem' + * + * Fill this stub out by replacing all the `any` types. + * + * Once filled out, we encourage you to share your work with the + * community by sending a pull request to: + * https://github.com/flowtype/flow-typed + */ + +declare module '@material-ui/core/MenuItem' { + declare module.exports: any; +} + +/** + * We include stubs for each file inside this npm package in case you need to + * require those files directly. Feel free to delete any files that aren't + * needed. + */ +declare module '@material-ui/core/MenuItem/MenuItem' { + declare module.exports: any; +} + +// Filename aliases +declare module '@material-ui/core/MenuItem/index' { + declare module.exports: $Exports<'@material-ui/core/MenuItem'>; +} +declare module '@material-ui/core/MenuItem/index.js' { + declare module.exports: $Exports<'@material-ui/core/MenuItem'>; +} +declare module '@material-ui/core/MenuItem/MenuItem.js' { + declare module.exports: $Exports<'@material-ui/core/MenuItem/MenuItem'>; +} diff --git a/flow-typed/npm/@material-ui/core/Paper_vx.x.x.js b/flow-typed/npm/@material-ui/core/Paper_vx.x.x.js new file mode 100644 index 000000000..956d54b96 --- /dev/null +++ b/flow-typed/npm/@material-ui/core/Paper_vx.x.x.js @@ -0,0 +1,38 @@ +// flow-typed signature: 1ac90635766a00f883f3d21d79c9f12e +// flow-typed version: <>/@material-ui/core/Paper_v3.1.0/flow_v0.81.0 + +/** + * This is an autogenerated libdef stub for: + * + * '@material-ui/core/Paper' + * + * Fill this stub out by replacing all the `any` types. + * + * Once filled out, we encourage you to share your work with the + * community by sending a pull request to: + * https://github.com/flowtype/flow-typed + */ + +declare module '@material-ui/core/Paper' { + declare module.exports: any; +} + +/** + * We include stubs for each file inside this npm package in case you need to + * require those files directly. Feel free to delete any files that aren't + * needed. + */ +declare module '@material-ui/core/Paper/Paper' { + declare module.exports: any; +} + +// Filename aliases +declare module '@material-ui/core/Paper/index' { + declare module.exports: $Exports<'@material-ui/core/Paper'>; +} +declare module '@material-ui/core/Paper/index.js' { + declare module.exports: $Exports<'@material-ui/core/Paper'>; +} +declare module '@material-ui/core/Paper/Paper.js' { + declare module.exports: $Exports<'@material-ui/core/Paper/Paper'>; +} diff --git a/flow-typed/npm/@material-ui/core/TextField_vx.x.x.js b/flow-typed/npm/@material-ui/core/TextField_vx.x.x.js new file mode 100644 index 000000000..71a2fa637 --- /dev/null +++ b/flow-typed/npm/@material-ui/core/TextField_vx.x.x.js @@ -0,0 +1,38 @@ +// flow-typed signature: 864619754dd206242d851f1d47ddb63f +// flow-typed version: <>/@material-ui/core/TextField_v3.0.1/flow_v0.81.0 + +/** + * This is an autogenerated libdef stub for: + * + * '@material-ui/core/TextField' + * + * Fill this stub out by replacing all the `any` types. + * + * Once filled out, we encourage you to share your work with the + * community by sending a pull request to: + * https://github.com/flowtype/flow-typed + */ + +declare module '@material-ui/core/TextField' { + declare module.exports: any; +} + +/** + * We include stubs for each file inside this npm package in case you need to + * require those files directly. Feel free to delete any files that aren't + * needed. + */ +declare module '@material-ui/core/TextField/TextField' { + declare module.exports: any; +} + +// Filename aliases +declare module '@material-ui/core/TextField/index' { + declare module.exports: $Exports<'@material-ui/core/TextField'>; +} +declare module '@material-ui/core/TextField/index.js' { + declare module.exports: $Exports<'@material-ui/core/TextField'>; +} +declare module '@material-ui/core/TextField/TextField.js' { + declare module.exports: $Exports<'@material-ui/core/TextField/TextField'>; +} diff --git a/flow-typed/npm/@material-ui/core_vx.x.x.js b/flow-typed/npm/@material-ui/core_vx.x.x.js index 2d3028ada..e5bc585bc 100644 --- a/flow-typed/npm/@material-ui/core_vx.x.x.js +++ b/flow-typed/npm/@material-ui/core_vx.x.x.js @@ -1,5 +1,5 @@ -// flow-typed signature: 55ef97a6a933779dac4db73eb9147735 -// flow-typed version: <>/@material-ui/core_v3.x.x/flow_v0.77.0 +// flow-typed signature: 412328dad707658ee9e7ff8346800ab2 +// flow-typed version: <>/@material-ui/core_v3.0.1/flow_v0.81.0 /** * This is an autogenerated libdef stub for: diff --git a/flow-typed/npm/@material-ui/icons_vx.x.x.js b/flow-typed/npm/@material-ui/icons_vx.x.x.js index 34874bc07..d92bd9c67 100644 --- a/flow-typed/npm/@material-ui/icons_vx.x.x.js +++ b/flow-typed/npm/@material-ui/icons_vx.x.x.js @@ -1,5 +1,5 @@ -// flow-typed signature: d51b6caa61b0a7c4c66d42853d664055 -// flow-typed version: <>/@material-ui/icons_v3.x.x/flow_v0.77.0 +// flow-typed signature: 59e1686415435e183c7c98de573c90d5 +// flow-typed version: <>/@material-ui/icons_v3.0.1/flow_v0.81.0 /** * This is an autogenerated libdef stub for: diff --git a/flow-typed/npm/autosuggest-highlight/match_vx.x.x.js b/flow-typed/npm/autosuggest-highlight/match_vx.x.x.js new file mode 100644 index 000000000..35be0bcf1 --- /dev/null +++ b/flow-typed/npm/autosuggest-highlight/match_vx.x.x.js @@ -0,0 +1,33 @@ +// flow-typed signature: f4ce515b9395f4f0279d388b18ef59b5 +// flow-typed version: <>/autosuggest-highlight/match_v3.1.1/flow_v0.81.0 + +/** + * This is an autogenerated libdef stub for: + * + * 'autosuggest-highlight/match' + * + * Fill this stub out by replacing all the `any` types. + * + * Once filled out, we encourage you to share your work with the + * community by sending a pull request to: + * https://github.com/flowtype/flow-typed + */ + +declare module 'autosuggest-highlight/match' { + declare module.exports: any; +} + +/** + * We include stubs for each file inside this npm package in case you need to + * require those files directly. Feel free to delete any files that aren't + * needed. + */ + + +// Filename aliases +declare module 'autosuggest-highlight/match/index' { + declare module.exports: $Exports<'autosuggest-highlight/match'>; +} +declare module 'autosuggest-highlight/match/index.js' { + declare module.exports: $Exports<'autosuggest-highlight/match'>; +} diff --git a/flow-typed/npm/autosuggest-highlight/parse_vx.x.x.js b/flow-typed/npm/autosuggest-highlight/parse_vx.x.x.js new file mode 100644 index 000000000..929f97c45 --- /dev/null +++ b/flow-typed/npm/autosuggest-highlight/parse_vx.x.x.js @@ -0,0 +1,33 @@ +// flow-typed signature: 7df3e3914baffd57187e87617a708990 +// flow-typed version: <>/autosuggest-highlight/parse_v3.1.1/flow_v0.81.0 + +/** + * This is an autogenerated libdef stub for: + * + * 'autosuggest-highlight/parse' + * + * Fill this stub out by replacing all the `any` types. + * + * Once filled out, we encourage you to share your work with the + * community by sending a pull request to: + * https://github.com/flowtype/flow-typed + */ + +declare module 'autosuggest-highlight/parse' { + declare module.exports: any; +} + +/** + * We include stubs for each file inside this npm package in case you need to + * require those files directly. Feel free to delete any files that aren't + * needed. + */ + + +// Filename aliases +declare module 'autosuggest-highlight/parse/index' { + declare module.exports: $Exports<'autosuggest-highlight/parse'>; +} +declare module 'autosuggest-highlight/parse/index.js' { + declare module.exports: $Exports<'autosuggest-highlight/parse'>; +} diff --git a/flow-typed/npm/react-autosuggest_vx.x.x.js b/flow-typed/npm/react-autosuggest_vx.x.x.js new file mode 100644 index 000000000..e0a65f71a --- /dev/null +++ b/flow-typed/npm/react-autosuggest_vx.x.x.js @@ -0,0 +1,60 @@ +// flow-typed signature: 844045a071365b8f4e9d7d1aac302959 +// flow-typed version: <>/react-autosuggest_v9.4.2/flow_v0.81.0 + +/** + * This is an autogenerated libdef stub for: + * + * 'react-autosuggest' + * + * Fill this stub out by replacing all the `any` types. + * + * Once filled out, we encourage you to share your work with the + * community by sending a pull request to: + * https://github.com/flowtype/flow-typed + */ + +declare module 'react-autosuggest' { + declare module.exports: any; +} + +/** + * We include stubs for each file inside this npm package in case you need to + * require those files directly. Feel free to delete any files that aren't + * needed. + */ +declare module 'react-autosuggest/dist/Autosuggest' { + declare module.exports: any; +} + +declare module 'react-autosuggest/dist/index' { + declare module.exports: any; +} + +declare module 'react-autosuggest/dist/standalone/autosuggest' { + declare module.exports: any; +} + +declare module 'react-autosuggest/dist/standalone/autosuggest.min' { + declare module.exports: any; +} + +declare module 'react-autosuggest/dist/theme' { + declare module.exports: any; +} + +// Filename aliases +declare module 'react-autosuggest/dist/Autosuggest.js' { + declare module.exports: $Exports<'react-autosuggest/dist/Autosuggest'>; +} +declare module 'react-autosuggest/dist/index.js' { + declare module.exports: $Exports<'react-autosuggest/dist/index'>; +} +declare module 'react-autosuggest/dist/standalone/autosuggest.js' { + declare module.exports: $Exports<'react-autosuggest/dist/standalone/autosuggest'>; +} +declare module 'react-autosuggest/dist/standalone/autosuggest.min.js' { + declare module.exports: $Exports<'react-autosuggest/dist/standalone/autosuggest.min'>; +} +declare module 'react-autosuggest/dist/theme.js' { + declare module.exports: $Exports<'react-autosuggest/dist/theme'>; +} diff --git a/flow-typed/npm/react-router_vx.x.x.js b/flow-typed/npm/react-router_vx.x.x.js new file mode 100644 index 000000000..5059e8cc6 --- /dev/null +++ b/flow-typed/npm/react-router_vx.x.x.js @@ -0,0 +1,199 @@ +// flow-typed signature: 4fb3dfe55b5d1711432e74df5fa80adc +// flow-typed version: <>/react-router_v4.3.1/flow_v0.81.0 + +/** + * This is an autogenerated libdef stub for: + * + * 'react-router' + * + * Fill this stub out by replacing all the `any` types. + * + * Once filled out, we encourage you to share your work with the + * community by sending a pull request to: + * https://github.com/flowtype/flow-typed + */ + +declare module 'react-router' { + declare module.exports: any; +} + +/** + * We include stubs for each file inside this npm package in case you need to + * require those files directly. Feel free to delete any files that aren't + * needed. + */ +declare module 'react-router/es/generatePath' { + declare module.exports: any; +} + +declare module 'react-router/es/index' { + declare module.exports: any; +} + +declare module 'react-router/es/matchPath' { + declare module.exports: any; +} + +declare module 'react-router/es/MemoryRouter' { + declare module.exports: any; +} + +declare module 'react-router/es/Prompt' { + declare module.exports: any; +} + +declare module 'react-router/es/Redirect' { + declare module.exports: any; +} + +declare module 'react-router/es/Route' { + declare module.exports: any; +} + +declare module 'react-router/es/Router' { + declare module.exports: any; +} + +declare module 'react-router/es/RouterContext' { + declare module.exports: any; +} + +declare module 'react-router/es/StaticRouter' { + declare module.exports: any; +} + +declare module 'react-router/es/Switch' { + declare module.exports: any; +} + +declare module 'react-router/es/withRouter' { + declare module.exports: any; +} + +declare module 'react-router/generatePath' { + declare module.exports: any; +} + +declare module 'react-router/matchPath' { + declare module.exports: any; +} + +declare module 'react-router/MemoryRouter' { + declare module.exports: any; +} + +declare module 'react-router/Prompt' { + declare module.exports: any; +} + +declare module 'react-router/Redirect' { + declare module.exports: any; +} + +declare module 'react-router/Route' { + declare module.exports: any; +} + +declare module 'react-router/Router' { + declare module.exports: any; +} + +declare module 'react-router/StaticRouter' { + declare module.exports: any; +} + +declare module 'react-router/Switch' { + declare module.exports: any; +} + +declare module 'react-router/umd/react-router' { + declare module.exports: any; +} + +declare module 'react-router/umd/react-router.min' { + declare module.exports: any; +} + +declare module 'react-router/withRouter' { + declare module.exports: any; +} + +// Filename aliases +declare module 'react-router/es/generatePath.js' { + declare module.exports: $Exports<'react-router/es/generatePath'>; +} +declare module 'react-router/es/index.js' { + declare module.exports: $Exports<'react-router/es/index'>; +} +declare module 'react-router/es/matchPath.js' { + declare module.exports: $Exports<'react-router/es/matchPath'>; +} +declare module 'react-router/es/MemoryRouter.js' { + declare module.exports: $Exports<'react-router/es/MemoryRouter'>; +} +declare module 'react-router/es/Prompt.js' { + declare module.exports: $Exports<'react-router/es/Prompt'>; +} +declare module 'react-router/es/Redirect.js' { + declare module.exports: $Exports<'react-router/es/Redirect'>; +} +declare module 'react-router/es/Route.js' { + declare module.exports: $Exports<'react-router/es/Route'>; +} +declare module 'react-router/es/Router.js' { + declare module.exports: $Exports<'react-router/es/Router'>; +} +declare module 'react-router/es/RouterContext.js' { + declare module.exports: $Exports<'react-router/es/RouterContext'>; +} +declare module 'react-router/es/StaticRouter.js' { + declare module.exports: $Exports<'react-router/es/StaticRouter'>; +} +declare module 'react-router/es/Switch.js' { + declare module.exports: $Exports<'react-router/es/Switch'>; +} +declare module 'react-router/es/withRouter.js' { + declare module.exports: $Exports<'react-router/es/withRouter'>; +} +declare module 'react-router/generatePath.js' { + declare module.exports: $Exports<'react-router/generatePath'>; +} +declare module 'react-router/index' { + declare module.exports: $Exports<'react-router'>; +} +declare module 'react-router/index.js' { + declare module.exports: $Exports<'react-router'>; +} +declare module 'react-router/matchPath.js' { + declare module.exports: $Exports<'react-router/matchPath'>; +} +declare module 'react-router/MemoryRouter.js' { + declare module.exports: $Exports<'react-router/MemoryRouter'>; +} +declare module 'react-router/Prompt.js' { + declare module.exports: $Exports<'react-router/Prompt'>; +} +declare module 'react-router/Redirect.js' { + declare module.exports: $Exports<'react-router/Redirect'>; +} +declare module 'react-router/Route.js' { + declare module.exports: $Exports<'react-router/Route'>; +} +declare module 'react-router/Router.js' { + declare module.exports: $Exports<'react-router/Router'>; +} +declare module 'react-router/StaticRouter.js' { + declare module.exports: $Exports<'react-router/StaticRouter'>; +} +declare module 'react-router/Switch.js' { + declare module.exports: $Exports<'react-router/Switch'>; +} +declare module 'react-router/umd/react-router.js' { + declare module.exports: $Exports<'react-router/umd/react-router'>; +} +declare module 'react-router/umd/react-router.min.js' { + declare module.exports: $Exports<'react-router/umd/react-router.min'>; +} +declare module 'react-router/withRouter.js' { + declare module.exports: $Exports<'react-router/withRouter'>; +} diff --git a/package.json b/package.json index 99fb06682..909680ad3 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@verdaccio/streams": "1.0.0", "JSONStream": "1.3.4", "async": "2.6.1", + "autosuggest-highlight": "3.1.1", "body-parser": "1.18.3", "bunyan": "1.8.12", "chalk": "2.4.1", @@ -45,6 +46,8 @@ "mkdirp": "0.5.1", "mv": "2.1.1", "pkginfo": "0.4.1", + "react-autosuggest": "9.4.2", + "react-router": "4.3.1", "request": "2.88.0", "semver": "5.5.1", "verdaccio-audit": "1.0.0", @@ -85,7 +88,6 @@ "codecov": "3.1.0", "cross-env": "5.2.0", "css-loader": "0.28.10", - "element-theme-default": "1.4.13", "emotion": "9.2.8", "enzyme": "3.6.0", "enzyme-adapter-react-16": "1.5.0", diff --git a/src/webui/app.js b/src/webui/app.js index 19b10e01b..1ccb16679 100644 --- a/src/webui/app.js +++ b/src/webui/app.js @@ -1,18 +1,28 @@ -import React, { Component } from 'react'; +import React, { Component, Fragment } from 'react'; import isNil from 'lodash/isNil'; -import 'element-theme-default'; +import Button from '@material-ui/core/Button'; +import Dialog from '@material-ui/core/Dialog'; +import DialogActions from '@material-ui/core/DialogActions'; +import DialogContent from '@material-ui/core/DialogContent'; +import DialogTitle from '@material-ui/core/DialogTitle'; +import SnackbarContent from '@material-ui/core/SnackbarContent'; +import ErrorIcon from '@material-ui/icons/Error'; import storage from './utils/storage'; import logo from './utils/logo'; import { makeLogin, isTokenExpire } from './utils/login'; -import Header from './components/Header'; 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 API from './utils/api'; +import { getDetailPageURL } from './utils/url'; import './styles/main.scss'; +import classes from "./app.scss"; import 'normalize.css'; export default class App extends Component { @@ -22,30 +32,46 @@ export default class App extends Component { user: {}, scope: (window.VERDACCIO_SCOPE) ? `${window.VERDACCIO_SCOPE}:` : '', showLoginModal: false, - isUserLoggedIn: false - }; - - constructor(props) { - super(props); - this.handleLogout = this.handleLogout.bind(this); - this.toggleLoginModal = this.toggleLoginModal.bind(this); - this.doLogin = this.doLogin.bind(this); - this.loadLogo = this.loadLogo.bind(this); - this.isUserAlreadyLoggedIn = this.isUserAlreadyLoggedIn.bind(this); + isUserLoggedIn: false, + packages: [], + searchPackages: [], + filteredPackages: [], + search: '', + isLoading: true, + showAlertDialog: false, + alertDialogContent: { + title: '', + message: '', + packages: [] + }, } componentDidMount() { this.loadLogo(); this.isUserAlreadyLoggedIn(); + this.loadPackages(); } - isUserAlreadyLoggedIn() { + // eslint-disable-next-line no-unused-vars + componentDidUpdate(_, prevState) { + if (prevState.isUserLoggedIn !== this.state.isUserLoggedIn) { + this.loadPackages(); + } + } + + loadLogo = async () => { + const logoUrl = await logo(); + this.setState({ + logoUrl + }); + } + + isUserAlreadyLoggedIn = () => { // checks for token validity const token = storage.getItem('token'); const username = storage.getItem('username'); - if (isTokenExpire(token) || isNil(username)) { - this.handleLogout(); + this.handleLogout(); } else { this.setState({ user: { username, token }, @@ -54,16 +80,38 @@ export default class App extends Component { } } - async loadLogo() { - const logoUrl = await logo(); - this.setState({ logoUrl }); + loadPackages = async () => { + try { + this.req = await API.request('packages', 'GET'); + const transformedPackages = this.req.map(({ name, ...others}) => ({ + label: name, + ...others + })); + this.setState({ + packages: transformedPackages, + filteredPackages: transformedPackages, + isLoading: false + }); + } catch (error) { + this.handleShowAlertDialog({ + title: 'Warning', + message: `Unable to load package list: ${error.message}` + }); + this.setLoading(false); + } } + setLoading = isLoading => ( + this.setState({ + isLoading + }) + ) + /** * Toggles the login modal * Required by:
*/ - toggleLoginModal() { + toggleLoginModal = () => { this.setState((prevState) => ({ showLoginModal: !prevState.showLoginModal, error: {} @@ -74,27 +122,16 @@ export default class App extends Component { * handles login * Required by:
*/ - async doLogin(usernameValue, passwordValue) { + doLogin = async (usernameValue, passwordValue) => { const { username, token, error } = await makeLogin( usernameValue, passwordValue ); if (username && token) { - this.setState({ - user: { - username, - token - } - }); + this.setLoggedUser(username, token); storage.setItem('username', username); storage.setItem('token', token); - // close login modal after successful login - // set isUserLoggedin to true - this.setState({ - isUserLoggedIn: true, - showLoginModal: false - }); } if (error) { @@ -105,11 +142,43 @@ export default class App extends Component { } } + setLoggedUser = (username, token) => { + this.setState({ + user: { + username, + token, + }, + isUserLoggedIn: true, // close login modal after successful login + showLoginModal: false // set isUserLoggedin to true + }); + } + + handleFetchPackages = async ({ value }) => { + try { + this.req = await API.request(`/search/${encodeURIComponent(value)}`, 'GET'); + const transformedPackages = this.req.map(({ name, ...others}) => ({ + label: name, + ...others + })); + // Implement cancel feature later + if (this.state.search === value) { + this.setState({ + searchPackages: transformedPackages + }); + } + } catch (error) { + this.handleShowAlertDialog({ + title: 'Warning', + message: `Unable to get search result: ${error.message}` + }); + } + } + /** * Logouts user * Required by:
*/ - handleLogout() { + handleLogout = () => { storage.removeItem('username'); storage.removeItem('token'); this.setState({ @@ -118,48 +187,159 @@ export default class App extends Component { }); } - renderHeader() { - const { - logoUrl, - user, - scope, - } = this.state; - return
; + handlePackagesClearRequested = () => { + this.setState({ + searchPackages: [] + }); + }; + + // eslint-disable-next-line no-unused-vars + handleSearch = (_, { newValue }) => { + const { filteredPackages, packages, search } = this.state; + const value = newValue.trim(); + this.setState({ + search: value, + filteredPackages: value.length < search.length ? + packages.filter(pkg => pkg.label.match(value)) : filteredPackages + }); + }; + + handleKeyDown = event => { + if (event.key === 'Enter') { + const { filteredPackages, packages } = this.state; + const value = event.target.value.trim(); + this.setState({ + filteredPackages: value ? + packages.filter(pkg => pkg.label.match(value)) : filteredPackages + }); + } } - renderLoginModal() { - const { - error, - showLoginModal - } = this.state; - return ; + // eslint-disable-next-line no-unused-vars + handleClickSearch = (_, { suggestionValue, method }) => { + const { packages } = this.state; + switch(method) { + case 'click': + window.location.href = getDetailPageURL(suggestionValue); + break; + case 'enter': + this.setState({ + filteredPackages: packages.filter(pkg => pkg.label.match(suggestionValue)) + }); + break; + } + } + + handleShowAlertDialog = content => { + this.setState({ + showAlertDialog: true, + alertDialogContent: content + }); + } + + handleDismissAlertDialog = () => { + this.setState({ + showAlertDialog: false + }); + }; + + getfilteredPackages = value => { + const inputValue = value.trim().toLowerCase(); + const inputLength = inputValue.length; + + if (inputLength === 0) { + return []; + } else { + return this.searchPackage(value); + } + } + + renderHeader = () => { + const { logoUrl, user, search, searchPackages } = this.state; + return ( +
+ ); + } + + renderAlertDialog = () => ( + + + {this.state.alertDialogContent.title} + + + + + + {this.state.alertDialogContent.message} + + + } + /> + + + + + + ) + + renderLoginModal = () => { + const { error, showLoginModal } = this.state; + return ( + + ); } render() { - const { isUserLoggedIn } = this.state; + const { isLoading, ...others } = this.state; return ( -
- + + {isLoading ? ( + + ) : ( + + {this.renderHeader()} + + + +
+ + )} + {this.renderAlertDialog()} {this.renderLoginModal()} - - -
+ ); } } diff --git a/src/webui/app.scss b/src/webui/app.scss new file mode 100644 index 000000000..462f8b9cc --- /dev/null +++ b/src/webui/app.scss @@ -0,0 +1,16 @@ +@import './styles/variables'; + +.alertError { + background-color: $red !important; + min-width: inherit !important; +} + +.alertErrorMsg { + display: flex; + align-items: center; +} + +.alertIcon { + opacity: 0.9; + margin-right: 8px; +} \ No newline at end of file diff --git a/src/webui/components/AutoComplete/index.js b/src/webui/components/AutoComplete/index.js new file mode 100644 index 000000000..558c9cfcd --- /dev/null +++ b/src/webui/components/AutoComplete/index.js @@ -0,0 +1,105 @@ +/** + * @prettier + * @flow + */ + +import React from 'react'; +import type { Node } from 'react'; +import Autosuggest from 'react-autosuggest'; +import match from 'autosuggest-highlight/match'; +import parse from 'autosuggest-highlight/parse'; +import Paper from '@material-ui/core/Paper'; +import MenuItem from '@material-ui/core/MenuItem'; + +import { fontWeight } from '../../utils/styles/sizes'; +import { Wrapper, InputField } from './styles'; +import { IProps } from './types'; + +const renderInputComponent = (inputProps): Node => { + const { ref, startAdornment, disableUnderline, onKeyDown, ...others } = inputProps; + return ( + { + ref(node); + }, + startAdornment, + disableUnderline, + onKeyDown, + }} + {...others} + /> + ); +}; + +const getSuggestionValue = (suggestion): string => suggestion.label; + +const renderSuggestion = (suggestion, { query, isHighlighted }): Node => { + const matches = match(suggestion.label, query); + const parts = parse(suggestion.label, matches); + return ( + +
+ {parts.map((part, index) => { + return part.highlight ? ( + + {part.text} + + ) : ( + + {part.text} + + ); + })} +
+
+ ); +}; + +const AutoComplete = ({ + suggestions, + startAdornment, + onChange, + onSuggestionsFetch, + onCleanSuggestions, + value = '', + placeholder = '', + disableUnderline = false, + color, + onClick, + onKeyDown, +}: IProps): Node => { + const autosuggestProps = { + renderInputComponent, + suggestions, + getSuggestionValue, + renderSuggestion, + onSuggestionsFetchRequested: onSuggestionsFetch, + onSuggestionsClearRequested: onCleanSuggestions, + }; + return ( + + ( + + {options.children} + + )} + onSuggestionSelected={onClick} + /> + + ); +}; + +export default AutoComplete; diff --git a/src/webui/components/AutoComplete/styles.js b/src/webui/components/AutoComplete/styles.js new file mode 100644 index 000000000..0137926e4 --- /dev/null +++ b/src/webui/components/AutoComplete/styles.js @@ -0,0 +1,52 @@ +/** + * @prettier + * @flow + */ + +import React from 'react'; +import styled, { css } from 'react-emotion'; + +import TxtField from '../TxtField'; +import { IInputField } from './types'; + +export const Wrapper = styled.div` + && { + width: 100%; + height: 32px; + position: relative; + z-index: 1; + } +`; + +export const InputField = ({ color, ...others }: IInputField) => ( + +); diff --git a/src/webui/components/AutoComplete/types.js b/src/webui/components/AutoComplete/types.js new file mode 100644 index 000000000..aba6c7c8a --- /dev/null +++ b/src/webui/components/AutoComplete/types.js @@ -0,0 +1,24 @@ +/** + * @prettier + * @flow + */ + +import { InputAdornmentProps } from '@material-ui/core/InputAdornment'; + +export interface IProps { + suggestions: any[]; + color?: string; + value?: string; + placeholder?: string; + startAdornment?: React.ComponentType; + disableUnderline?: boolean; + onChange?: (event: SyntheticKeyboardEvent) => void; + onSuggestionsFetch?: (event: SyntheticKeyboardEvent) => void; + onCleanSuggestions?: () => void; + onClick?: () => void; + onKeyDown?: (event: SyntheticKeyboardEvent) => void; +} + +export interface IInputField { + color: string; +} diff --git a/src/webui/components/CopyToClipBoard/index.js b/src/webui/components/CopyToClipBoard/index.js index b894ab11b..6ba1f742a 100644 --- a/src/webui/components/CopyToClipBoard/index.js +++ b/src/webui/components/CopyToClipBoard/index.js @@ -8,7 +8,7 @@ import FileCopy from '@material-ui/icons/FileCopy'; import Tooltip from '@material-ui/core/Tooltip/index'; import type { Node } from 'react'; -import { IProps } from './interfaces'; +import { IProps } from './types'; import { ClipBoardCopy, ClipBoardCopyText, CopyIcon } from './styles'; @@ -33,7 +33,7 @@ const CopyToClipBoard = ({ text }: IProps): Node => ( {text} - + diff --git a/src/webui/components/CopyToClipBoard/interfaces.js b/src/webui/components/CopyToClipBoard/types.js similarity index 100% rename from src/webui/components/CopyToClipBoard/interfaces.js rename to src/webui/components/CopyToClipBoard/types.js diff --git a/src/webui/components/Header/index.js b/src/webui/components/Header/index.js index f0c3728b9..3f29b1de2 100644 --- a/src/webui/components/Header/index.js +++ b/src/webui/components/Header/index.js @@ -12,17 +12,21 @@ import Info from '@material-ui/icons/Info'; import Help from '@material-ui/icons/Help'; import Tooltip from '@material-ui/core/Tooltip/index'; import AccountCircle from '@material-ui/icons/AccountCircle'; +import InputAdornment from '@material-ui/core/InputAdornment'; +import { default as IconSearch } from '@material-ui/icons/Search'; import { getRegistryURL } from '../../utils/url'; import Link from '../Link'; import Logo from '../Logo'; -import Label from '../Label'; import CopyToClipBoard from '../CopyToClipBoard/index'; import RegistryInfoDialog from '../RegistryInfoDialog'; +import AutoComplete from '../AutoComplete'; +import Label from '../Label'; import type { Node } from 'react'; -import { IProps, IState } from './interfaces'; -import { Wrapper, InnerWrapper, Greetings } from './styles'; +import { IProps, IState } from './types'; +import colors from '../../utils/styles/colors'; +import { Greetings, NavBar, InnerNavBar, MobileNavBar, InnerMobileNavBar, LeftSide, RightSide, Search, IconSearchButton } from './styles'; class Header extends Component { handleLoggedInMenu: Function; @@ -32,20 +36,27 @@ class Header extends Component { handleToggleLogin: Function; renderInfoDialog: Function; - constructor(props: Object) { + constructor(props: IProps) { super(props); - this.handleLoggedInMenu = this.handleLoggedInMenu.bind(this); - this.handleLoggedInMenuClose = this.handleLoggedInMenuClose.bind(this); - this.handleOpenRegistryInfoDialog = this.handleOpenRegistryInfoDialog.bind(this); - this.handleCloseRegistryInfoDialog = this.handleCloseRegistryInfoDialog.bind(this); - this.handleToggleLogin = this.handleToggleLogin.bind(this); - this.renderInfoDialog = this.renderInfoDialog.bind(this); + const { packages = [] } = props; this.state = { openInfoDialog: false, registryUrl: '', + packages, + showMobileNavBar: false, }; } + static getDerivedStateFromProps(nextProps: IProps, prevState: IState) { + if (nextProps.packages !== prevState.packages) { + return { + packages: nextProps.packages, + }; + } + + return null; + } + componentDidMount() { const registryUrl = getRegistryURL(); this.setState({ @@ -56,65 +67,104 @@ class Header extends Component { /** * opens popover menu for logged in user. */ - handleLoggedInMenu(event: SyntheticEvent) { + handleLoggedInMenu = (event: SyntheticEvent) => { this.setState({ anchorEl: event.currentTarget, }); - } + }; /** * closes popover menu for logged in user */ - handleLoggedInMenuClose() { + handleLoggedInMenuClose = () => { this.setState({ anchorEl: null, }); - } + }; /** * opens registry information dialog. */ - handleOpenRegistryInfoDialog() { + handleOpenRegistryInfoDialog = () => { this.setState({ openInfoDialog: true, }); - } + }; /** * closes registry information dialog. */ - handleCloseRegistryInfoDialog() { + handleCloseRegistryInfoDialog = () => { this.setState({ openInfoDialog: false, }); - } + }; /** * close/open popover menu for logged in users. */ - handleToggleLogin() { + handleToggleLogin = () => { this.setState( { anchorEl: null, }, this.props.toggleLoginModal ); - } + }; - renderLeftSide(): Node { - const { registryUrl } = this.state; + handleToggleMNav = () => { + this.setState({ + showMobileNavBar: !this.state.showMobileNavBar, + }); + }; + + handleDismissMNav = () => { + this.setState({ + showMobileNavBar: false, + }); + }; + + renderLeftSide = (): Node => { + const { packages } = this.state; + const { onSearch = () => {}, search = '', withoutSearch = false, ...others } = this.props; return ( - - - + + + + + {!withoutSearch && ( + + + + + } + {...others} + /> + + )} + ); - } + }; - renderRightSide(): Node { - const { username = '' } = this.props; + renderRightSide = (): Node => { + const { username = '', withoutSearch = false } = this.props; const installationLink = 'https://verdaccio.org/docs/en/installation'; return ( -
+ + {!withoutSearch && ( + + + + + + )} @@ -132,20 +182,20 @@ class Header extends Component { Login )} -
+ ); - } + }; /** * render popover menu */ - renderMenu(): Node { - const { handleLogout, username = '' } = this.props; + renderMenu = (): Node => { + const { onLogout, username = '' } = this.props; const { anchorEl } = this.state; const open = Boolean(anchorEl); return ( - + { {`Hi,`} ); - } + }; - renderInfoDialog(): Node { + renderInfoDialog = (): Node => { const { scope } = this.props; const { openInfoDialog, registryUrl } = this.state; return ( @@ -185,17 +235,32 @@ class Header extends Component { ); - } + }; render() { + const { packages, showMobileNavBar } = this.state; + const { onSearch = () => {}, search = '', withoutSearch = false, ...others } = this.props; return ( - - - {this.renderLeftSide()} - {this.renderRightSide()} - - {this.renderInfoDialog()} - +
+ + + {this.renderLeftSide()} + {this.renderRightSide()} + + {this.renderInfoDialog()} + + {showMobileNavBar && + !withoutSearch && ( + + + + + + + )} +
); } } diff --git a/src/webui/components/Header/interfaces.js b/src/webui/components/Header/interfaces.js deleted file mode 100644 index 386f93e12..000000000 --- a/src/webui/components/Header/interfaces.js +++ /dev/null @@ -1,17 +0,0 @@ -/** - * @prettier - * @flow - */ - -export interface IProps { - username?: string; - handleLogout: Function; - toggleLoginModal: Function; - scope: string; -} - -export interface IState { - anchorEl?: any; - openInfoDialog: boolean; - registryUrl: string; -} diff --git a/src/webui/components/Header/styles.js b/src/webui/components/Header/styles.js index de2d25559..94fa67f15 100644 --- a/src/webui/components/Header/styles.js +++ b/src/webui/components/Header/styles.js @@ -1,38 +1,102 @@ -/** - * @prettier - * @flow - */ - -import styled, { css } from 'react-emotion'; -import AppBar from '@material-ui/core/AppBar/index'; -import Toolbar from '@material-ui/core/Toolbar/index'; -import colors from '../../utils/styles/colors'; -import mq from '../../utils/styles/media'; - -export const Wrapper = styled(AppBar)` - && { - background-color: ${colors.primary}; - position: fixed; - } -`; - -export const InnerWrapper = styled(Toolbar)` - && { - justify-content: space-between; - align-items: center; - padding: 0 20px; - ${mq.medium(css` - min-width: 400px; - max-width: 800px; - width: 100%; - margin: auto; - `)}; - ${mq.large(css` - max-width: 1240px; - `)}; - } -`; - -export const Greetings = styled.span` - margin: 0 5px 0 0; -`; +/** + * @prettier + * @flow + */ + +import styled, { css } from 'react-emotion'; +import AppBar from '@material-ui/core/AppBar/index'; +import Toolbar from '@material-ui/core/Toolbar/index'; +import IconButton from '@material-ui/core/IconButton/index'; + +import colors from '../../utils/styles/colors'; +import mq from '../../utils/styles/media'; + +export const NavBar = styled(AppBar)` + && { + background-color: ${colors.primary}; + min-height: 60px; + display: flex; + justify-content: center; + } +`; + +export const InnerNavBar = styled(Toolbar)` + && { + justify-content: space-between; + align-items: center; + padding: 0 20px; + ${mq.medium(css` + min-width: 400px; + max-width: 800px; + width: 100%; + margin: auto; + `)}; + ${mq.large(css` + max-width: 1240px; + `)}; + } +`; + +export const Greetings = styled.span` + && { + margin: 0 5px 0 0; + } +`; + +export const RightSide = styled(Toolbar)` + && { + display: flex; + padding: 0; + } +`; + +export const LeftSide = styled(RightSide)` + && { + flex: 1; + } +`; + +export const MobileNavBar = styled.div` + && { + align-items: center; + display: flex; + border-bottom: 1px solid ${colors.greyLight}; + padding: 8px; + position: relative; + ${mq.medium(css` + display: none; + `)}; + } +`; + +export const InnerMobileNavBar = styled.div` + && { + border-radius: 4px; + background-color: ${colors.greyLight}; + color: ${colors.white}; + width: 100%; + padding: 0px 5px; + margin: 0 10px 0 0; + } +`; + +export const Search = styled.div` + && { + display: none; + max-width: 393px; + width: 100%; + display: none; + ${mq.medium(css` + display: flex; + `)}; + } +`; + +export const IconSearchButton = styled(IconButton)` + && { + display: block; + ${mq.medium(css` + display: none; + `)}; + } +`; diff --git a/src/webui/components/Header/types.js b/src/webui/components/Header/types.js new file mode 100644 index 000000000..3c53b34c6 --- /dev/null +++ b/src/webui/components/Header/types.js @@ -0,0 +1,23 @@ +/** + * @prettier + * @flow + */ + +export interface IProps { + username?: string; + onLogout?: Function; + toggleLoginModal: Function; + scope: string; + search?: string; + packages?: any[]; + withoutSearch?: boolean; + onSearch?: (event: SyntheticKeyboardEvent) => void; +} + +export interface IState { + anchorEl?: any; + openInfoDialog: boolean; + registryUrl: string; + packages: any[]; + showMobileNavBar: boolean; +} diff --git a/src/webui/components/Label/index.js b/src/webui/components/Label/index.js index 947a6cd35..11cbdf081 100644 --- a/src/webui/components/Label/index.js +++ b/src/webui/components/Label/index.js @@ -9,7 +9,7 @@ import { fontWeight } from '../../utils/styles/sizes'; import ellipsis from '../../utils/styles/ellipsis'; import type { Node } from 'react'; -import { IProps } from './interfaces'; +import { IProps } from './types'; const Wrapper = styled.span` font-weight: ${({ weight }) => fontWeight[weight]}; diff --git a/src/webui/components/Label/interfaces.js b/src/webui/components/Label/types.js similarity index 100% rename from src/webui/components/Label/interfaces.js rename to src/webui/components/Label/types.js diff --git a/src/webui/components/Layout/index.js b/src/webui/components/Layout/index.js new file mode 100644 index 000000000..05f69a7fd --- /dev/null +++ b/src/webui/components/Layout/index.js @@ -0,0 +1,29 @@ +/** + * @prettier + * @flow + */ + +import styled, { css } from 'react-emotion'; + +export const Content = styled.div` + && { + background-color: #fff; + flex: 1; + position: relative; + } +`; + +export const Container = styled.div` + && { + display: flex; + flex-direction: column; + min-height: 100vh; + overflow: hidden; + ${({ isLoading }) => + isLoading && + css` + ${Content} { + background-color: #f5f6f8; + } + `} +`; diff --git a/src/webui/components/Link/index.js b/src/webui/components/Link/index.js index 95a0d30ef..5a5e6e699 100644 --- a/src/webui/components/Link/index.js +++ b/src/webui/components/Link/index.js @@ -5,7 +5,7 @@ import React from 'react'; import type { Node } from 'react'; -import { IProps } from './interfaces'; +import { IProps } from './types'; const Link = ({ children, to = '#', blank = false, ...props }: IProps): Node => ( diff --git a/src/webui/components/Link/interfaces.js b/src/webui/components/Link/types.js similarity index 100% rename from src/webui/components/Link/interfaces.js rename to src/webui/components/Link/types.js diff --git a/src/webui/components/Loading/index.js b/src/webui/components/Loading/index.js new file mode 100644 index 000000000..fc770f78f --- /dev/null +++ b/src/webui/components/Loading/index.js @@ -0,0 +1,23 @@ +/** + * @prettier + * @flow + */ + +import React from 'react'; +import type { Node } from 'react'; + +import Logo from '../Logo'; +import Spinner from '../Spinner'; + +import { Wrapper, Badge } from './styles'; + +const Loading = (): Node => ( + + + + + + +); + +export default Loading; diff --git a/src/webui/components/Loading/styles.js b/src/webui/components/Loading/styles.js new file mode 100644 index 000000000..d482253ad --- /dev/null +++ b/src/webui/components/Loading/styles.js @@ -0,0 +1,24 @@ +/** + * @prettier + * @flow + */ + +import styled from 'react-emotion'; + +export const Wrapper = styled.div` + && { + transform: translate(-50%, -50%); + top: 50%; + left: 50%; + position: absolute; + } +`; + +export const Badge = styled.div` + && { + margin: 0 0 30px 0; + border-radius: 25px; + box-shadow: 0 10px 20px 0 rgba(69, 58, 100, 0.2); + background: #f7f8f6; + } +`; diff --git a/src/webui/components/Login/index.js b/src/webui/components/Login/index.js index 17457ddbc..72b87c314 100644 --- a/src/webui/components/Login/index.js +++ b/src/webui/components/Login/index.js @@ -103,7 +103,6 @@ export default class LoginModal extends Component { return type === 'error' && ( Login @@ -137,7 +135,6 @@ export default class LoginModal extends Component { {this.renderLoginError(error)} @@ -156,7 +153,6 @@ export default class LoginModal extends Component { + props.md && + css` + width: 90px; + height: 90px; + `}; + } `; export default Logo; diff --git a/src/webui/components/NotFound/404.scss b/src/webui/components/NotFound/404.scss index 145ef0f56..5ed544cc5 100644 --- a/src/webui/components/NotFound/404.scss +++ b/src/webui/components/NotFound/404.scss @@ -1,5 +1,4 @@ @import '../../styles/variables'; -@import '../../styles/mixins'; .notFound { width: 100%; @@ -7,9 +6,6 @@ .notFound { line-height: $line-height-xl; border: none; outline: none; - @include border-bottom-default($grey-light); - - &:focus { - @include border-bottom-default($grey); - } + flex-direction: column; + align-items: center; } diff --git a/src/webui/components/NotFound/index.js b/src/webui/components/NotFound/index.js index 4a5970356..8533ac7cb 100644 --- a/src/webui/components/NotFound/index.js +++ b/src/webui/components/NotFound/index.js @@ -6,7 +6,7 @@ import classes from './404.scss'; const NotFound = (props) => { return ( -
+

Error 404 - {props.pkg}


diff --git a/src/webui/components/Package/package.scss b/src/webui/components/Package/package.scss index 459aad24d..f50e0aa07 100644 --- a/src/webui/components/Package/package.scss +++ b/src/webui/components/Package/package.scss @@ -21,7 +21,6 @@ .package { .tags { margin: 0 0.5em 0.5em 0; - white-space: nowrap; font-size: $font-size-sm; :global { .el-tag { diff --git a/src/webui/components/PackageList/index.js b/src/webui/components/PackageList/index.js index fb24865c6..9782e50dc 100644 --- a/src/webui/components/PackageList/index.js +++ b/src/webui/components/PackageList/index.js @@ -1,6 +1,5 @@ -import React from 'react'; +import React, { Fragment } from 'react'; import PropTypes from 'prop-types'; -import isEmpty from 'lodash/isEmpty'; import Package from '../Package'; import Help from '../Help'; @@ -15,63 +14,49 @@ export default class PackageList extends React.Component { help: PropTypes.bool }; + renderPackges = () => { + const { packages } = this.props; + return ( + packages.length > 0 ? ( + +

Available Packages

+ {this.renderList()} + + ) : ( + + ) + ); + } + + renderList = () => { + const { packages } = this.props; + return ( +
    + {packages.map((pkg, i) => { + const { label: name, version, description, time, keywords } = pkg; + const author = formatAuthor(pkg.author); + const license = formatLicense(pkg.license); + return ( +
  • + +
  • + ); + })} +
+ ); + } + render() { + const { help } = this.props; return (
- {this.renderTitle()} - {this.isTherePackages() ? this.renderList() : this.renderOptions()} + {help ? : this.renderPackges()}
); } - - renderTitle() { - if (this.isTherePackages() === false) { - return; - } - - return

Available Packages

; - } - - renderList() { - return this.props.packages.map((pkg, i) => { - const {name, version, description, time, keywords} = pkg; - const author = formatAuthor(pkg.author); - const license = formatLicense(pkg.license); - return ( -
  • - -
  • - ); - }); - } - - renderOptions() { - if (this.isTherePackages() === false && this.props.help) { - return this.renderHelp(); - } else { - return this.renderNoItems(); - } - } - - renderNoItems() { - return ( - - ); - } - - renderHelp() { - if (this.props.help === false) { - return; - } - return ; - } - - isTherePackages() { - return isEmpty(this.props.packages) === false; - } } diff --git a/src/webui/components/PackageList/packageList.scss b/src/webui/components/PackageList/packageList.scss index fb6ffc6d4..fc45752c0 100644 --- a/src/webui/components/PackageList/packageList.scss +++ b/src/webui/components/PackageList/packageList.scss @@ -17,7 +17,6 @@ .pkgContainer { .listTitle { font-weight: $font-weight-regular; font-size: $font-size-xl; - margin-top: 30px; - margin-bottom: 0; + margin: 0; } } diff --git a/src/webui/components/RegistryInfoDialog/index.js b/src/webui/components/RegistryInfoDialog/index.js index b5aa389ed..820cfe377 100644 --- a/src/webui/components/RegistryInfoDialog/index.js +++ b/src/webui/components/RegistryInfoDialog/index.js @@ -11,7 +11,7 @@ import { Title, Content } from './styles'; import type { Node } from 'react'; -import { IProps } from './interfaces'; +import { IProps } from './types'; const RegistryInfoDialog = ({ open = false, children, onClose }: IProps): Node => ( diff --git a/src/webui/components/RegistryInfoDialog/interfaces.js b/src/webui/components/RegistryInfoDialog/types.js similarity index 100% rename from src/webui/components/RegistryInfoDialog/interfaces.js rename to src/webui/components/RegistryInfoDialog/types.js diff --git a/src/webui/components/Search/index.js b/src/webui/components/Search/index.js deleted file mode 100644 index 7d78af044..000000000 --- a/src/webui/components/Search/index.js +++ /dev/null @@ -1,35 +0,0 @@ - -import React from 'react'; -import PropTypes from 'prop-types'; - -import classes from './search.scss'; - -const noSubmit = (e) => { - e.preventDefault(); -}; - -const Search = (props) => { - return ( -
    - -
    - ); -}; - -Search.defaultProps = { - placeHolder: 'Type to search...' -}; - -Search.propTypes = { - handleSearchInput: PropTypes.func.isRequired, - placeHolder: PropTypes.string, -}; - -export default Search; diff --git a/src/webui/components/Search/search.scss b/src/webui/components/Search/search.scss deleted file mode 100644 index 34cb44112..000000000 --- a/src/webui/components/Search/search.scss +++ /dev/null @@ -1,5 +0,0 @@ -@import '../../styles/mixins'; - -.searchBox { - @include searchBox; -} diff --git a/src/webui/components/Spinner/index.js b/src/webui/components/Spinner/index.js index 33aee9ecd..d7ba7de34 100644 --- a/src/webui/components/Spinner/index.js +++ b/src/webui/components/Spinner/index.js @@ -6,10 +6,10 @@ import React from 'react'; import type { Node } from 'react'; -import { IProps } from './interfaces'; +import { IProps } from './types'; import { Wrapper, Circular } from './styles'; -const Spinner = ({ size = 50, centered = true }: IProps): Node => ( +const Spinner = ({ size = 50, centered = false }: IProps): Node => ( diff --git a/src/webui/components/Spinner/styles.js b/src/webui/components/Spinner/styles.js index 623c07d9d..0ca240001 100644 --- a/src/webui/components/Spinner/styles.js +++ b/src/webui/components/Spinner/styles.js @@ -1,15 +1,25 @@ -import styled from 'react-emotion'; -import CircularProgress from '@material-ui/core/CircularProgress'; +/** + * @prettier + * @flow + */ + +import styled, { css } from 'react-emotion'; +import CircularProgress from '@material-ui/core/CircularProgress/index'; import colors from '../../utils/styles/colors'; export const Wrapper = styled.div` && { - ${({ centered }) => centered && ` - flex: 1; - display: flex; - justify-content: center; - align-items: center; - `} + display: flex; + align-items: center; + justify-content: center; + ${props => + props.centered && + css` + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + `} `; export const Circular = styled(CircularProgress)` diff --git a/src/webui/components/Spinner/interfaces.js b/src/webui/components/Spinner/types.js similarity index 100% rename from src/webui/components/Spinner/interfaces.js rename to src/webui/components/Spinner/types.js diff --git a/src/webui/components/TxtField/index.js b/src/webui/components/TxtField/index.js new file mode 100644 index 000000000..0d985c2b5 --- /dev/null +++ b/src/webui/components/TxtField/index.js @@ -0,0 +1,19 @@ +/** + * @prettier + * @flow + */ + +import React from 'react'; +import TextField, { TextFieldProps } from '@material-ui/core/TextField'; + +const TxtField = ({ InputProps, classes, ...other }: TextFieldProps) => ( + +); + +export default TxtField; diff --git a/src/webui/modules/home/index.js b/src/webui/modules/home/index.js deleted file mode 100644 index 606339401..000000000 --- a/src/webui/modules/home/index.js +++ /dev/null @@ -1,193 +0,0 @@ -import React, { Component, Fragment } from 'react'; -import PropTypes from 'prop-types'; -import Button from '@material-ui/core/Button'; -import Dialog from '@material-ui/core/Dialog'; -import DialogActions from '@material-ui/core/DialogActions'; -import DialogContent from '@material-ui/core/DialogContent'; -import DialogTitle from '@material-ui/core/DialogTitle'; -import SnackbarContent from '@material-ui/core/SnackbarContent'; -import ErrorIcon from '@material-ui/icons/Error'; -import isEmpty from 'lodash/isEmpty'; -import debounce from 'lodash/debounce'; - -import API from '../../utils/api'; - -import PackageList from '../../components/PackageList'; -import Search from '../../components/Search'; -import Spinner from '../../components/Spinner'; - -import classes from "./home.scss"; - -class Home extends Component { - static propTypes = { - children: PropTypes.element, - isUserLoggedIn: PropTypes.bool - }; - - state = { - showAlertDialog: false, - alertDialogContent: { - title: '', - message: '' - }, - loading: true, - fistTime: true, - query: '' - }; - - constructor(props) { - super(props); - this.handleSearchInput = this.handleSearchInput.bind(this); - this.handleShowAlertDialog = this.handleShowAlertDialog.bind(this); - this.handleCloseAlertDialog = this.handleCloseAlertDialog.bind(this); - this.searchPackage = debounce(this.searchPackage, 800); - } - - componentDidMount() { - this.loadPackages(); - } - - componentDidUpdate(prevProps, prevState) { - if (prevState.query !== this.state.query) { - if (this.req && this.req.abort) this.req.abort(); - this.setState({ - loading: true - }); - - if (prevState.query !== '' && this.state.query === '') { - this.loadPackages(); - } else { - this.searchPackage(this.state.query); - } - } - - if (prevProps.isUserLoggedIn !== this.props.isUserLoggedIn) { - this.loadPackages(); - } - } - - async loadPackages() { - try { - this.req = await API.request('packages', 'GET'); - - if (this.state.query === '') { - this.setState({ - packages: this.req, - loading: false - }); - } - } catch (error) { - this.handleShowAlertDialog({ - title: 'Warning', - message: `Unable to load package list: ${error.error}` - }); - } - } - - async searchPackage(query) { - try { - this.req = await API.request(`/search/${query}`, 'GET'); - - // Implement cancel feature later - if (this.state.query === query) { - this.setState({ - packages: this.req, - fistTime: false, - loading: false - }); - } - } catch (err) { - this.handleShowAlertDialog({ - title: 'Warning', - message: 'Unable to get search result, please try again later.' - }); - } - } - - renderAlertDialog() { - return ( - - - {this.state.alertDialogContent.title} - - - - - - {this.state.alertDialogContent.message} - -
    - } - /> - - - - - - ); - } - - handleShowAlertDialog(content) { - this.setState({ - showAlertDialog: true, - alertDialogContent: content - }); - }; - - handleCloseAlertDialog() { - this.setState({ - showAlertDialog: false - }); - }; - - handleSearchInput(e) { - this.setState({ - query: e.target.value.trim() - }); - } - - isTherePackages() { - return isEmpty(this.state.packages); - } - - render() { - const { packages, loading } = this.state; - return ( - - {this.renderSearchBar()} - {loading ? ( - - ) : ( - - )} - {this.renderAlertDialog()} - - ); - } - - renderSearchBar() { - if (this.isTherePackages() && this.state.fistTime) { - return; - } - return ; - } -} - -export default Home; diff --git a/src/webui/modules/detail/detail.scss b/src/webui/pages/detail/detail.scss similarity index 95% rename from src/webui/modules/detail/detail.scss rename to src/webui/pages/detail/detail.scss index e2bf3e48d..849a7f6de 100644 --- a/src/webui/modules/detail/detail.scss +++ b/src/webui/pages/detail/detail.scss @@ -3,7 +3,6 @@ .twoColumn { @include container-size; - margin: 0 10px; display: flex; > div { diff --git a/src/webui/modules/detail/index.jsx b/src/webui/pages/detail/index.jsx similarity index 93% rename from src/webui/modules/detail/index.jsx rename to src/webui/pages/detail/index.jsx index 126bf4c63..80c9df8b9 100644 --- a/src/webui/modules/detail/index.jsx +++ b/src/webui/pages/detail/index.jsx @@ -67,12 +67,14 @@ export default class Detail extends Component { const { notFound, readMe } = this.state; if (notFound) { - return ; + return ( + + ); } else if (isEmpty(readMe)) { return ; } return ( -
    +
    diff --git a/src/webui/modules/home/home.scss b/src/webui/pages/home/home.scss similarity index 100% rename from src/webui/modules/home/home.scss rename to src/webui/pages/home/home.scss diff --git a/src/webui/pages/home/index.js b/src/webui/pages/home/index.js new file mode 100644 index 000000000..ec3dda5d4 --- /dev/null +++ b/src/webui/pages/home/index.js @@ -0,0 +1,47 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; + +import PackageList from '../../components/PackageList'; + +class Home extends Component { + static propTypes = { + children: PropTypes.element, + isUserLoggedIn: PropTypes.bool, + packages: PropTypes.array, + filteredPackages: PropTypes.array, + }; + + constructor(props) { + super(props); + this.state = { + fistTime: true, + packages: props.packages, + filteredPackages: props.filteredPackages + }; + } + + static getDerivedStateFromProps(nextProps, prevState) { + if (nextProps.packages !== prevState.packages) { + return { + packages: nextProps.packages, + }; + } + if (nextProps.filteredPackages !== prevState.filteredPackages) { + return { + filteredPackages: nextProps.filteredPackages, + }; + } + return null; + } + + render() { + const { filteredPackages, packages } = this.state; + return ( +
    + 0} packages={filteredPackages} /> +
    + ); + } +} + +export default Home; diff --git a/src/webui/router.js b/src/webui/router.js index f65c09d1a..2afec6458 100644 --- a/src/webui/router.js +++ b/src/webui/router.js @@ -4,40 +4,40 @@ import {HashRouter as Router, Route, Switch} from 'react-router-dom'; import {asyncComponent} from './utils/asyncComponent'; -const DetailPackage = asyncComponent(() => import('./modules/detail')); -const HomePage = asyncComponent(() => import('./modules/home')); +const DetailPackage = asyncComponent(() => import('./pages/detail')); +import HomePage from './pages/home'; class RouterApp extends Component { static propTypes = { isUserLoggedIn: PropTypes.bool }; + render() { - const {isUserLoggedIn} = this.props; return ( -
    } + path="/" + render={() => ( + + )} /> ( - + )} /> ( - + )} /> -
    ); } diff --git a/src/webui/styles/global.scss b/src/webui/styles/global.scss index 9cd1144a7..2b38a8eef 100644 --- a/src/webui/styles/global.scss +++ b/src/webui/styles/global.scss @@ -3,7 +3,7 @@ :global { .container { - margin-top: 94px; + margin-top: 20px; flex: 1; @include container-size; @@ -15,13 +15,13 @@ :global { .content { display: flex; - flex-direction: column; } .page-full-height { display: flex; flex-direction: column; min-height: 100vh; + overflow: hidden; } .el-button { @@ -40,4 +40,8 @@ :global { .el-dialog__headerbtn:hover .el-dialog__close { color: $eclipse; } + + .package-list-items { + width: 100%; + } } diff --git a/src/webui/utils/styles/sizes.js b/src/webui/utils/styles/sizes.js index cf5d17931..179bca054 100644 --- a/src/webui/utils/styles/sizes.js +++ b/src/webui/utils/styles/sizes.js @@ -16,8 +16,8 @@ export const lineHeight = { }; export const fontWeight = { - light: 400, + light: 300, regular: 400, - semiBold: 600, + semiBold: 500, bold: 700 }; diff --git a/test/unit/webui/app.spec.js b/test/unit/webui/app.spec.js index af0b4f6ff..778bca95e 100644 --- a/test/unit/webui/app.spec.js +++ b/test/unit/webui/app.spec.js @@ -27,8 +27,6 @@ jest.mock('../../../src/webui/utils/storage', () => { return new LocalStorageMock(); }); -jest.mock('element-theme-default', () => ({})); - jest.mock('../../../src/webui/utils/api', () => ({ request: require('./components/__mocks__/api').default.request })); @@ -66,15 +64,14 @@ describe('App', () => { expect(wrapper.state('user').username).toEqual('verdaccio'); }); - it('handleLogout - logouts the user and clear localstorage', () => { + it('handleLogout - logouts the user and clear localstorage', async () => { const { handleLogout } = wrapper.instance(); storage.setItem('username', 'verdaccio'); storage.setItem('token', 'xxxx.TOKEN.xxxx'); - handleLogout(); - expect(handleLogout()).toBeUndefined(); + await handleLogout(); expect(wrapper.state('user')).toEqual({}); - expect(wrapper.state('isLoggedIn')).toBeFalsy(); + expect(wrapper.state('isUserLoggedIn')).toBeFalsy(); }); it('doLogin - login the user successfully', async () => { @@ -84,11 +81,11 @@ describe('App', () => { username: 'sam', token: 'TEST_TOKEN' }; - expect(wrapper.state('user')).toEqual(result); expect(wrapper.state('isUserLoggedIn')).toBeTruthy(); expect(wrapper.state('showLoginModal')).toBeFalsy(); expect(storage.getItem('username')).toEqual('sam'); expect(storage.getItem('token')).toEqual('TEST_TOKEN'); + expect(wrapper.state('user')).toEqual(result); }); it('doLogin - authentication failure', async () => { diff --git a/test/unit/webui/components/__snapshots__/copyToClipBoard.spec.js.snap b/test/unit/webui/components/__snapshots__/copyToClipBoard.spec.js.snap index 8e178ff4f..3c4a31a33 100644 --- a/test/unit/webui/components/__snapshots__/copyToClipBoard.spec.js.snap +++ b/test/unit/webui/components/__snapshots__/copyToClipBoard.spec.js.snap @@ -1,3 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[` component render the component 1`] = `"

    copy text

    "`; +exports[` component render the component 1`] = `"

    copy text

    "`; diff --git a/test/unit/webui/components/__snapshots__/header.spec.js.snap b/test/unit/webui/components/__snapshots__/header.spec.js.snap index 2245772a6..ea9068dcf 100644 --- a/test/unit/webui/components/__snapshots__/header.spec.js.snap +++ b/test/unit/webui/components/__snapshots__/header.spec.js.snap @@ -1,5 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`
    component with logged in state should load the component in logged in state 1`] = `"
    "`; +exports[`
    component with logged in state should load the component in logged in state 1`] = `"
    "`; -exports[`
    component with logged out state should load the component in logged out state 1`] = `"
    "`; +exports[`
    component with logged out state should load the component in logged out state 1`] = `"
    "`; diff --git a/test/unit/webui/components/__snapshots__/login.spec.js.snap b/test/unit/webui/components/__snapshots__/login.spec.js.snap index 39c89ac96..78c0cbe96 100644 --- a/test/unit/webui/components/__snapshots__/login.spec.js.snap +++ b/test/unit/webui/components/__snapshots__/login.spec.js.snap @@ -1,5 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[` should load the component in default state 1`] = `"

    Login

    "`; +exports[` should load the component in default state 1`] = `"

    Login

    "`; -exports[` should load the component with props 1`] = `"

    Login

    Error Title
    Error Description
    "`; +exports[` should load the component with props 1`] = `"

    Login

    Error Title
    Error Description
    "`; diff --git a/test/unit/webui/components/__snapshots__/notfound.spec.js.snap b/test/unit/webui/components/__snapshots__/notfound.spec.js.snap index f84e66ceb..6d0ff4eaf 100644 --- a/test/unit/webui/components/__snapshots__/notfound.spec.js.snap +++ b/test/unit/webui/components/__snapshots__/notfound.spec.js.snap @@ -1,3 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[` component should set html from props 1`] = `"

    Error 404 - verdaccio


    Oops, The package you are trying to access does not exist.

    "`; +exports[` component should set html from props 1`] = `"

    Error 404 - verdaccio


    Oops, The package you are trying to access does not exist.

    "`; diff --git a/test/unit/webui/components/__snapshots__/packagelist.spec.js.snap b/test/unit/webui/components/__snapshots__/packagelist.spec.js.snap index ad1ee3c2f..2dad54aac 100644 --- a/test/unit/webui/components/__snapshots__/packagelist.spec.js.snap +++ b/test/unit/webui/components/__snapshots__/packagelist.spec.js.snap @@ -1,3 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[` component should load the component with packages 1`] = `""`; +exports[` component should load the component with packages 1`] = `""`; diff --git a/test/unit/webui/components/__snapshots__/search.spec.js.snap b/test/unit/webui/components/__snapshots__/search.spec.js.snap deleted file mode 100644 index bfa984b61..000000000 --- a/test/unit/webui/components/__snapshots__/search.spec.js.snap +++ /dev/null @@ -1,3 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` component should match the snapshot 1`] = `"
    "`; diff --git a/test/unit/webui/components/header.spec.js b/test/unit/webui/components/header.spec.js index d27369d79..4af6f1050 100644 --- a/test/unit/webui/components/header.spec.js +++ b/test/unit/webui/components/header.spec.js @@ -17,12 +17,18 @@ describe('
    component with logged in state', () => { handleLogout: jest.fn(), toggleLoginModal: jest.fn(), scope: 'test scope', + withoutSearch: true, }; wrapper = mount(
    ); }); test('should load the component in logged in state', () => { - const state = { openInfoDialog: false, registryUrl: 'http://localhost' }; + const state = { + openInfoDialog: false, + packages: undefined, + registryUrl: 'http://localhost', + showMobileNavBar: false, + }; expect(wrapper.state()).toEqual(state); expect(wrapper.html()).toMatchSnapshot(); }); @@ -53,12 +59,18 @@ describe('
    component with logged out state', () => { handleLogout: jest.fn(), toggleLoginModal: jest.fn(), scope: 'test scope', + withoutSearch: true, }; wrapper = mount(
    ); }); test('should load the component in logged out state', () => { - const state = { openInfoDialog: false, registryUrl: 'http://localhost' }; + const state = { + openInfoDialog: false, + packages: undefined, + registryUrl: 'http://localhost', + showMobileNavBar: false, + }; expect(wrapper.state()).toEqual(state); expect(wrapper.html()).toMatchSnapshot(); }); diff --git a/test/unit/webui/components/packagelist.spec.js b/test/unit/webui/components/packagelist.spec.js index bee0d07f9..a71d7fc66 100644 --- a/test/unit/webui/components/packagelist.spec.js +++ b/test/unit/webui/components/packagelist.spec.js @@ -5,8 +5,6 @@ import React from 'react'; import { mount } from 'enzyme'; import PackageList from '../../../../src/webui/components/PackageList/index'; -import Help from '../../../../src/webui/components/Help/index'; -import NoItems from '../../../../src/webui/components/NoItems/index'; import { BrowserRouter } from 'react-router-dom'; describe(' component', () => { @@ -18,12 +16,10 @@ describe(' component', () => { const wrapper = mount( ); + expect(wrapper.find('Help')).toHaveLength(1); + + expect(wrapper.find('h1').text()).toEqual('No Package Published Yet'); - const instance = wrapper.instance(); - expect(instance.isTherePackages()).toBeFalsy(); - expect(instance.renderHelp()).toBeTruthy(); - expect(instance.renderOptions()).toEqual(); - expect(instance.renderTitle()).toBeUndefined(); }); it('should load the component with packages', () => { @@ -52,23 +48,16 @@ describe(' component', () => { ], help: false }; + const wrapper = mount( ); - const instance = wrapper.find(PackageList).instance(); - expect(instance.isTherePackages()).toBeTruthy(); - expect(instance.renderHelp()).toBeUndefined(); - expect(instance.renderTitle().props.children).toEqual('Available Packages'); - expect(instance.renderNoItems()).toEqual( - - ); - expect(instance.renderOptions()).toEqual( - - ); + expect(wrapper.find('.listTitle').text()).toContain('Available Packages'); + // package count expect(wrapper.find('Package')).toHaveLength(3); // match snapshot diff --git a/test/unit/webui/components/search.spec.js b/test/unit/webui/components/search.spec.js deleted file mode 100644 index 643ea98f9..000000000 --- a/test/unit/webui/components/search.spec.js +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Search component - */ - -import React from 'react'; -import { shallow } from 'enzyme'; -import Search from '../../../../src/webui/components/Search/index'; -console.error = jest.fn(); - -describe(' component', () => { - it('should give error for the required fields', () => { - const wrapper = shallow(); - expect(console.error).toBeCalled(); - expect(wrapper.find('input').prop('placeholder')).toEqual( - 'Type to search...' - ); - }); - - it('should have element with correct properties', () => { - const props = { - handleSearchInput: () => {}, - placeHolder: 'Test placeholder' - }; - const wrapper = shallow(); - expect(wrapper.find('input')).toHaveLength(1); - expect(wrapper.find('input').prop('placeholder')).toEqual( - 'Test placeholder' - ); - }); - - it('should call the handleSearchInput function', () => { - const props = { - handleSearchInput: jest.fn() - }; - const wrapper = shallow(); - wrapper.find('input').simulate('change'); - expect(props.handleSearchInput).toBeCalled(); - }); - - it('should match the snapshot', () => { - const props = { handleSearchInput: () => {} }; - const wrapper = shallow(); - expect(wrapper.html()).toMatchSnapshot(); - }); -}); diff --git a/test/unit/webui/modules/home.spec.js b/test/unit/webui/modules/home.spec.js deleted file mode 100644 index 2f28133bc..000000000 --- a/test/unit/webui/modules/home.spec.js +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Home Component - */ - -import React from 'react'; -import { mount } from 'enzyme'; -import Home from '../../../../src/webui/modules/home/index'; - -describe(' Component', () => { - let wrapper; - - beforeEach(() => { - wrapper = mount(); - }); - - it('handleSearchInput - should match the search query', () => { - const { handleSearchInput } = wrapper.instance(); - const result = 'test query string one'; - const input = { - target: { - value: result - } - }; - handleSearchInput(input); - expect(wrapper.state('query')).toBe(result); - }); - - it('handleSearchInput - should match the trimmed search query', () => { - const { handleSearchInput } = wrapper.instance(); - const result = ' '; - const input = { - target: { - value: result - } - }; - handleSearchInput(input); - expect(wrapper.state('query')).toBe(result.trim()); - }); -}); diff --git a/yarn.lock b/yarn.lock index ca325abd3..3577085e9 100644 Binary files a/yarn.lock and b/yarn.lock differ