2017-09-20 12:30:53 +03:00
|
|
|
import React from 'react';
|
|
|
|
import { compose, graphql } from 'react-apollo';
|
|
|
|
import { connect } from 'react-redux';
|
2017-11-23 14:18:38 +02:00
|
|
|
import { stopSubmit, startSubmit, reset, change } from 'redux-form';
|
|
|
|
import { set } from 'react-redux-values';
|
|
|
|
import ReduxForm from 'declarative-redux-form';
|
2017-09-20 12:30:53 +03:00
|
|
|
import forceArray from 'force-array';
|
|
|
|
import get from 'lodash.get';
|
2017-11-23 14:18:38 +02:00
|
|
|
import intercept from 'apr-intercept';
|
2017-09-20 12:30:53 +03:00
|
|
|
import find from 'lodash.find';
|
2017-11-23 14:18:38 +02:00
|
|
|
import sort from 'lodash.sortby';
|
|
|
|
import remcalc from 'remcalc';
|
2017-10-13 22:51:18 +03:00
|
|
|
|
2017-10-09 16:43:51 +03:00
|
|
|
import {
|
|
|
|
ViewContainer,
|
|
|
|
Message,
|
|
|
|
MessageDescription,
|
2017-11-23 14:18:38 +02:00
|
|
|
MessageTitle,
|
|
|
|
StatusLoader,
|
|
|
|
Divider
|
2017-10-09 16:43:51 +03:00
|
|
|
} from 'joyent-ui-toolkit';
|
2017-09-20 12:30:53 +03:00
|
|
|
|
2017-10-04 20:27:55 +03:00
|
|
|
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';
|
2017-12-21 02:12:42 +02:00
|
|
|
import ToolbarForm from '@components/instances/toolbar';
|
2017-11-23 14:18:38 +02:00
|
|
|
import Index from '@state/gen-index';
|
2017-12-06 20:16:11 +02:00
|
|
|
import parseError from '@state/parse-error';
|
2017-10-04 20:27:55 +03:00
|
|
|
|
2017-11-23 14:18:38 +02:00
|
|
|
import {
|
|
|
|
default as InstanceList,
|
2018-01-05 17:42:09 +02:00
|
|
|
Item as InstanceListItem
|
2017-11-23 14:18:38 +02:00
|
|
|
} from '@components/instances/list';
|
2017-09-20 12:30:53 +03:00
|
|
|
|
2018-01-05 17:42:09 +02:00
|
|
|
import InstanceListActions from '@components/instances/footer';
|
|
|
|
|
2017-11-23 14:18:38 +02:00
|
|
|
const TABLE_FORM_NAME = 'instance-list-table';
|
|
|
|
const MENU_FORM_NAME = 'instance-list-menu';
|
2017-09-20 12:30:53 +03:00
|
|
|
|
2017-12-21 02:12:42 +02:00
|
|
|
export const List = ({
|
2017-10-04 20:27:55 +03:00
|
|
|
instances = [],
|
2017-11-23 14:18:38 +02:00
|
|
|
selected = [],
|
|
|
|
allowedActions,
|
2017-12-21 02:12:42 +02:00
|
|
|
statuses,
|
2017-11-23 14:18:38 +02:00
|
|
|
sortBy = 'name',
|
|
|
|
sortOrder = 'desc',
|
2017-10-04 20:27:55 +03:00
|
|
|
loading = false,
|
2017-11-23 14:18:38 +02:00
|
|
|
error = null,
|
2017-12-22 05:28:36 +02:00
|
|
|
mutationError = null,
|
2017-12-21 02:12:42 +02:00
|
|
|
submitting,
|
2017-11-23 14:18:38 +02:00
|
|
|
handleAction,
|
|
|
|
toggleSelectAll,
|
|
|
|
handleSortBy
|
2017-10-04 20:27:55 +03:00
|
|
|
}) => {
|
2017-09-20 12:30:53 +03:00
|
|
|
const _instances = forceArray(instances);
|
2017-11-23 14:18:38 +02:00
|
|
|
|
|
|
|
const _loading =
|
|
|
|
loading && !_instances.length
|
|
|
|
? [
|
|
|
|
<Divider key="divider" height={remcalc(30)} transparent />,
|
|
|
|
<StatusLoader key="spinner" />
|
|
|
|
]
|
|
|
|
: null;
|
2017-09-20 12:30:53 +03:00
|
|
|
|
2017-10-04 20:27:55 +03:00
|
|
|
const _error =
|
|
|
|
error && !_instances.length && !_loading ? (
|
2017-10-09 16:43:51 +03:00
|
|
|
<Message error>
|
|
|
|
<MessageTitle>Ooops!</MessageTitle>
|
|
|
|
<MessageDescription>
|
|
|
|
An error occurred while loading your instances
|
|
|
|
</MessageDescription>
|
|
|
|
</Message>
|
2017-10-04 20:27:55 +03:00
|
|
|
) : null;
|
|
|
|
|
2017-12-22 05:28:36 +02:00
|
|
|
const _mutationError = mutationError && (
|
|
|
|
<Message error>
|
|
|
|
<MessageTitle>Ooops!</MessageTitle>
|
|
|
|
<MessageDescription>{mutationError}</MessageDescription>
|
|
|
|
</Message>
|
|
|
|
);
|
|
|
|
|
2017-12-21 02:12:42 +02:00
|
|
|
const handleStart = selected => handleAction({ name: 'start', selected });
|
|
|
|
const handleStop = selected => handleAction({ name: 'stop', selected });
|
2017-12-22 05:28:36 +02:00
|
|
|
const handleReboot = selected => handleAction({ name: 'reboot', selected });
|
2017-12-21 02:12:42 +02:00
|
|
|
const handleDelete = selected => handleAction({ name: 'delete', selected });
|
|
|
|
|
2017-11-23 14:18:38 +02:00
|
|
|
const _table = !loading ? (
|
2017-12-21 02:12:42 +02:00
|
|
|
<ReduxForm form={TABLE_FORM_NAME}>
|
|
|
|
{props => (
|
|
|
|
<InstanceList
|
|
|
|
{...props}
|
|
|
|
allSelected={instances.length && selected.length === instances.length}
|
|
|
|
sortBy={sortBy}
|
|
|
|
sortOrder={sortOrder}
|
|
|
|
toggleSelectAll={toggleSelectAll}
|
|
|
|
onSortBy={handleSortBy}
|
|
|
|
>
|
|
|
|
{_instances.map(({ id, ...rest }) => (
|
|
|
|
<InstanceListItem
|
|
|
|
key={id}
|
|
|
|
id={id}
|
|
|
|
{...rest}
|
2017-12-22 05:28:36 +02:00
|
|
|
onStart={() => handleStart([{ id }])}
|
|
|
|
onStop={() => handleStop([{ id }])}
|
|
|
|
onReboot={() => handleReboot([{ id }])}
|
|
|
|
onDelete={() => handleDelete([{ id }])}
|
2017-12-21 02:12:42 +02:00
|
|
|
/>
|
|
|
|
))}
|
|
|
|
</InstanceList>
|
|
|
|
)}
|
2017-11-23 14:18:38 +02:00
|
|
|
</ReduxForm>
|
|
|
|
) : null;
|
|
|
|
|
2017-12-21 02:12:42 +02:00
|
|
|
const _footer =
|
|
|
|
!loading && selected.length ? (
|
|
|
|
<InstanceListActions
|
|
|
|
allowedActions={allowedActions}
|
|
|
|
statuses={statuses}
|
|
|
|
submitting={submitting}
|
|
|
|
onStart={() => handleStart(selected)}
|
|
|
|
onStop={() => handleStop(selected)}
|
|
|
|
onReboot={() => handleReboot(selected)}
|
|
|
|
onDelete={() => handleDelete(selected)}
|
|
|
|
/>
|
|
|
|
) : null;
|
|
|
|
|
2017-09-20 12:30:53 +03:00
|
|
|
return (
|
2017-09-27 17:23:49 +03:00
|
|
|
<ViewContainer main>
|
2017-11-23 14:18:38 +02:00
|
|
|
<Divider height={remcalc(30)} transparent />
|
2017-12-21 02:12:42 +02:00
|
|
|
<ReduxForm form={MENU_FORM_NAME}>
|
|
|
|
{props => (
|
|
|
|
<ToolbarForm
|
|
|
|
{...props}
|
|
|
|
searchLabel="Filter instances"
|
|
|
|
searchPlaceholder="Search for name, state, tags, etc..."
|
|
|
|
searchable={!_loading}
|
|
|
|
actionLabel="Create Instance"
|
|
|
|
actionable={false}
|
|
|
|
/>
|
|
|
|
)}
|
2017-11-23 14:18:38 +02:00
|
|
|
</ReduxForm>
|
2017-12-22 05:28:36 +02:00
|
|
|
{!_mutationError ? _error : null}
|
|
|
|
{_mutationError}
|
2017-11-23 14:18:38 +02:00
|
|
|
{_loading}
|
|
|
|
{_table}
|
2017-12-21 02:12:42 +02:00
|
|
|
{_footer}
|
2017-09-20 12:30:53 +03:00
|
|
|
</ViewContainer>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
export default compose(
|
2017-10-04 20:27:55 +03:00
|
|
|
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, {
|
2017-09-20 12:30:53 +03:00
|
|
|
options: () => ({
|
|
|
|
pollInterval: 1000
|
|
|
|
}),
|
2017-11-23 14:18:38 +02:00
|
|
|
props: ({ data: { machines, loading, error, refetch } }) => {
|
|
|
|
const instances = forceArray(machines).map(({ state, ...machine }) => ({
|
|
|
|
...machine,
|
|
|
|
state,
|
|
|
|
allowedActions: {
|
|
|
|
start: state !== 'RUNNING',
|
|
|
|
stop: state === 'RUNNING'
|
|
|
|
}
|
|
|
|
}));
|
2017-09-20 12:30:53 +03:00
|
|
|
|
|
|
|
return {
|
2017-11-23 14:18:38 +02:00
|
|
|
instances,
|
2017-09-20 12:30:53 +03:00
|
|
|
loading,
|
|
|
|
error,
|
2017-11-23 14:18:38 +02:00
|
|
|
index: Index(instances),
|
|
|
|
refetch
|
2017-09-20 12:30:53 +03:00
|
|
|
};
|
|
|
|
}
|
|
|
|
}),
|
2017-10-04 20:27:55 +03:00
|
|
|
connect(
|
2017-12-21 02:12:42 +02:00
|
|
|
({ form, values }, { index, error, instances = [] }) => {
|
2017-11-23 14:18:38 +02:00
|
|
|
// 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`, {});
|
2017-12-21 02:12:42 +02:00
|
|
|
// check whether the main form is submitting
|
|
|
|
const submitting = get(form, `${TABLE_FORM_NAME}.submitting`, false);
|
|
|
|
// check whether the main form has an error
|
2017-12-22 05:28:36 +02:00
|
|
|
const mutationError = get(form, `${TABLE_FORM_NAME}.error`, null);
|
2017-11-23 14:18:38 +02:00
|
|
|
// get sort values
|
|
|
|
const sortBy = get(values, 'instance-list-sort-by', 'name');
|
|
|
|
const sortOrder = get(values, 'instance-list-sort-order', 'asc');
|
2017-10-04 20:27:55 +03:00
|
|
|
|
2017-11-23 14:18:38 +02:00
|
|
|
// if user is searching something, get items that match that query
|
|
|
|
const filtered = filter
|
2017-10-04 20:27:55 +03:00
|
|
|
? index.search(filter).map(({ ref }) => find(instances, ['id', ref]))
|
|
|
|
: instances;
|
|
|
|
|
2017-11-23 14:18:38 +02:00
|
|
|
// from filtered instances, sort asc
|
2017-12-21 02:12:42 +02:00
|
|
|
// set's mutating flag
|
|
|
|
const ascSorted = sort(filtered, [sortBy]).map(({ id, ...item }) => ({
|
|
|
|
...item,
|
|
|
|
id,
|
|
|
|
mutating: get(values, `${id}-mutating`, false)
|
|
|
|
}));
|
2017-11-23 14:18:38 +02:00
|
|
|
|
|
|
|
// 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]))
|
2017-10-11 19:59:59 +03:00
|
|
|
.filter(Boolean);
|
2017-10-04 20:27:55 +03:00
|
|
|
|
2017-11-23 14:18:38 +02:00
|
|
|
const allowedActions = {
|
|
|
|
start: selected.some(({ state }) => state !== 'RUNNING'),
|
|
|
|
stop: selected.some(({ state }) => state === 'RUNNING')
|
|
|
|
};
|
|
|
|
|
2017-12-21 02:12:42 +02:00
|
|
|
// get mutating statuses
|
|
|
|
const statuses = {
|
|
|
|
starting: get(values, 'instance-list-starting', false),
|
|
|
|
stopping: get(values, 'instance-list-stoping', false),
|
2017-12-22 05:28:36 +02:00
|
|
|
rebooting: get(values, 'instance-list-rebooting', false),
|
2017-12-21 02:12:42 +02:00
|
|
|
deleting: get(values, 'instance-list-deleteing', false)
|
|
|
|
};
|
|
|
|
|
2017-10-04 20:27:55 +03:00
|
|
|
return {
|
2017-11-23 14:18:38 +02:00
|
|
|
// is sortOrder !== asc, reverse order
|
|
|
|
instances: sortOrder === 'asc' ? ascSorted : ascSorted.reverse(),
|
|
|
|
allowedActions,
|
|
|
|
selected,
|
2017-12-21 02:12:42 +02:00
|
|
|
statuses,
|
|
|
|
submitting,
|
2017-12-22 05:28:36 +02:00
|
|
|
mutationError,
|
2017-11-23 14:18:38 +02:00
|
|
|
index,
|
|
|
|
sortOrder,
|
|
|
|
sortBy
|
2017-10-04 20:27:55 +03:00
|
|
|
};
|
|
|
|
},
|
2017-11-23 14:18:38 +02:00
|
|
|
(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'
|
|
|
|
})
|
2017-10-09 16:44:26 +03:00
|
|
|
);
|
2017-11-23 14:18:38 +02:00
|
|
|
}
|
2017-10-04 20:27:55 +03:00
|
|
|
|
2017-11-23 14:18:38 +02:00
|
|
|
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;
|
2017-10-13 22:51:18 +03:00
|
|
|
|
2017-11-23 14:18:38 +02:00
|
|
|
// 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);
|
|
|
|
|
2017-12-22 05:28:36 +02:00
|
|
|
// sets (starting/rebooting/etc) to true so that we can, for instance,
|
2017-11-23 14:18:38 +02:00
|
|
|
// 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);
|
|
|
|
|
2017-12-22 05:28:36 +02:00
|
|
|
// reverts (starting/rebooting/etc) to false
|
2017-11-23 14:18:38 +02:00
|
|
|
const setIngFalse = set({
|
|
|
|
name: `instance-list-${gerund}`,
|
|
|
|
value: false
|
|
|
|
});
|
|
|
|
|
|
|
|
// reverts the individual item mutation flags
|
|
|
|
const setMutatingFalse = selected.map(({ id }) =>
|
|
|
|
set({ name: `${id}-mutating`, value: false })
|
2017-10-13 22:51:18 +03:00
|
|
|
);
|
2017-11-23 14:18:38 +02:00
|
|
|
|
|
|
|
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());
|
2017-10-04 20:27:55 +03:00
|
|
|
}
|
2017-11-23 14:18:38 +02:00
|
|
|
}),
|
|
|
|
(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 })
|
|
|
|
};
|
|
|
|
}
|
2017-10-04 20:27:55 +03:00
|
|
|
)
|
2017-09-20 12:30:53 +03:00
|
|
|
)(List);
|