import React from 'react';
import { compose, graphql } from 'react-apollo';
import { connect } from 'react-redux';
import { stopSubmit, startSubmit, reset, change } from 'redux-form';
import { set } from 'react-redux-values';
import ReduxForm from 'declarative-redux-form';
import { Margin } from 'styled-components-spacing';
import forceArray from 'force-array';
import get from 'lodash.get';
import intercept from 'apr-intercept';
import find from 'lodash.find';
import reverse from 'lodash.reverse';
import sort from 'lodash.sortby';
import remcalc from 'remcalc';
import {
ViewContainer,
Message,
MessageDescription,
MessageTitle,
StatusLoader,
Divider
} from 'joyent-ui-toolkit';
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 RemoveInstance from '@graphql/remove-instance.gql';
import ToolbarForm from '@components/instances/toolbar';
import Index from '@state/gen-index';
import parseError from '@state/parse-error';
import {
default as InstanceList,
Item as InstanceListItem
} from '@components/instances/list';
import Empty from '@components/empty';
import InstanceListActions from '@components/instances/footer';
const TABLE_FORM_NAME = 'instance-list-table';
const MENU_FORM_NAME = 'instance-list-menu';
export const List = ({
instances = [],
selected = [],
allowedActions,
statuses,
sortBy = 'name',
sortOrder = 'desc',
loading = false,
error = null,
mutationError = null,
submitting,
handleAction,
toggleSelectAll,
handleSortBy,
history
}) => {
const _instances = forceArray(instances);
const _loading =
loading && !_instances.length
? [
,
]
: null;
const _error =
error && !_instances.length && !_loading ? (
Ooops!
An error occurred while loading your instances
) : null;
const _mutationError = mutationError && (
Ooops!
{mutationError}
);
const handleStart = selected => handleAction({ name: 'start', selected });
const handleStop = selected => handleAction({ name: 'stop', selected });
const handleReboot = selected => handleAction({ name: 'reboot', selected });
const handleRemove = selected => handleAction({ name: 'remove', selected });
const _table = !loading ? (
{props => (
{_instances.map(({ id, ...rest }) => (
handleStart([{ id }])}
onStop={() => handleStop([{ id }])}
onReboot={() => handleReboot([{ id }])}
onRemove={() => handleRemove([{ id }])}
/>
))}
)}
) : null;
const _empty =
!loading && !_instances.length ? (
You haven't created any instances yet, but they're really easy to set
up.
Click above to get going.
) : null;
const _footer =
!loading && selected.length ? (
handleStart(selected)}
onStop={() => handleStop(selected)}
onReboot={() => handleReboot(selected)}
onRemove={() => handleRemove(selected)}
/>
) : null;
return (
{props => (
history.push(`/instances/~create`)}
/>
)}
{!_mutationError ? _error : null}
{_mutationError}
{_loading}
{_table}
{_empty}
{_footer}
);
};
export default compose(
graphql(StopInstance, { name: 'stop' }),
graphql(StartInstance, { name: 'start' }),
graphql(RebootInstance, { name: 'reboot' }),
graphql(RemoveInstance, { name: 'remove' }),
graphql(ListInstances, {
options: () => ({
pollInterval: 1000
}),
props: ({ data: { machines, loading, error, refetch } }) => {
const instances = forceArray(machines).map(({ state, ...machine }) => ({
...machine,
state,
allowedActions: {
start: state !== 'RUNNING',
stop: state === 'RUNNING',
reboot: state === 'RUNNING',
remove: state !== 'PROVISIONING'
}
}));
return {
instances,
loading,
error,
index: Index(instances),
refetch
};
}
}),
connect(
({ form, values }, { index, error, instances = [] }) => {
// get search value
const filter = get(form, `${MENU_FORM_NAME}.values.filter`, false);
// check checked items ids
const checked = get(form, `${TABLE_FORM_NAME}.values`, {});
// check whether the main form is submitting
const submitting = get(form, `${TABLE_FORM_NAME}.submitting`, false);
// check whether the main form has an error
const mutationError = get(form, `${TABLE_FORM_NAME}.error`, null);
// get sort values
const sortBy = get(values, 'instance-list-sort-by', 'name');
const sortOrder = get(values, 'instance-list-sort-order', 'asc');
// if user is searching something, get items that match that query
const filtered = filter
? index.search(filter).map(({ ref }) => find(instances, ['id', ref]))
: instances;
// from filtered instances, sort asc
// set's mutating flag
const ascSorted = sort(filtered, [sortBy]).map(({ id, ...item }) => ({
...item,
id,
mutating: get(values, `${id}-mutating`, false)
}));
// if "select-all" is checked, all the instances are selected
// otherwise, map through the checked ids and get the instance value
const selected = Object.keys(checked)
.filter(id => Boolean(checked[id]))
.map(id => find(ascSorted, ['id', id]))
.filter(Boolean);
const allowedActions = {
start: selected.every(({ state }) => state === 'STOPPED'),
stop: selected.every(({ state }) => state === 'RUNNING'),
reboot: selected.every(({ state }) => state === 'RUNNING'),
remove: selected.every(({ state }) => state !== 'PROVISIONING')
};
// get mutating statuses
const statuses = {
starting: get(values, 'instance-list-starting', false),
stopping: get(values, 'instance-list-stoping', false),
rebooting: get(values, 'instance-list-rebooting', false),
removing: get(values, 'instance-list-removeing', false)
};
return {
// is sortOrder !== asc, reverse order
instances: sortOrder === 'asc' ? ascSorted : reverse(ascSorted),
allowedActions,
selected,
statuses,
submitting,
mutationError,
index,
sortOrder,
sortBy
};
},
(dispatch, { refetch, ...ownProps }) => ({
handleSortBy: ({ sortBy: currentSortBy, sortOrder }) => newSortBy => {
// sort prop is the same, toggle
if (currentSortBy === newSortBy) {
return dispatch(
set({
name: `instance-list-sort-order`,
value: sortOrder === 'desc' ? 'asc' : 'desc'
})
);
}
dispatch([
set({
name: `instance-list-sort-order`,
value: 'desc'
}),
set({
name: `instance-list-sort-by`,
value: newSortBy
})
]);
},
toggleSelectAll: ({ selected = [], instances = [] }) => () => {
const same = selected.length === instances.length;
const hasSelected = selected.length > 0;
// none are selected, toggle to all
if (!hasSelected) {
return dispatch(
instances.map(({ id }) => change(TABLE_FORM_NAME, id, true))
);
}
// all are selected, toggle to none
if (hasSelected && same) {
return dispatch(
instances.map(({ id }) => change(TABLE_FORM_NAME, id, false))
);
}
// some are selected, toggle to all
if (hasSelected && !same) {
return dispatch(
instances.map(({ id }) => change(TABLE_FORM_NAME, id, true))
);
}
},
handleAction: async ({ selected, name }) => {
const action = ownProps[name];
const gerund = `${name}ing`;
// flips submitting flag to true so that we can disable everything
const flipSubmitTrue = startSubmit(TABLE_FORM_NAME);
// sets (starting/rebooting/etc) to true so that we can, for instance,
// have a spinner on the correct button
const setIngTrue = set({
name: `instance-list-${gerund}`,
value: true
});
// sets the individual item mutation flags so that we can show a
// spinner in the row
const setMutatingTrue = selected.map(({ id }) =>
set({ name: `${id}-mutating`, value: true })
);
// wait for everything to finish and catch the error
const [err] = await intercept(
Promise.resolve(
dispatch([flipSubmitTrue, setIngTrue, ...setMutatingTrue])
).then(() => {
// starts all the mutations for all the selected items
return Promise.all(
selected.map(({ id }) => action({ variables: { id } }))
);
})
);
// reverts submitting flag to false and propagates the error if it exists
const flipSubmitFalse = stopSubmit(TABLE_FORM_NAME, {
_error: err && parseError(err)
});
// if no error, clears selected
const clearSelected = !err && reset(TABLE_FORM_NAME);
// reverts (starting/rebooting/etc) to false
const setIngFalse = set({
name: `instance-list-${gerund}`,
value: false
});
// reverts the individual item mutation flags
const setMutatingFalse =
name !== 'remove' &&
selected.map(({ id }) =>
set({ name: `${id}-mutating`, value: false })
);
const actions = [
flipSubmitFalse,
clearSelected,
setIngFalse,
...setMutatingFalse
].filter(Boolean);
// refetch list - even though we poll anyway - after clearing everything
return Promise.resolve(dispatch(actions)).then(() => refetch());
}
}),
(stateProps, dispatchProps, ownProps) => {
const { selected, instances, sortBy, sortOrder } = stateProps;
const { toggleSelectAll, handleSortBy } = dispatchProps;
return {
...ownProps,
...stateProps,
selected,
instances,
...dispatchProps,
toggleSelectAll: toggleSelectAll({ selected, instances }),
handleSortBy: handleSortBy({ sortBy, sortOrder })
};
}
)
)(List);