2017-09-20 12:30:53 +03:00
|
|
|
import React from 'react';
|
|
|
|
import forceArray from 'force-array';
|
2017-10-13 22:56:08 +03:00
|
|
|
import { connect } from 'react-redux';
|
2018-01-05 18:35:26 +02:00
|
|
|
import { stopSubmit, startSubmit, change, reset } from 'redux-form';
|
2017-09-20 12:30:53 +03:00
|
|
|
import { compose, graphql } from 'react-apollo';
|
2018-01-30 18:04:03 +02:00
|
|
|
import { Margin } from 'styled-components-spacing';
|
2017-09-20 12:30:53 +03:00
|
|
|
import find from 'lodash.find';
|
2018-01-30 01:20:22 +02:00
|
|
|
import reverse from 'lodash.reverse';
|
2017-09-20 12:30:53 +03:00
|
|
|
import get from 'lodash.get';
|
2018-01-05 17:42:09 +02:00
|
|
|
import sort from 'lodash.sortby';
|
|
|
|
import { set } from 'react-redux-values';
|
|
|
|
import ReduxForm from 'declarative-redux-form';
|
|
|
|
import intercept from 'apr-intercept';
|
2017-10-13 22:56:08 +03:00
|
|
|
|
2017-10-09 16:43:51 +03:00
|
|
|
import {
|
|
|
|
ViewContainer,
|
|
|
|
Message,
|
|
|
|
MessageTitle,
|
2018-01-05 17:42:09 +02:00
|
|
|
MessageDescription,
|
|
|
|
StatusLoader
|
2017-10-09 16:43:51 +03:00
|
|
|
} from 'joyent-ui-toolkit';
|
2017-09-20 12:30:53 +03:00
|
|
|
|
2018-01-23 14:20:36 +02:00
|
|
|
import {
|
|
|
|
default as SnapshotsList,
|
|
|
|
AddForm as SnapshotAddForm
|
|
|
|
} from '@components/instances/snapshots';
|
|
|
|
|
2017-09-20 12:30:53 +03:00
|
|
|
import GetSnapshots from '@graphql/list-snapshots.gql';
|
2017-10-13 22:56:08 +03:00
|
|
|
import StartSnapshot from '@graphql/start-from-snapshot.gql';
|
|
|
|
import RemoveSnapshot from '@graphql/remove-snapshot.gql';
|
2018-01-05 17:42:09 +02:00
|
|
|
import CreateSnapshotMutation from '@graphql/create-snapshot.gql';
|
|
|
|
import ToolbarForm from '@components/instances/toolbar';
|
|
|
|
import SnapshotsListActions from '@components/instances/footer';
|
|
|
|
import parseError from '@state/parse-error';
|
2018-01-23 14:20:36 +02:00
|
|
|
import GenIndex from '@state/gen-index';
|
2017-10-13 22:56:08 +03:00
|
|
|
|
2018-01-05 17:42:09 +02:00
|
|
|
const MENU_FORM_NAME = 'snapshot-list-menu';
|
|
|
|
const TABLE_FORM_NAME = 'snapshot-list-table';
|
|
|
|
const CREATE_FORM_NAME = 'create-snapshot-form';
|
2017-09-20 12:30:53 +03:00
|
|
|
|
2017-10-13 22:56:08 +03:00
|
|
|
const Snapshots = ({
|
|
|
|
snapshots = [],
|
2018-01-05 17:42:09 +02:00
|
|
|
instance = {},
|
2017-10-13 22:56:08 +03:00
|
|
|
selected = [],
|
|
|
|
loading,
|
2018-01-05 17:42:09 +02:00
|
|
|
submitting,
|
2017-10-13 22:56:08 +03:00
|
|
|
error,
|
2018-01-05 17:42:09 +02:00
|
|
|
mutationError,
|
|
|
|
allowedActions,
|
|
|
|
statuses,
|
|
|
|
handleAction,
|
|
|
|
handleCreateSnapshot,
|
|
|
|
sortOrder,
|
|
|
|
handleSortBy,
|
|
|
|
sortBy,
|
|
|
|
toggleSelectAll,
|
|
|
|
toggleCreateSnapshotOpen,
|
|
|
|
createSnapshotOpen
|
2017-10-13 22:56:08 +03:00
|
|
|
}) => {
|
|
|
|
const _values = forceArray(snapshots);
|
2018-01-05 17:42:09 +02:00
|
|
|
const _loading = !_values.length && loading ? <StatusLoader /> : null;
|
|
|
|
|
|
|
|
const handleStart = selected => handleAction({ name: 'start', selected });
|
|
|
|
const handleRemove = selected => handleAction({ name: 'remove', selected });
|
2017-09-20 12:30:53 +03:00
|
|
|
|
2017-10-13 22:56:08 +03:00
|
|
|
const _error = error &&
|
2017-10-31 12:03:44 +02:00
|
|
|
!_loading &&
|
|
|
|
!_values.length && (
|
2018-01-30 18:04:03 +02:00
|
|
|
<Margin bottom={4}>
|
|
|
|
<Message error>
|
|
|
|
<MessageTitle>Ooops!</MessageTitle>
|
|
|
|
<MessageDescription>
|
|
|
|
An error occurred while loading your instance snapshots
|
|
|
|
</MessageDescription>
|
|
|
|
</Message>
|
|
|
|
</Margin>
|
2017-10-31 12:03:44 +02:00
|
|
|
);
|
2017-09-20 12:30:53 +03:00
|
|
|
|
2018-01-05 17:42:09 +02:00
|
|
|
const _createSnapshot =
|
|
|
|
!loading && createSnapshotOpen ? (
|
2018-02-01 12:38:12 +02:00
|
|
|
<Margin bottom={4}>
|
|
|
|
<ReduxForm form={CREATE_FORM_NAME} onSubmit={handleCreateSnapshot}>
|
|
|
|
{props => <SnapshotAddForm {...props} onCancel={() => toggleCreateSnapshotOpen(false)} />}
|
|
|
|
</ReduxForm>
|
|
|
|
</Margin>
|
2018-01-05 17:42:09 +02:00
|
|
|
) : null;
|
|
|
|
|
|
|
|
const _footer =
|
|
|
|
!loading && selected.length > 0 ? (
|
|
|
|
<SnapshotsListActions
|
|
|
|
submitting={submitting}
|
|
|
|
allowedActions={allowedActions}
|
|
|
|
statuses={statuses}
|
|
|
|
onStart={() => handleStart(selected)}
|
|
|
|
onRemove={() => handleRemove(selected)}
|
|
|
|
/>
|
|
|
|
) : null;
|
|
|
|
|
|
|
|
const _mutationError = mutationError ? (
|
2018-02-01 12:38:12 +02:00
|
|
|
<Margin bottom={4}>
|
|
|
|
<Message error>
|
|
|
|
<MessageTitle>Ooops!</MessageTitle>
|
|
|
|
<MessageDescription>{mutationError}</MessageDescription>
|
|
|
|
</Message>
|
|
|
|
</Margin>
|
2018-01-05 17:42:09 +02:00
|
|
|
) : null;
|
|
|
|
|
|
|
|
const _items = !_loading ? (
|
|
|
|
<ReduxForm form={TABLE_FORM_NAME}>
|
|
|
|
{props => (
|
|
|
|
<SnapshotsList
|
|
|
|
snapshots={_values}
|
|
|
|
onStart={snapshot => handleStart([snapshot])}
|
|
|
|
onRemove={snapshot => handleRemove([snapshot])}
|
|
|
|
selected={selected}
|
|
|
|
sortBy={sortBy}
|
|
|
|
sortOrder={sortOrder}
|
|
|
|
onSortBy={handleSortBy}
|
|
|
|
toggleSelectAll={toggleSelectAll}
|
|
|
|
allSelected={_values.length && selected.length === _values.length}
|
|
|
|
/>
|
|
|
|
)}
|
|
|
|
</ReduxForm>
|
|
|
|
) : null;
|
|
|
|
|
2017-09-20 12:30:53 +03:00
|
|
|
return (
|
2017-10-13 22:56:08 +03:00
|
|
|
<ViewContainer main>
|
2018-01-05 17:42:09 +02:00
|
|
|
<ReduxForm form={MENU_FORM_NAME}>
|
|
|
|
{props => (
|
|
|
|
<ToolbarForm
|
|
|
|
{...props}
|
|
|
|
searchLabel="Filter Snapshots"
|
|
|
|
searchPlaceholder="Search for name, created...."
|
|
|
|
searchable={!_loading}
|
|
|
|
actionLabel="Create Snapshot"
|
|
|
|
actionable={!createSnapshotOpen}
|
|
|
|
onActionClick={() => toggleCreateSnapshotOpen(true)}
|
|
|
|
/>
|
|
|
|
)}
|
|
|
|
</ReduxForm>
|
|
|
|
{_loading}
|
2017-09-20 12:30:53 +03:00
|
|
|
{_error}
|
2018-01-05 17:42:09 +02:00
|
|
|
{_mutationError}
|
|
|
|
{_createSnapshot}
|
|
|
|
{_items}
|
|
|
|
{_footer}
|
2017-09-20 12:30:53 +03:00
|
|
|
</ViewContainer>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
export default compose(
|
2017-10-13 22:56:08 +03:00
|
|
|
graphql(StartSnapshot, { name: 'start' }),
|
|
|
|
graphql(RemoveSnapshot, { name: 'remove' }),
|
2018-01-05 17:42:09 +02:00
|
|
|
graphql(CreateSnapshotMutation, { name: 'createSnapshot' }),
|
2017-09-20 12:30:53 +03:00
|
|
|
graphql(GetSnapshots, {
|
|
|
|
options: ({ match }) => ({
|
|
|
|
pollInterval: 1000,
|
|
|
|
variables: {
|
|
|
|
name: get(match, 'params.instance')
|
|
|
|
}
|
|
|
|
}),
|
2018-01-05 17:42:09 +02:00
|
|
|
props: ({ data: { loading, error, variables, refetch, ...rest } }) => {
|
2017-10-13 22:56:08 +03:00
|
|
|
const { name } = variables;
|
|
|
|
const instance = find(get(rest, 'machines', []), ['name', name]);
|
|
|
|
|
2018-01-05 18:35:26 +02:00
|
|
|
const snapshots = get(instance, 'snapshots', []);
|
2017-10-13 22:56:08 +03:00
|
|
|
|
2018-02-01 12:38:12 +02:00
|
|
|
const index = GenIndex(snapshots.map(({ name, ...rest }) => ({ ...rest, id: name })));
|
2017-10-13 22:56:08 +03:00
|
|
|
|
|
|
|
return {
|
|
|
|
index,
|
|
|
|
snapshots,
|
|
|
|
instance,
|
|
|
|
loading,
|
2018-01-05 17:42:09 +02:00
|
|
|
error,
|
|
|
|
refetch
|
2017-10-13 22:56:08 +03:00
|
|
|
};
|
|
|
|
}
|
|
|
|
}),
|
|
|
|
connect(
|
2018-01-05 17:42:09 +02:00
|
|
|
({ form, values }, { index, snapshots = [], ...rest }) => {
|
|
|
|
const tableValues = get(form, `${TABLE_FORM_NAME}.values`) || {};
|
|
|
|
const filter = get(form, `${MENU_FORM_NAME}.values.filter`, false);
|
2017-10-13 22:56:08 +03:00
|
|
|
|
2018-01-05 17:42:09 +02:00
|
|
|
// check whether the table form has an error
|
|
|
|
const tableMutationError = get(form, `${TABLE_FORM_NAME}.error`, null);
|
|
|
|
// check whether the create form has an error
|
|
|
|
const createMutationError = get(form, `${CREATE_FORM_NAME}.error`, null);
|
|
|
|
// check whether the main form is submitting
|
|
|
|
const submitting = get(form, `${TABLE_FORM_NAME}.submitting`, false);
|
|
|
|
|
|
|
|
const selected = Object.keys(tableValues)
|
|
|
|
.filter(key => Boolean(tableValues[key]))
|
|
|
|
.map(name => find(snapshots, ['name', name]))
|
|
|
|
.filter(Boolean);
|
|
|
|
|
|
|
|
const sortBy = get(values, 'snapshots-list-sort-by', 'name');
|
|
|
|
const sortOrder = get(values, 'snapshots-list-sort-order', 'asc');
|
|
|
|
const createSnapshotOpen = get(values, 'snapshots-create-open', false);
|
|
|
|
|
|
|
|
// if user is searching something, get items that match that query
|
|
|
|
const filtered = filter
|
2017-10-13 22:56:08 +03:00
|
|
|
? index.search(filter).map(({ ref }) => find(snapshots, ['name', ref]))
|
|
|
|
: snapshots;
|
|
|
|
|
2018-01-05 17:42:09 +02:00
|
|
|
// 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)
|
|
|
|
}));
|
|
|
|
|
|
|
|
const allowedActions = {
|
|
|
|
start: selected.length === 1,
|
|
|
|
remove: true
|
|
|
|
};
|
|
|
|
|
|
|
|
// get mutating statuses
|
|
|
|
const statuses = {
|
|
|
|
starting: get(values, 'snapshot-list-starting', false),
|
|
|
|
removing: get(values, 'snapshot-list-removeing', false)
|
|
|
|
};
|
2017-10-13 22:56:08 +03:00
|
|
|
|
|
|
|
return {
|
|
|
|
...rest,
|
2018-01-30 01:20:22 +02:00
|
|
|
snapshots: sortOrder === 'asc' ? ascSorted : reverse(ascSorted),
|
2018-01-05 17:42:09 +02:00
|
|
|
selected,
|
|
|
|
sortBy,
|
|
|
|
sortOrder,
|
|
|
|
submitting,
|
|
|
|
mutationError: tableMutationError || createMutationError,
|
|
|
|
allowedActions,
|
|
|
|
statuses,
|
|
|
|
createSnapshotOpen
|
2017-10-13 22:56:08 +03:00
|
|
|
};
|
|
|
|
},
|
2018-01-05 17:42:09 +02:00
|
|
|
(dispatch, ownProps) => {
|
2018-01-05 18:35:26 +02:00
|
|
|
const { instance, createSnapshot, refetch } = ownProps;
|
2017-10-13 22:56:08 +03:00
|
|
|
|
2018-01-05 17:42:09 +02:00
|
|
|
return {
|
|
|
|
handleSortBy: (newSortBy, sortOrder) => {
|
|
|
|
dispatch([
|
|
|
|
set({
|
|
|
|
name: `snapshots-list-sort-order`,
|
|
|
|
value: sortOrder === 'desc' ? 'asc' : 'desc'
|
|
|
|
}),
|
|
|
|
set({
|
|
|
|
name: `snapshots-list-sort-by`,
|
|
|
|
value: newSortBy
|
|
|
|
})
|
|
|
|
]);
|
|
|
|
},
|
|
|
|
toggleCreateSnapshotOpen: value =>
|
2017-10-13 22:56:08 +03:00
|
|
|
dispatch(
|
2018-01-05 17:42:09 +02:00
|
|
|
set({
|
|
|
|
name: `snapshots-create-open`,
|
|
|
|
value
|
|
|
|
})
|
|
|
|
),
|
|
|
|
toggleSelectAll: ({ selected = [], snapshots = [] }) => () => {
|
|
|
|
const same = selected.length === snapshots.length;
|
|
|
|
const hasSelected = selected.length > 0;
|
|
|
|
|
|
|
|
// none are selected, toggle to all
|
|
|
|
if (!hasSelected) {
|
2018-02-01 12:38:12 +02:00
|
|
|
return dispatch(snapshots.map(({ name }) => change(TABLE_FORM_NAME, name, true)));
|
2018-01-05 17:42:09 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// all are selected, toggle to none
|
|
|
|
if (hasSelected && same) {
|
2018-02-01 12:38:12 +02:00
|
|
|
return dispatch(snapshots.map(({ name }) => change(TABLE_FORM_NAME, name, false)));
|
2018-01-05 17:42:09 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// some are selected, toggle to all
|
|
|
|
if (hasSelected && !same) {
|
2018-02-01 12:38:12 +02:00
|
|
|
return dispatch(snapshots.map(({ name }) => change(TABLE_FORM_NAME, name, true)));
|
2018-01-05 17:42:09 +02:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
handleCreateSnapshot: async ({ name }) => {
|
|
|
|
const [err] = await intercept(
|
|
|
|
createSnapshot({
|
|
|
|
variables: { name, id: instance.id }
|
2017-10-13 22:56:08 +03:00
|
|
|
})
|
|
|
|
);
|
|
|
|
|
2018-01-05 17:42:09 +02:00
|
|
|
if (err) {
|
|
|
|
return dispatch(
|
2018-01-04 20:26:28 +02:00
|
|
|
stopSubmit(TABLE_FORM_NAME, {
|
|
|
|
_error: parseError(err)
|
2018-01-05 17:42:09 +02:00
|
|
|
})
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2017-10-13 22:56:08 +03:00
|
|
|
dispatch(
|
2018-01-05 17:42:09 +02:00
|
|
|
set({
|
|
|
|
name: `snapshots-create-open`,
|
|
|
|
value: false
|
|
|
|
})
|
2017-10-13 22:56:08 +03:00
|
|
|
);
|
2018-01-05 17:42:09 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
handleAction: async ({ name, selected = [] }) => {
|
|
|
|
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: `snapshot-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(
|
2018-02-01 12:38:12 +02:00
|
|
|
Promise.resolve(dispatch([flipSubmitTrue, setIngTrue, ...setMutatingTrue])).then(() => {
|
2018-01-05 17:42:09 +02:00
|
|
|
// starts all the mutations for all the selected items
|
|
|
|
return Promise.all(
|
|
|
|
selected.map(({ name }) =>
|
|
|
|
action({ variables: { id: instance.id, snapshot: name } })
|
|
|
|
)
|
|
|
|
);
|
|
|
|
})
|
|
|
|
);
|
|
|
|
|
|
|
|
// 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: `snapshot-list-${gerund}`,
|
|
|
|
value: false
|
|
|
|
});
|
|
|
|
|
|
|
|
// reverts the individual item mutation flags
|
|
|
|
// when action === remove, let it stay spinning
|
|
|
|
const setMutatingFalse =
|
|
|
|
name !== 'remove' &&
|
2018-02-01 12:38:12 +02:00
|
|
|
selected.map(({ id }) => set({ name: `${id}-mutating`, value: false }));
|
2018-01-05 17:42:09 +02:00
|
|
|
|
2018-02-01 12:38:12 +02:00
|
|
|
const actions = [flipSubmitFalse, clearSelected, setIngFalse, ...setMutatingFalse].filter(
|
|
|
|
Boolean
|
|
|
|
);
|
2018-01-05 17:42:09 +02:00
|
|
|
|
|
|
|
// refetch list - even though we poll anyway - after clearing everything
|
|
|
|
return Promise.resolve(dispatch(actions)).then(() => refetch());
|
|
|
|
}
|
|
|
|
};
|
|
|
|
},
|
|
|
|
(stateProps, dispatchProps, ownProps) => {
|
2018-01-05 18:35:26 +02:00
|
|
|
const { selected, snapshots } = stateProps;
|
2018-01-05 17:42:09 +02:00
|
|
|
const { toggleSelectAll } = dispatchProps;
|
|
|
|
|
|
|
|
return {
|
|
|
|
...ownProps,
|
|
|
|
...stateProps,
|
|
|
|
selected,
|
|
|
|
snapshots,
|
|
|
|
...dispatchProps,
|
|
|
|
toggleSelectAll: toggleSelectAll({ selected, snapshots })
|
|
|
|
};
|
|
|
|
}
|
2017-10-13 22:56:08 +03:00
|
|
|
)
|
2017-09-20 12:30:53 +03:00
|
|
|
)(Snapshots);
|