feat(my-joy-beta): sort and actions

This commit is contained in:
Sérgio Ramos 2017-10-04 18:27:55 +01:00 committed by Sérgio Ramos
parent 3135f3b5a7
commit dd32058c9d
27 changed files with 255 additions and 76 deletions

View File

@ -3,6 +3,7 @@
"rules": { "rules": {
"no-console": 0, "no-console": 0,
"new-cap": 0, "new-cap": 0,
"camelcase": 1,
// temp // temp
"no-undef": 1, "no-undef": 1,
"no-debugger": 1, "no-debugger": 1,

View File

@ -16,11 +16,13 @@
"prepublish": "echo 0" "prepublish": "echo 0"
}, },
"dependencies": { "dependencies": {
"@manaflair/redux-batch": "^0.1.0",
"apollo": "^0.2.2", "apollo": "^0.2.2",
"joyent-ui-toolkit": "^2.0.0", "joyent-ui-toolkit": "^2.0.0",
"lodash.find": "^4.6.0", "lodash.find": "^4.6.0",
"lodash.get": "^4.4.2", "lodash.get": "^4.4.2",
"lodash.isstring": "^4.0.1", "lodash.isstring": "^4.0.1",
"lodash.sortby": "^4.7.0",
"lunr": "^2.1.3", "lunr": "^2.1.3",
"normalized-styled-components": "^1.0.14", "normalized-styled-components": "^1.0.14",
"param-case": "^2.1.1", "param-case": "^2.1.1",

View File

@ -24,7 +24,7 @@ const stateColor = {
FAILED: 'red' FAILED: 'red'
}; };
export default ({ name, state, primaryIp, last, first }) => ( export default ({ name, state, primary_ip, last, first }) => (
<Card collapsed flat={!last} topMargin={first} bottomless={!last} gapless> <Card collapsed flat={!last} topMargin={first} bottomless={!last} gapless>
<CardView> <CardView>
<CardMeta> <CardMeta>
@ -35,7 +35,7 @@ export default ({ name, state, primaryIp, last, first }) => (
</CardAction> </CardAction>
<CardTitle to={`/instances/${name}`}>{name}</CardTitle> <CardTitle to={`/instances/${name}`}>{name}</CardTitle>
<Small> <Small>
<CardLabel>{primaryIp}</CardLabel> <CardLabel>{primary_ip}</CardLabel>
</Small> </Small>
<Small> <Small>
<CardLabel <CardLabel

View File

@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import { Row, Col } from 'react-styled-flexboxgrid';
import forceArray from 'force-array'; import forceArray from 'force-array';
import { import {
@ -6,7 +7,8 @@ import {
Input, Input,
FormLabel, FormLabel,
ViewContainer, ViewContainer,
StatusLoader StatusLoader,
Select
} from 'joyent-ui-toolkit'; } from 'joyent-ui-toolkit';
import Item from './item'; import Item from './item';
@ -15,7 +17,9 @@ export default ({
instances, instances,
loading, loading,
handleChange = () => null, handleChange = () => null,
handleSubmit onAction = () => null,
handleSubmit,
...rest
}) => { }) => {
const _instances = forceArray(instances); const _instances = forceArray(instances);
@ -36,11 +40,66 @@ export default ({
) : null; ) : null;
return ( return (
<form onSubmit={() => handleSubmit(ctx => handleChange(ctx))}> <form
<FormGroup name="filter" reduxForm> onChange={() => handleSubmit(ctx => handleChange(ctx))}
<FormLabel>Filter instances</FormLabel> onSubmit={handleSubmit}
<Input /> >
</FormGroup> <Row between="xs">
<Col xs={10} sm={8} lg={6}>
<Row>
<Col xs={7} sm={7} md={6} lg={6}>
<FormGroup name="filter" reduxForm>
<FormLabel>Filter instances</FormLabel>
<Input placeholder="Search for name, state, tags, etc..." fluid />
</FormGroup>
</Col>
<Col xs={5} sm={3} lg={3}>
<FormGroup name="sort" reduxForm>
<FormLabel>Sort</FormLabel>
<Select disabled={!items.length}>
<option value="name">Name</option>
<option value="state">State</option>
<option value="primary_ip">IP</option>
<option value="image.name">Image</option>
<option value="firewall_enabled">Firewall</option>
<option value="created">Created</option>
<option value="updated">Updated</option>
<option value="brand">Brand</option>
<option value="memory">Memory</option>
<option value="disk">Disk</option>
<option value="package.name">Package</option>
</Select>
</FormGroup>
</Col>
</Row>
</Col>
<Col xs={2} sm={4} lg={6}>
<Row end="xs">
<Col xs={12} sm={3} md={3} lg={2}>
<FormGroup>
<FormLabel>&#8291;</FormLabel>
<Select
value="actions"
disabled={!items.length}
onChange={({ target }) => onAction(target.value)}
>
<option value="actions" selected disabled>
&#8801;
</option>
<option value="stop">Stop</option>
<option value="start">Start</option>
<option value="reboot">Reboot</option>
<option value="resize">Resize</option>
<option value="enable-fw">Enable Firewall</option>
<option value="disable-fw">Disable Firewall</option>
<option value="create-snap">Create Snapshot</option>
<option value="start-snap">Start from Snapshot</option>
</Select>
</FormGroup>
</Col>
</Row>
</Col>
</Row>
{_loading} {_loading}
{items} {items}
</form> </form>

View File

@ -32,13 +32,14 @@ const Firewall = ({
/> />
)); ));
const _error = (error && !values.length && !_loading) ? ( const _error =
<Message error && !values.length && !_loading ? (
title="Ooops!" <Message
message="An error occurred while loading your instance firewall rules" title="Ooops!"
error message="An error occurred while loading your instance firewall rules"
/> error
) : null; />
) : null;
return ( return (
<ViewContainer center={Boolean(_loading)} main> <ViewContainer center={Boolean(_loading)} main>

View File

@ -2,39 +2,65 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { compose, graphql } from 'react-apollo'; import { compose, graphql } from 'react-apollo';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { reduxForm } from 'redux-form'; import { reduxForm, change } from 'redux-form';
import forceArray from 'force-array'; import forceArray from 'force-array';
import get from 'lodash.get'; import get from 'lodash.get';
import sortBy from 'lodash.sortby';
import find from 'lodash.find'; import find from 'lodash.find';
import { ViewContainer, Title, Message } from 'joyent-ui-toolkit'; import { ViewContainer, Title, Message } from 'joyent-ui-toolkit';
import GetInstances from '@graphql/list-instances.gql'; import ListInstances from '@graphql/list-instances.gql';
import StopInstance from '@graphql/stop-instance.gql';
import StartInstance from '@graphql/start-instance.gql';
import RebootInstance from '@graphql/reboot-instance.gql';
import ResizeInstance from '@graphql/resize-instance.gql';
import EnableInstanceFw from '@graphql/enable-instance-fw.gql';
import DisableInstanceFw from '@graphql/disable-instance-fw.gql';
import CreateSnapshot from '@graphql/create-snapshot.gql';
import StartSnapshot from '@graphql/start-from-snapshot.gql';
import { List as InstanceList } from '@components/instances'; import { List as InstanceList } from '@components/instances';
import GenIndex from '@state/gen-index'; import GenIndex from '@state/gen-index';
const InstanceListForm = reduxForm({ const InstanceListForm = reduxForm({
form: `instance-list` form: `instance-list`,
initialValues: {
sort: 'name'
}
})(InstanceList); })(InstanceList);
const List = ({ instances = [], loading = false, error }) => { const List = ({
selected = [],
instances = [],
loading = false,
error,
onAction
}) => {
const _title = <Title>Instances</Title>; const _title = <Title>Instances</Title>;
const _instances = forceArray(instances); const _instances = forceArray(instances);
const _loading = !instances.length && loading; const _loading = !instances.length && loading;
const _error = (error && !_instances.length && !_loading) ? ( const _error =
<Message error && !_instances.length && !_loading ? (
title="Ooops!" <Message
message="An error occurred while loading your instances." title="Ooops!"
error message="An error occurred while loading your instances."
/> error
) : null; />
) : null;
const handleAction = name => onAction({ name, ids: selected });
return ( return (
<ViewContainer main> <ViewContainer main>
{_title} {_title}
{!_loading && _error} {!_loading && _error}
<InstanceListForm instances={_instances} loading={loading} /> <InstanceListForm
instances={_instances}
loading={loading}
onAction={handleAction}
/>
</ViewContainer> </ViewContainer>
); );
}; };
@ -50,7 +76,15 @@ List.propTypes = {
}; };
export default compose( export default compose(
graphql(GetInstances, { graphql(StopInstance, { name: 'stop' }),
graphql(StartInstance, { name: 'start' }),
graphql(RebootInstance, { name: 'reboot' }),
graphql(ResizeInstance, { name: 'resize' }),
graphql(EnableInstanceFw, { name: 'enableFw' }),
graphql(DisableInstanceFw, { name: 'disableFw' }),
graphql(CreateSnapshot, { name: 'createSnapshot' }),
graphql(StartSnapshot, { name: 'startSnapshot' }),
graphql(ListInstances, {
options: () => ({ options: () => ({
pollInterval: 1000 pollInterval: 1000
}), }),
@ -65,15 +99,60 @@ export default compose(
}; };
} }
}), }),
connect((state, ownProps) => { connect(
const filter = get(state, 'form.instance-list.values.filter'); (state, ownProps) => {
const { index, instances = [], ...rest } = ownProps; const { index, instances = [], ...rest } = ownProps;
return { const form = get(state, 'form.instance-list.values', {});
...rest, const filter = get(form, 'filter');
instances: !filter const sort = get(form, 'sort');
? instances
: index.search(filter).map(({ ref }) => find(instances, ['id', ref])) const values = filter
}; ? index.search(filter).map(({ ref }) => find(instances, ['id', ref]))
}) : instances;
const selected = Object.keys(form)
.map(name => find(values, ['name', name]))
.filter(Boolean)
.map(({ id }) => id);
return {
...rest,
instances: sortBy(values, value => get(value, sort)),
selected
};
},
(dispatch, { instances, ...ownProps }) => ({
onAction: ({ name, ids = [] }) => {
const types = {
stop: () =>
Promise.all(ids.map(id => ownProps.stop({ variables: { id } }))),
start: () =>
Promise.all(ids.map(id => ownProps.start({ variables: { id } }))),
reboot: () =>
Promise.all(ids.map(id => ownProps.reboot({ variables: { id } }))),
resize: () => null,
'enable-fw': () => null,
'disable-fw': () => null,
'create-snap': () => null,
'start-snap': () => null
};
const clearSelected = () => dispatch(ids.map(id => {
const form = 'instance-list';
const field = get(find(instances, ['id', id]), 'name');
const value = false;
if (!field) {
return;
}
return change(form, field, value);
}));
const fn = types[name];
return fn && fn().then(clearSelected);
}
})
)
)(List); )(List);

View File

@ -38,19 +38,18 @@ const MetadataForms = (metadata = []) =>
const Metadata = ({ metadata = [], loading, error }) => { const Metadata = ({ metadata = [], loading, error }) => {
const values = forceArray(metadata); const values = forceArray(metadata);
const _title = <Title>Metadata</Title>; const _title = <Title>Metadata</Title>;
const _loading = !(loading && !values.length) ? null : ( const _loading = !(loading && !values.length) ? null : <StatusLoader />;
<StatusLoader />
);
const _metadata = !_loading && MetadataForms(values); const _metadata = !_loading && MetadataForms(values);
const _error = (error && !values.length && !_loading) ? ( const _error =
<Message error && !values.length && !_loading ? (
title="Ooops!" <Message
message="An error occurred while loading your instance metadata" title="Ooops!"
error message="An error occurred while loading your instance metadata"
/> error
) : null; />
) : null;
return ( return (
<ViewContainer center={Boolean(_loading)} main> <ViewContainer center={Boolean(_loading)} main>

View File

@ -28,13 +28,14 @@ const Networks = ({ networks = [], loading, error }) => {
/> />
)); ));
const _error = (error && !values.length && !_loading) ? ( const _error =
<Message error && !values.length && !_loading ? (
title="Ooops!" <Message
message="An error occurred while loading your instance networks" title="Ooops!"
error message="An error occurred while loading your instance networks"
/> error
) : null; />
) : null;
return ( return (
<ViewContainer center={Boolean(_loading)} main> <ViewContainer center={Boolean(_loading)} main>

View File

@ -38,19 +38,18 @@ const TagForms = (tags = []) =>
const Tags = ({ tags = [], loading, error }) => { const Tags = ({ tags = [], loading, error }) => {
const values = forceArray(tags); const values = forceArray(tags);
const _title = <Title>Tags</Title>; const _title = <Title>Tags</Title>;
const _loading = (loading && !values.length) ? ( const _loading = loading && !values.length ? <StatusLoader /> : null;
<StatusLoader />
) : null;
const _tags = !_loading && TagForms(tags); const _tags = !_loading && TagForms(tags);
const _error = (error && !values.length && !_loading) ? ( const _error =
<Message error && !values.length && !_loading ? (
title="Ooops!" <Message
message="An error occurred while loading your instance tags" title="Ooops!"
error message="An error occurred while loading your instance tags"
/> error
) : null; />
) : null;
return ( return (
<ViewContainer center={Boolean(_loading)} main> <ViewContainer center={Boolean(_loading)} main>

View File

@ -0,0 +1,3 @@
mutation createInstanceSnapshot($id: ID!, $name: String) {
createMachineSnapshot(id: $id, name: $name)
}

View File

@ -0,0 +1,3 @@
mutation disableMachineFirewall($id: ID!) {
disableMachineFirewall(id: $id)
}

View File

@ -0,0 +1,3 @@
mutation enableMachineFirewall($id: ID!) {
enableMachineFirewall(id: $id)
}

View File

@ -1,4 +1,4 @@
query Instance($name: String!) { query instance($name: String!) {
machines(name: $name) { machines(name: $name) {
id id
name name

View File

@ -1,4 +1,4 @@
query Instance($name: String!) { query instance($name: String!) {
machines(name: $name) { machines(name: $name) {
id id
name name

View File

@ -1,10 +1,17 @@
query Instances { query instances {
machines { machines {
id id
name name
state state
firewall_enabled
primary_ip primary_ip
docker firewall_enabled
created
updated
brand
memory
disk
package {
name
}
} }
} }

View File

@ -1,4 +1,4 @@
query Instance($name: String!) { query instance($name: String!) {
machines(name: $name) { machines(name: $name) {
id id
name name

View File

@ -1,4 +1,4 @@
query Instance($name: String!) { query instance($name: String!) {
machines(name: $name) { machines(name: $name) {
id id
name name

View File

@ -1,4 +1,4 @@
query Instance($name: String!) { query instance($name: String!) {
machines(name: $name) { machines(name: $name) {
id id
name name

View File

@ -1,4 +1,4 @@
query Instance($name: String!) { query instance($name: String!) {
machines(name: $name) { machines(name: $name) {
id id
name name

View File

@ -0,0 +1,5 @@
mutation rebootInstance($id: ID!) {
rebootMachine(id: $id) {
id
}
}

View File

@ -0,0 +1,3 @@
mutation resizeInstance($id: ID!, $package: ID!) {
resizeMachine(id: $id, package: $package)
}

View File

@ -0,0 +1,3 @@
mutation startInstanceFromSnapshot($id: ID!, $snapshot: ID!) {
startMachineFromSnapshot(id: $id, snapshot: $snapshot)
}

View File

@ -0,0 +1,3 @@
mutation startInstance($id: ID!) {
startMachine(id: $id)
}

View File

@ -0,0 +1,3 @@
mutation stopInstance($id: ID!) {
stopMachine(id: $id)
}

View File

@ -1,3 +1,4 @@
import { reduxBatch } from '@manaflair/redux-batch';
import { createStore, combineReducers, applyMiddleware, compose } from 'redux'; import { createStore, combineReducers, applyMiddleware, compose } from 'redux';
import { reducer as formReducer } from 'redux-form'; import { reducer as formReducer } from 'redux-form';
import { ApolloClient, createNetworkInterface } from 'react-apollo'; import { ApolloClient, createNetworkInterface } from 'react-apollo';
@ -52,6 +53,7 @@ export const store = createStore(
}), }),
state, // Initial state state, // Initial state
compose( compose(
reduxBatch,
applyMiddleware(client.middleware()), applyMiddleware(client.middleware()),
// If you are using the devToolsExtension, you can add it here also // If you are using the devToolsExtension, you can add it here also
// eslint-disable-next-line no-negated-condition // eslint-disable-next-line no-negated-condition

View File

@ -126,9 +126,11 @@ const ToggleBase = ({ container = null, type = 'radio' }) =>
id: rndId() id: rndId()
}; };
const checked = type === 'checkbox' && rest.value === true;
const toggle = ( const toggle = (
<InnerContainer {...types} type={type}> <InnerContainer {...types} type={type}>
<StyledInput {...rest} id={newValue.id} type={type} /> <StyledInput {...rest} id={newValue.id} type={type} checked={checked} />
<Label <Label
{...types} {...types}
htmlFor={newValue.id} htmlFor={newValue.id}

View File

@ -11,6 +11,7 @@ const Label = styled.label`
font-stretch: normal; font-stretch: normal;
display: block; display: block;
color: ${props => props.theme.secondary}; color: ${props => props.theme.secondary};
text-align: left;
`; `;
export default Baseline(Label); export default Baseline(Label);