diff --git a/packages/cloudapi-gql/src/schema/resolvers.js b/packages/cloudapi-gql/src/schema/resolvers.js
index f6daa5eb..3b00cc93 100644
--- a/packages/cloudapi-gql/src/schema/resolvers.js
+++ b/packages/cloudapi-gql/src/schema/resolvers.js
@@ -229,7 +229,13 @@ const resolvers = {
startMachineFromSnapshot: (root, { id, name }) =>
api.machines.snapshots
.startFromSnapshot({ id, name })
- .then(() => resolvers.Query.machine(null, { id }))
+ .then(() => resolvers.Query.machine(null, { id })),
+
+ deleteMachineSnapshot: async (root, { id, snapshot: name }) => {
+ const snapshot = await api.machines.snapshots.get({ id, name });
+ await api.machines.snapshots.destroy({ id, name });
+ return snapshot;
+ }
}
};
diff --git a/packages/my-joy-beta/package.json b/packages/my-joy-beta/package.json
index 026301ae..7ad6c957 100644
--- a/packages/my-joy-beta/package.json
+++ b/packages/my-joy-beta/package.json
@@ -25,6 +25,7 @@
"lodash.isstring": "^4.0.1",
"lodash.sortby": "^4.7.0",
"lunr": "^2.1.3",
+ "moment": "^2.19.1",
"normalized-styled-components": "^1.0.17",
"param-case": "^2.1.1",
"prop-types": "^15.6.0",
diff --git a/packages/my-joy-beta/src/components/instances/index.js b/packages/my-joy-beta/src/components/instances/index.js
index 3f888caf..115137b4 100644
--- a/packages/my-joy-beta/src/components/instances/index.js
+++ b/packages/my-joy-beta/src/components/instances/index.js
@@ -5,3 +5,5 @@ export { default as Network } from './network';
export { default as FirewallRule } from './firewall-rule';
export { default as Resize } from './resize';
export { default as CreateSnapshot } from './create-snapshot';
+export { default as Snapshots } from './snapshots';
+export { default as Snapshot } from './snapshot';
diff --git a/packages/my-joy-beta/src/components/instances/snapshot.js b/packages/my-joy-beta/src/components/instances/snapshot.js
new file mode 100644
index 00000000..d11dbad8
--- /dev/null
+++ b/packages/my-joy-beta/src/components/instances/snapshot.js
@@ -0,0 +1,56 @@
+import React from 'react';
+import titleCase from 'title-case';
+import moment from 'moment';
+
+import {
+ Card,
+ CardMeta,
+ CardAction,
+ CardTitle,
+ CardLabel,
+ CardView,
+ Checkbox,
+ FormGroup,
+ QueryBreakpoints,
+ StatusLoader
+} from 'joyent-ui-toolkit';
+
+const { SmallOnly, Small } = QueryBreakpoints;
+
+const stateColor = {
+ QUEUED: 'blue',
+ CANCELED: 'grey',
+ FAILED: 'red',
+ CREATED: 'green'
+};
+
+export default ({ name, state, created, loading, last, first }) => (
+
+
+
+
+
+
+
+
+ {name}
+ {moment.unix(created).fromNow()}
+ {loading && (
+
+
+
+ )}
+ {!loading && (
+
+ {titleCase(state)}
+
+ )}
+ {!loading && (
+
+
+
+ )}
+
+
+
+);
diff --git a/packages/my-joy-beta/src/components/instances/snapshots.js b/packages/my-joy-beta/src/components/instances/snapshots.js
new file mode 100644
index 00000000..2740cb6d
--- /dev/null
+++ b/packages/my-joy-beta/src/components/instances/snapshots.js
@@ -0,0 +1,162 @@
+import React from 'react';
+import { Row, Col } from 'react-styled-flexboxgrid';
+import forceArray from 'force-array';
+import find from 'lodash.find';
+
+import {
+ FormGroup,
+ Input,
+ FormLabel,
+ ViewContainer,
+ StatusLoader,
+ Select,
+ Message,
+ MessageTitle,
+ MessageDescription,
+ Button,
+ QueryBreakpoints
+} from 'joyent-ui-toolkit';
+
+import Item from './snapshot';
+
+const { SmallOnly, Medium } = QueryBreakpoints;
+
+export default ({
+ snapshots = [],
+ selected = [],
+ loading,
+ error,
+ handleChange = () => null,
+ onAction = () => null,
+ handleSubmit,
+ submitting = false,
+ pristine = true,
+ ...rest
+}) => {
+ const allowedActions = {
+ delete: selected.length > 0,
+ start: selected.length === 1
+ };
+
+ const handleActions = ev => {
+ ev.stopPropagation();
+ ev.preventDefault();
+
+ onAction({
+ name: ev.target.value,
+ items: selected
+ });
+ };
+
+ const _snapshots = forceArray(snapshots);
+
+ const _loading = !_snapshots.length &&
+ loading && (
+
+
+
+ );
+
+ const items = _snapshots.map((snapshot, i, all) => {
+ const { name } = snapshot;
+
+ const isSelected = Boolean(find(selected, ['name', name]));
+ const isSubmitting = isSelected && submitting;
+
+ return (
+
+ );
+ });
+
+ const _error = error &&
+ !submitting && (
+
+ Ooops!
+ {error}
+
+ );
+
+ return (
+
+ );
+};
diff --git a/packages/my-joy-beta/src/containers/instances/snapshots.js b/packages/my-joy-beta/src/containers/instances/snapshots.js
index c5805543..8417e643 100644
--- a/packages/my-joy-beta/src/containers/instances/snapshots.js
+++ b/packages/my-joy-beta/src/containers/instances/snapshots.js
@@ -1,31 +1,50 @@
import React from 'react';
-import ReactJson from 'react-json-view';
-import PropTypes from 'prop-types';
+import moment from 'moment';
import forceArray from 'force-array';
+import { connect } from 'react-redux';
import { compose, graphql } from 'react-apollo';
import find from 'lodash.find';
+import sortBy from 'lodash.sortby';
import get from 'lodash.get';
+import { reduxForm, stopSubmit, startSubmit, change } from 'redux-form';
+
import {
ViewContainer,
Title,
- StatusLoader,
Message,
MessageTitle,
MessageDescription
} from 'joyent-ui-toolkit';
import GetSnapshots from '@graphql/list-snapshots.gql';
+import StartSnapshot from '@graphql/start-from-snapshot.gql';
+import RemoveSnapshot from '@graphql/remove-snapshot.gql';
+import { Snapshots as SnapshotsList } from '@components/instances';
+import GenIndex from '@state/gen-index';
-const Snapshots = ({ snapshots = [], loading, error }) => {
+const SnapshotsListForm = reduxForm({
+ form: `snapshots-list`,
+ initialValues: {
+ sort: 'name'
+ }
+})(SnapshotsList);
+
+const Snapshots = ({
+ snapshots = [],
+ selected = [],
+ loading,
+ error,
+ handleAction
+}) => {
const _title = Snapshots;
- const _loading = !(loading && !forceArray(snapshots).length) ? null : (
-
- );
- const _summary = !_loading && ;
+ const _values = forceArray(snapshots);
+ const _loading = !_values.length && loading;
- const _error = !(error && !_loading) ? null : (
+ const _error = error &&
+ !_loading &&
+ !_values.length && (
Ooops!
@@ -35,20 +54,22 @@ const Snapshots = ({ snapshots = [], loading, error }) => {
);
return (
-
+
{_title}
- {_loading}
{_error}
- {_summary}
+
);
};
-Snapshots.propTypes = {
- loading: PropTypes.bool
-};
-
export default compose(
+ graphql(StartSnapshot, { name: 'start' }),
+ graphql(RemoveSnapshot, { name: 'remove' }),
graphql(GetSnapshots, {
options: ({ match }) => ({
pollInterval: 1000,
@@ -56,14 +77,108 @@ export default compose(
name: get(match, 'params.instance')
}
}),
- props: ({ data: { loading, error, variables, ...rest } }) => ({
- snapshots: get(
- find(get(rest, 'machines', []), ['name', variables.name]),
+ props: ({ data: { loading, error, variables, ...rest } }) => {
+ const { name } = variables;
+ const instance = find(get(rest, 'machines', []), ['name', name]);
+
+ const snapshots = get(
+ instance,
'snapshots',
[]
- ),
- loading,
- error
+ ).map(({ created, updated, ...rest }) => ({
+ ...rest,
+ created: moment.utc(created).unix(),
+ updated: moment.utc(updated).unix()
+ }));
+
+ const index = GenIndex(
+ snapshots.map(({ name, ...rest }) => ({ ...rest, id: name }))
+ );
+
+ return {
+ index,
+ snapshots,
+ instance,
+ loading,
+ error
+ };
+ }
+ }),
+ connect(
+ (state, { index, snapshots = [], ...rest }) => {
+ const form = get(state, 'form.snapshots-list.values', {});
+ const filter = get(form, 'filter');
+ const sort = get(form, 'sort');
+
+ const values = filter
+ ? index.search(filter).map(({ ref }) => find(snapshots, ['name', ref]))
+ : snapshots;
+
+ const selected = Object.keys(form)
+ .filter(key => Boolean(form[key]))
+ .map(name => find(values, ['name', name]))
+ .filter(Boolean)
+ .map(({ name }) => find(snapshots, ['name', name]))
+ .filter(Boolean);
+
+ return {
+ ...rest,
+ snapshots: sortBy(values, value => get(value, sort)),
+ selected
+ };
+ },
+ (dispatch, { create, start, remove, instance, history, match }) => ({
+ handleAction: ({ name, items = [] }) => {
+ const form = 'snapshots-list';
+
+ const types = {
+ start: () =>
+ Promise.resolve(dispatch(startSubmit(form))).then(() =>
+ Promise.all(
+ items.map(({ name }) =>
+ start({ variables: { id: instance.id, snapshot: name } })
+ )
+ )
+ ),
+ delete: () =>
+ Promise.resolve(dispatch(startSubmit(form))).then(() =>
+ Promise.all(
+ items.map(({ name }) =>
+ remove({ variables: { id: instance.id, snapshot: name } })
+ )
+ )
+ ),
+ create: () =>
+ Promise.resolve(
+ history.push(`/instances/${instance.name}/snapshots/~create`)
+ )
+ };
+
+ const handleError = error => {
+ dispatch(
+ stopSubmit(form, {
+ _error: error.graphQLErrors
+ .map(({ message }) => message)
+ .join('\n')
+ })
+ );
+ };
+
+ const handleSuccess = () => {
+ dispatch(
+ items
+ .map(({ name: field }) => change(form, field, false))
+ .concat([stopSubmit(form)])
+ );
+ };
+
+ return (
+ types[name] &&
+ types[name]()
+ .then(handleSuccess)
+ .catch(handleError)
+ );
+ }
})
- })
+ )
)(Snapshots);
diff --git a/packages/my-joy-beta/src/graphql/list-snapshots.gql b/packages/my-joy-beta/src/graphql/list-snapshots.gql
index 64d9f4f9..953b12f8 100644
--- a/packages/my-joy-beta/src/graphql/list-snapshots.gql
+++ b/packages/my-joy-beta/src/graphql/list-snapshots.gql
@@ -5,6 +5,8 @@ query instance($name: String!) {
snapshots {
name
state
+ created
+ updated
}
}
}
diff --git a/packages/my-joy-beta/src/graphql/remove-snapshot.gql b/packages/my-joy-beta/src/graphql/remove-snapshot.gql
new file mode 100644
index 00000000..8c430fcb
--- /dev/null
+++ b/packages/my-joy-beta/src/graphql/remove-snapshot.gql
@@ -0,0 +1,5 @@
+mutation deleteMachineSnapshot($id: ID!, $snapshot: ID!) {
+ deleteMachineSnapshot(id: $id, snapshot: $snapshot) {
+ name
+ }
+}
diff --git a/packages/my-joy-beta/src/graphql/start-from-snapshot.gql b/packages/my-joy-beta/src/graphql/start-from-snapshot.gql
index 824ee90f..6bd8cecf 100644
--- a/packages/my-joy-beta/src/graphql/start-from-snapshot.gql
+++ b/packages/my-joy-beta/src/graphql/start-from-snapshot.gql
@@ -1,3 +1,5 @@
mutation startInstanceFromSnapshot($id: ID!, $snapshot: ID!) {
- startMachineFromSnapshot(id: $id, snapshot: $snapshot)
+ startMachineFromSnapshot(id: $id, snapshot: $snapshot) {
+ id
+ }
}
diff --git a/packages/my-joy-beta/src/router.js b/packages/my-joy-beta/src/router.js
index 90a6f4d3..af237633 100644
--- a/packages/my-joy-beta/src/router.js
+++ b/packages/my-joy-beta/src/router.js
@@ -112,7 +112,7 @@ export default () => (
component={InstanceCreateSnapshot}
/>
diff --git a/packages/ui-toolkit/src/breakpoints/index.js b/packages/ui-toolkit/src/breakpoints/index.js
index 19535924..6ebe602c 100644
--- a/packages/ui-toolkit/src/breakpoints/index.js
+++ b/packages/ui-toolkit/src/breakpoints/index.js
@@ -6,18 +6,18 @@ import pascalCase from 'pascal-case';
export const breakpoints = {
small: {
- upper: 768
+ upper: 767
},
medium: {
- upper: 1024,
- lower: 769
+ upper: 1023,
+ lower: 768
},
large: {
- upper: 1200,
- lower: 1025
+ upper: 1199,
+ lower: 1024
},
xlarge: {
- lower: 1201
+ lower: 1200
}
};
diff --git a/packages/ui-toolkit/src/button/index.js b/packages/ui-toolkit/src/button/index.js
index 00d6e243..9151a0d4c 100644
--- a/packages/ui-toolkit/src/button/index.js
+++ b/packages/ui-toolkit/src/button/index.js
@@ -17,8 +17,9 @@ const style = css`
justify-content: center;
align-items: center;
- margin: 0;
- padding: ${remcalc(15)} ${remcalc(18)};
+ margin-bottom: ${remcalc(8)};
+ margin-top: ${remcalc(8)};
+ padding: ${remcalc(13)} ${remcalc(18)};
position: relative;
${typography.normal};
@@ -154,8 +155,7 @@ const style = css`
`};
${is('small')`
- padding: ${remcalc(9)} ${remcalc(18)};
- font-weight: 600;
+ padding: ${remcalc(13)} ${remcalc(18)};
`};
${is('icon')`
@@ -167,6 +167,10 @@ const style = css`
width: 100%;
max-width: 100%;
`};
+
+ ${is('marginless')`
+ margin: 0;
+ `};
`;
const StyledButton = NButton.extend`