add Initial Account DB and REST API

This commit is contained in:
Sukchan Lee 2017-09-22 09:57:52 +09:00
parent c2de3c5596
commit ec3530524a
15 changed files with 816 additions and 9 deletions

View file

@ -16,4 +16,11 @@ restify.serve(router, Profile, {
version: ''
});
const Account = require('../models/account');
restify.serve(router, Account, {
prefix: '',
version: '',
idProperty: 'username'
});
module.exports = router;

View file

@ -0,0 +1,126 @@
import { Component } from 'react';
import PropTypes from 'prop-types';
import withWidth, { SMALL } from 'helpers/with-width';
import { Form } from 'components';
const schema = {
"title": "",
"type": "object",
"properties": {
"username": {
"type": "string",
"title": "Username",
"required": true,
},
"roles": {
"type" : "array",
"title" : "",
"items": {
"type": "string",
"title": "Role",
"enum": [ "user", "admin" ],
}
},
}
};
const uiSchema = {
"username" : {
classNames: "col-xs-12",
},
"roles" : {
"ui:options": {
"addable": false,
"orderable": false,
"removable": false
}
}
}
class Edit extends Component {
static propTypes = {
visible: PropTypes.bool,
action: PropTypes.string,
formData: PropTypes.object,
isLoading: PropTypes.bool,
validate: PropTypes.func,
onHide: PropTypes.func,
onSubmit: PropTypes.func,
onError: PropTypes.func
}
constructor(props) {
super(props);
this.state = this.getStateFromProps(props);
}
componentWillReceiveProps(nextProps) {
this.setState(this.getStateFromProps(nextProps));
}
getStateFromProps(props) {
const {
action,
width,
formData,
} = props;
let state = {
schema,
uiSchema,
formData,
};
if (action === 'update') {
state.uiSchema = Object.assign(state.uiSchema, {
"username": {
"ui:disabled": true
}
});
} else if (width !== SMALL) {
state.uiSchema = Object.assign(state.uiSchema, {
"username": {
"ui:autofocus": true
}
});
}
return state;
}
render() {
const {
visible,
action,
isLoading,
validate,
onHide,
onSubmit,
onError
} = this.props;
const {
formData
} = this.state;
return (
<Form
visible={visible}
title={(action === 'update') ? 'Edit Account' : 'Create Account'}
width="480px"
height="240px"
schema={this.state.schema}
uiSchema={this.state.uiSchema}
formData={formData}
isLoading={isLoading}
validate={validate}
onHide={onHide}
onSubmit={onSubmit}
onError={onError}/>
)
}
}
export default withWidth()(Edit);

View file

@ -0,0 +1,180 @@
import { Component } from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import oc from 'open-color';
import { media } from 'helpers/style-utils';
import EditIcon from 'react-icons/lib/md/edit';
import DeleteIcon from 'react-icons/lib/md/delete';
import { Tooltip, Spinner } from 'components';
const Card = styled.div`
position: relative;
display: flex;
padding: 0.5rem;
cursor: pointer;
${p => p.disabled && 'opacity: 0.5; cursor: not-allowed; pointer-events: none;'};
.actions {
position: absolute;
top: 0;
right: 0;
width: 8rem;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
}
&:hover {
background: ${oc.gray[1]};
.actions {
${p => p.disabled ? 'opacity: 0;' : 'opacity: 1;'};
}
}
`;
const CircleButton = styled.div`
height: 2rem;
width: 2rem;
display: flex;
align-items: center;
justify-content: center;
margin: 1px;
color: ${oc.gray[6]};
border-radius: 1rem;
font-size: 1.5rem;
&:hover {
color: ${oc.indigo[6]};
}
&.delete {
&:hover {
color: ${oc.pink[6]};
}
}
`
const Account = styled.div`
display: flex;
flex-direction: row;
flex:1;
line-height: 2.5rem;
margin : 0 2rem;
.username {
font-size: 1.25rem;
color: ${oc.gray[8]};
width: 320px;
}
.role {
font-size: 1.1rem;
color: ${oc.gray[6]};
width: 240px;
}
`;
const SpinnerWrapper = styled.div`
position: absolute;
top: 0;
right: 0;
width: 4rem;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
`
const propTypes = {
account: PropTypes.shape({
username: PropTypes.string
}),
onEdit: PropTypes.func,
onDelete: PropTypes.func
}
class Item extends Component {
static propTypes = {
account: PropTypes.shape({
username: PropTypes.string
}),
onEdit: PropTypes.func,
onDelete: PropTypes.func
}
handleEdit = e => {
e.stopPropagation();
const {
account,
onEdit,
} = this.props;
const {
username
} = account;
onEdit(username)
}
handleDelete = e => {
e.stopPropagation();
const {
account,
onDelete
} = this.props;
const {
username
} = account;
onDelete(username)
}
render() {
const {
handleEdit,
handleDelete
} = this;
const {
disabled,
account,
onEdit,
onDelete
} = this.props;
const {
username,
roles
} = account;
return (
<Card disabled={disabled} onClick={handleEdit}>
<Account>
<div className='username'>{username}</div>
<div className='role'>{roles[0]}</div>
</Account>
<div className="actions">
<Tooltip content='Delete' width="60px">
<CircleButton className="delete" onClick={handleDelete}><DeleteIcon/></CircleButton>
</Tooltip>
</div>
{disabled && <SpinnerWrapper><Spinner sm/></SpinnerWrapper>}
</Card>
)
}
}
export default Item;

View file

@ -0,0 +1,47 @@
import PropTypes from 'prop-types';
import styled from 'styled-components';
import oc from 'open-color';
import { media } from 'helpers/style-utils';
import { Layout, Blank } from 'components';
import Item from './Item';
const Wrapper = styled.div`
display: block;
margin: 2rem;
${media.mobile`
margin: 0.5rem 0.25rem;
`}
`
const propTypes = {
accounts: PropTypes.arrayOf(PropTypes.object),
onEdit: PropTypes.func,
onDelete: PropTypes.func,
search: PropTypes.string
}
const List = ({ accounts, deletedId, onEdit, onDelete, search }) => {
const accountList = accounts
.filter(s => s.username.indexOf(search) !== -1)
.map(account =>
<Item
key={account.username}
account={account}
disabled={deletedId === account.username}
onEdit={onEdit}
onDelete={onDelete} />
);
return (
<Wrapper>
{accountList}
</Wrapper>
)
}
List.propTypes = propTypes;
export default List;

View file

@ -0,0 +1,7 @@
import List from './List';
import Edit from './Edit';
export {
List,
Edit
};

View file

@ -88,6 +88,10 @@ const Sidebar = ({ isOpen, width, selectedView, onSelectView }) => (
<Icon><ProfileIcon/></Icon>
<Title>Profile</Title>
</Item>
<Item name="account" selected={selectedView} onSelect={onSelectView}>
<Icon><ProfileIcon/></Icon>
<Title>Account</Title>
</Item>
</Menu>
)

View file

@ -15,7 +15,6 @@ const Card = styled.div`
display: flex;
padding : 0.5rem;
background: white;
cursor: pointer;
${p => p.disabled && 'opacity: 0.5; cursor: not-allowed; pointer-events: none;'}

View file

@ -2,8 +2,7 @@ import PropTypes from 'prop-types';
import styled from 'styled-components';
import oc from 'open-color';
import { media, transitions } from 'helpers/style-utils';
import CSSTransitionGroup from 'react-transition-group/CSSTransitionGroup';
import { media } from 'helpers/style-utils';
import { Layout, Blank } from 'components';
import Item from './Item';

View file

@ -16,7 +16,7 @@ const Wrapper = styled.div`
display: flex;
flex-direction: column;
postion: relative;
width: 1050px;
width: ${p => p.width || `1050px`};
${media.mobile`
width: calc(100vw - 2rem);
@ -39,7 +39,7 @@ const Body = styled.div`
padding: 2rem;
font-size: 14px;
height: 500px;
height: ${p => p.height || `500px`};
${media.mobile`
height: calc(100vh - 16rem);
`}
@ -242,11 +242,11 @@ class Form extends Component {
visible={visible}
onOutside={handleOutside}
disableOnClickOutside={this.state.confirm}>
<Wrapper id='nprogress-base-form'>
<Wrapper id='nprogress-base-form' width={this.props.width}>
<Header>
{title}
</Header>
<Body>
<Body height={this.props.height}>
{isLoading && <Spinner/>}
{!isLoading &&
<JsonSchemaForm

View file

@ -18,6 +18,7 @@ import Confirm from './Shared/Confirm';
import * as Subscriber from './Subscriber';
import * as Profile from './Profile';
import * as Account from './Account';
export {
Layout,
@ -39,5 +40,6 @@ export {
Confirm,
Subscriber,
Profile
Profile,
Account
}

View file

@ -0,0 +1,216 @@
import { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { MODEL, fetchAccounts, deleteAccount } from 'modules/crud/account';
import { clearActionStatus } from 'modules/crud/actions';
import { select, selectActionStatus } from 'modules/crud/selectors';
import * as Notification from 'modules/notification/actions';
import {
Layout,
Account,
Spinner,
FloatingButton,
Blank,
Dimmed,
Confirm
} from 'components';
import Document from './Document';
class Collection extends Component {
state = {
search: '',
document: {
action: '',
visible: false,
dimmed: false
},
confirm: {
visible: false,
username: ''
},
};
componentWillMount() {
const { accounts, dispatch } = this.props
if (accounts.needsFetch) {
dispatch(accounts.fetch)
}
}
componentWillReceiveProps(nextProps) {
const { accounts, status } = nextProps
const { dispatch } = this.props
if (accounts.needsFetch) {
dispatch(accounts.fetch)
}
if (status.response) {
dispatch(Notification.success({
title: 'Account',
message: `${status.id} has been deleted`
}));
dispatch(clearActionStatus(MODEL, 'delete'));
}
if (status.error) {
let title = 'Unknown Code';
let message = 'Unknown Error';
if (response.data && response.data.name && response.data.message) {
title = response.data.name;
message = response.data.message;
} else {
title = response.status;
message = response.statusText;
}
dispatch(Notification.error({
title,
message,
autoDismiss: 0,
action: {
label: 'Dismiss'
}
}));
dispatch(clearActionStatus(MODEL, 'delete'));
}
}
handleSearchChange = (e) => {
this.setState({
search: e.target.value
});
}
handleSearchClear = (e) => {
this.setState({
search: ''
});
}
documentHandler = {
show: (action, payload) => {
this.setState({
document: {
action,
visible: true,
dimmed: true,
...payload
}
})
},
hide: () => {
this.setState({
document: {
action: '',
visible: false,
dimmed: false
},
})
},
actions: {
create: () => {
this.documentHandler.show('create');
},
update: (username) => {
this.documentHandler.show('update', { username });
}
}
}
confirmHandler = {
show: (username) => {
this.setState({
confirm: {
visible: true,
username,
}
})
},
hide: () => {
this.setState({
confirm: {
...this.state.confirm,
visible: false
}
})
},
actions : {
delete: () => {
const { dispatch } = this.props
if (this.state.confirm.visible === true) {
this.confirmHandler.hide();
this.documentHandler.hide();
dispatch(deleteAccount(this.state.confirm.username));
}
}
}
}
render() {
const {
handleSearchChange,
handleSearchClear,
documentHandler,
confirmHandler
} = this;
const {
search,
document
} = this.state;
const {
accounts,
status
} = this.props
const {
isLoading,
data
} = accounts;
return (
<Layout.Content>
<Account.List
accounts={data}
deletedId={status.id}
onEdit={documentHandler.actions.update}
onDelete={confirmHandler.show}
search={search}
/>
{isLoading && <Spinner md />}
<FloatingButton onClick={documentHandler.actions.create}/>
<Document
{ ...document }
onEdit={documentHandler.actions.update}
onDelete={confirmHandler.show}
onHide={documentHandler.hide} />
<Dimmed visible={document.dimmed} />
<Confirm
visible={this.state.confirm.visible}
message="Delete this account?"
onOutside={confirmHandler.hide}
buttons={[
{ text: "CANCEL", action: confirmHandler.hide, info:true },
{ text: "DELETE", action: confirmHandler.actions.delete, danger:true }
]}/>
</Layout.Content>
)
}
}
Collection = connect(
(state) => ({
accounts: select(fetchAccounts(), state.crud),
status: selectActionStatus(MODEL, state.crud, 'delete')
})
)(Collection);
export default Collection;

View file

@ -0,0 +1,180 @@
import { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import NProgress from 'nprogress';
import { MODEL, fetchAccounts, fetchAccount, createAccount, updateAccount } from 'modules/crud/account';
import { clearActionStatus } from 'modules/crud/actions';
import { select, selectActionStatus } from 'modules/crud/selectors';
import * as Notification from 'modules/notification/actions';
import { Account } from 'components';
import traverse from 'traverse';
const formData = {
"roles": [ "user" ]
}
class Document extends Component {
static propTypes = {
action: PropTypes.string,
visible: PropTypes.bool,
onHide: PropTypes.func
}
state = {
formData
}
componentWillMount() {
const { account, dispatch } = this.props
if (account.needsFetch) {
dispatch(account.fetch)
}
}
componentWillReceiveProps(nextProps) {
const { account, status } = nextProps
const { dispatch, action, onHide } = this.props
if (account.needsFetch) {
dispatch(account.fetch)
}
if (account.data) {
this.setState({ formData: account.data })
} else {
this.setState({ formData });
}
if (status.response) {
NProgress.configure({
parent: 'body',
trickleSpeed: 5
});
NProgress.done(true);
const message = action === 'create' ? "New account created" : `${status.id} account updated`;
dispatch(Notification.success({
title: 'Account',
message
}));
dispatch(clearActionStatus(MODEL, action));
onHide();
}
if (status.error) {
NProgress.configure({
parent: 'body',
trickleSpeed: 5
});
NProgress.done(true);
const response = ((status || {}).error || {}).response || {};
let title = 'Unknown Code';
let message = 'Unknown Error';
if (response.data && response.data.name && response.data.message) {
title = response.data.name;
message = response.data.message;
} else {
title = response.status;
message = response.statusText;
}
dispatch(Notification.error({
title,
message,
autoDismiss: 0,
action: {
label: 'Dismiss',
callback: () => onHide()
}
}));
dispatch(clearActionStatus(MODEL, action));
}
}
validate = (formData, errors) => {
const { accounts, action, status } = this.props;
const { username } = formData;
if (action === 'create' && accounts && accounts.data &&
accounts.data.filter(account => account.username === username).length > 0) {
errors.username.addError(`'${username}' is duplicated`);
}
return errors;
}
handleSubmit = (formData) => {
const { dispatch, action } = this.props;
NProgress.configure({
parent: '#nprogress-base-form',
trickleSpeed: 5
});
NProgress.start();
if (action === 'create') {
dispatch(createAccount({}, formData));
} else if (action === 'update') {
dispatch(updateAccount(formData.username, {}, formData));
} else {
throw new Error(`Action type '${action}' is invalid.`);
}
}
handleError = errors => {
const { dispatch } = this.props;
errors.map(error =>
dispatch(Notification.error({
title: 'Validation Error',
message: error.stack
}))
)
}
render() {
const {
validate,
handleSubmit,
handleError
} = this;
const {
visible,
action,
status,
account,
onHide
} = this.props
return (
<Account.Edit
visible={visible}
action={action}
formData={this.state.formData}
isLoading={account.isLoading && !status.pending}
validate={validate}
onHide={onHide}
onSubmit={handleSubmit}
onError={handleError} />
)
}
}
Document = connect(
(state, props) => ({
accounts: select(fetchAccounts(), state.crud),
account: select(fetchAccount(props.username), state.crud),
status: selectActionStatus(MODEL, state.crud, props.action)
})
)(Document);
export default Document;

View file

@ -0,0 +1,7 @@
import Collection from './Collection';
import Document from './Document';
export {
Collection,
Document
};

View file

@ -10,7 +10,7 @@ import { Layout } from 'components';
import Notification from 'containers/Notification';
import * as Subscriber from 'containers/Subscriber';
import * as Profile from 'containers/Profile';
import * as Account from 'containers/Account';
class App extends Component {
static propTypes = {
@ -49,6 +49,9 @@ class App extends Component {
<Layout.Container visible={view === "profile"}>
<Profile.Collection/>
</Layout.Container>
<Layout.Container visible={view === "account"}>
<Account.Collection/>
</Layout.Container>
<Notification/>
</Layout>
)

View file

@ -0,0 +1,30 @@
import {
fetchCollection,
fetchDocument,
createDocument,
updateDocument,
deleteDocument
} from './actions'
export const MODEL = 'accounts';
export const URL = '/Account';
export const fetchAccounts = (params = {}) => {
return fetchCollection(MODEL, URL, params, { idProperty: 'username' });
}
export const fetchAccount = (username, params = {}) => {
return fetchDocument(MODEL, username, `${URL}/${username}`, params, { idProperty: 'username' });
}
export const createAccount = (params = {}, data = {}) => {
return createDocument(MODEL, URL, params, data, { idProperty: 'username' });
}
export const updateAccount = (username, params = {}, data = {}) => {
return updateDocument(MODEL, username, `${URL}/${username}`, params, data, { idProperty: 'username' });
}
export const deleteAccount = (username, params = {}) => {
return deleteDocument(MODEL, username, `${URL}/${username}`, params, { idProperty: 'username' });
}