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": {
"no-console": 0,
"new-cap": 0,
"camelcase": 1,
// temp
"no-undef": 1,
"no-debugger": 1,

View File

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

View File

@ -24,7 +24,7 @@ const stateColor = {
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>
<CardView>
<CardMeta>
@ -35,7 +35,7 @@ export default ({ name, state, primaryIp, last, first }) => (
</CardAction>
<CardTitle to={`/instances/${name}`}>{name}</CardTitle>
<Small>
<CardLabel>{primaryIp}</CardLabel>
<CardLabel>{primary_ip}</CardLabel>
</Small>
<Small>
<CardLabel

View File

@ -1,4 +1,5 @@
import React from 'react';
import { Row, Col } from 'react-styled-flexboxgrid';
import forceArray from 'force-array';
import {
@ -6,7 +7,8 @@ import {
Input,
FormLabel,
ViewContainer,
StatusLoader
StatusLoader,
Select
} from 'joyent-ui-toolkit';
import Item from './item';
@ -15,7 +17,9 @@ export default ({
instances,
loading,
handleChange = () => null,
handleSubmit
onAction = () => null,
handleSubmit,
...rest
}) => {
const _instances = forceArray(instances);
@ -36,11 +40,66 @@ export default ({
) : null;
return (
<form onSubmit={() => handleSubmit(ctx => handleChange(ctx))}>
<form
onChange={() => handleSubmit(ctx => handleChange(ctx))}
onSubmit={handleSubmit}
>
<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 />
<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}
{items}
</form>

View File

@ -32,7 +32,8 @@ const Firewall = ({
/>
));
const _error = (error && !values.length && !_loading) ? (
const _error =
error && !values.length && !_loading ? (
<Message
title="Ooops!"
message="An error occurred while loading your instance firewall rules"

View File

@ -2,27 +2,47 @@ import React from 'react';
import PropTypes from 'prop-types';
import { compose, graphql } from 'react-apollo';
import { connect } from 'react-redux';
import { reduxForm } from 'redux-form';
import { reduxForm, change } from 'redux-form';
import forceArray from 'force-array';
import get from 'lodash.get';
import sortBy from 'lodash.sortby';
import find from 'lodash.find';
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 GenIndex from '@state/gen-index';
const InstanceListForm = reduxForm({
form: `instance-list`
form: `instance-list`,
initialValues: {
sort: 'name'
}
})(InstanceList);
const List = ({ instances = [], loading = false, error }) => {
const List = ({
selected = [],
instances = [],
loading = false,
error,
onAction
}) => {
const _title = <Title>Instances</Title>;
const _instances = forceArray(instances);
const _loading = !instances.length && loading;
const _error = (error && !_instances.length && !_loading) ? (
const _error =
error && !_instances.length && !_loading ? (
<Message
title="Ooops!"
message="An error occurred while loading your instances."
@ -30,11 +50,17 @@ const List = ({ instances = [], loading = false, error }) => {
/>
) : null;
const handleAction = name => onAction({ name, ids: selected });
return (
<ViewContainer main>
{_title}
{!_loading && _error}
<InstanceListForm instances={_instances} loading={loading} />
<InstanceListForm
instances={_instances}
loading={loading}
onAction={handleAction}
/>
</ViewContainer>
);
};
@ -50,7 +76,15 @@ List.propTypes = {
};
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: () => ({
pollInterval: 1000
}),
@ -65,15 +99,60 @@ export default compose(
};
}
}),
connect((state, ownProps) => {
const filter = get(state, 'form.instance-list.values.filter');
connect(
(state, ownProps) => {
const { index, instances = [], ...rest } = ownProps;
const form = get(state, 'form.instance-list.values', {});
const filter = get(form, 'filter');
const sort = get(form, 'sort');
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: !filter
? instances
: index.search(filter).map(({ ref }) => find(instances, ['id', ref]))
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);

View File

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

View File

@ -28,7 +28,8 @@ const Networks = ({ networks = [], loading, error }) => {
/>
));
const _error = (error && !values.length && !_loading) ? (
const _error =
error && !values.length && !_loading ? (
<Message
title="Ooops!"
message="An error occurred while loading your instance networks"

View File

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

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) {
id
name

View File

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

View File

@ -1,10 +1,17 @@
query Instances {
query instances {
machines {
id
name
state
firewall_enabled
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) {
id
name

View File

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

View File

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

View File

@ -1,4 +1,4 @@
query Instance($name: String!) {
query instance($name: String!) {
machines(name: $name) {
id
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 { reducer as formReducer } from 'redux-form';
import { ApolloClient, createNetworkInterface } from 'react-apollo';
@ -52,6 +53,7 @@ export const store = createStore(
}),
state, // Initial state
compose(
reduxBatch,
applyMiddleware(client.middleware()),
// If you are using the devToolsExtension, you can add it here also
// eslint-disable-next-line no-negated-condition

View File

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

View File

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