From 8cdf8070e860c97390efa05989fc2d4c71aaf148 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81rgio=20Ramos?= Date: Fri, 13 Oct 2017 20:56:08 +0100 Subject: [PATCH] feat: list, create and start snapshots --- packages/cloudapi-gql/src/schema/resolvers.js | 8 +- packages/my-joy-beta/package.json | 1 + .../src/components/instances/index.js | 2 + .../src/components/instances/snapshot.js | 56 ++++++ .../src/components/instances/snapshots.js | 162 ++++++++++++++++++ .../src/containers/instances/snapshots.js | 161 ++++++++++++++--- .../src/graphql/list-snapshots.gql | 2 + .../src/graphql/remove-snapshot.gql | 5 + .../src/graphql/start-from-snapshot.gql | 4 +- packages/my-joy-beta/src/router.js | 2 +- packages/ui-toolkit/src/breakpoints/index.js | 12 +- packages/ui-toolkit/src/button/index.js | 12 +- 12 files changed, 391 insertions(+), 36 deletions(-) create mode 100644 packages/my-joy-beta/src/components/instances/snapshot.js create mode 100644 packages/my-joy-beta/src/components/instances/snapshots.js create mode 100644 packages/my-joy-beta/src/graphql/remove-snapshot.gql 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 ( +
handleSubmit(ctx => handleChange(ctx))} + onSubmit={handleSubmit} + > + + + + + + Filter snapshots + + + + + + Sort + + + + + + + + + + + + + + + + + + + + + + + {_loading} + {_error} + {items} +
+ ); +}; 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`