From 2538453d98672ad90d056546dfdc866f42ad4d5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81rgio=20Ramos?= Date: Wed, 6 Dec 2017 18:16:11 +0000 Subject: [PATCH] feat(my-joy-beta): revise Metadata and KeyValue implementations fixes #908 --- .../src/components/instances/key-value.js | 328 ++++++++------- .../src/components/instances/metadata.js | 47 +++ .../src/containers/instances/list.js | 7 +- .../src/containers/instances/metadata.js | 374 ++++++++++-------- packages/ui-toolkit/src/form/group.js | 4 +- 5 files changed, 436 insertions(+), 324 deletions(-) create mode 100644 packages/my-joy-beta/src/components/instances/metadata.js diff --git a/packages/my-joy-beta/src/components/instances/key-value.js b/packages/my-joy-beta/src/components/instances/key-value.js index f1c95e45..3c2b745b 100644 --- a/packages/my-joy-beta/src/components/instances/key-value.js +++ b/packages/my-joy-beta/src/components/instances/key-value.js @@ -1,4 +1,6 @@ import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; +import { withTheme } from 'styled-components'; import { Row, Col } from 'react-styled-flexboxgrid'; import Value from 'react-redux-values'; import { Field } from 'redux-form'; @@ -6,6 +8,7 @@ import styled from 'styled-components'; import remcalc from 'remcalc'; import titleCase from 'title-case'; import { Margin, Padding } from 'styled-components-spacing'; +import Flex, { FlexItem } from 'styled-flex-component'; import Editor from 'joyent-ui-toolkit/dist/es/editor'; import { @@ -25,7 +28,9 @@ import { FormMeta, Button, Textarea, - Divider + Editor, + Divider, + DeleteIcon } from 'joyent-ui-toolkit'; const CollapsedKeyValue = styled.span` @@ -49,155 +54,180 @@ class ValueTextareaField extends PureComponent { } } -const KeyValue = ({ - id, - label = '', - textarea, - create, - last, - first, - expanded, - removing, - pristine, - error, - submitting, - onRemove, - onToggleExpanded, - handleSubmit, - onClear -}) => { - const _error = error && - !submitting && ( - - Ooops! - {error} - +const TextareaKeyValue = ({ type, submitting }) => [ + + + + {titleCase(type)} key + + + + + + , + + + + {titleCase(type)} value + + + + + + +]; + +const InputKeyValue = ({ type, submitting }) => ( + + + + {titleCase(type)} key + + + + + + + {titleCase(type)} value + + + + + +); + +const KeyValue = withTheme( + ({ + input = 'input', + type = 'metadata', + method = 'add', + error = null, + expanded = true, + submitting = false, + pristine = true, + removing = false, + handleSubmit, + onToggleExpanded = () => null, + onCancel = () => null, + onRemove = () => null, + theme + }) => { + const handleHeaderClick = method === 'edit' && onToggleExpanded; + + return ( +
+ + + + {method === 'add' ? ( +

{`${titleCase(method)} ${type}`}

+ ) : ( + + + expanded ? ( + `${input.value}: ` + ) : ( + {`${input.value}: `} + ) + } + /> + input.value} + /> + + )} +
+
+ + + {error && !submitting ? ( + + + + Ooops! + {error} + + + + ) : null} + {input === 'input' ? ( + + ) : ( + + )} + + + + + + + + + + + +
+ + ); + } +); - const _meta = expanded ? ( -

{create ? `Create ${label}` : `Edit ${label}`}

- ) : ( - - {`${input.value}: `}} - /> - input.value} /> - - ); - - const chevronToggle = create ? null : ( - - - - ); - - const _valueField = textarea ? ( - - ) : ( - - ); - - const _cancel = ( - - ); - - const _submit = ( - - ); - - return ( -
- - - - - {_meta} - - {chevronToggle} - - - - - {_error} - - - - - Enter {titleCase(label)} key - - - - - - - Enter {titleCase(label)} value - {_valueField} - - - - - - - {_cancel} - {_submit} - - - - - - - - - ); +KeyValue.propTypes = { + input: PropTypes.oneOf(['input', 'textarea']).isRequired, + type: PropTypes.string.isRequired, + method: PropTypes.oneOf(['add', 'edit']).isRequired, + removing: PropTypes.bool.isRequired, + expanded: PropTypes.bool.isRequired, + onToggleExpanded: PropTypes.func, + onCancel: PropTypes.func, + onRemove: PropTypes.func }; -export default ({ id, ...rest }) => ( - - {({ value: removing }) => ( - - )} - -); +export default props => ; diff --git a/packages/my-joy-beta/src/components/instances/metadata.js b/packages/my-joy-beta/src/components/instances/metadata.js new file mode 100644 index 00000000..4d57054f --- /dev/null +++ b/packages/my-joy-beta/src/components/instances/metadata.js @@ -0,0 +1,47 @@ +import React from 'react'; +import KeyValue from './key-value'; + +import { + Row, + Col, + FormGroup, + Input, + FormLabel, + Button +} from 'joyent-ui-toolkit'; + +export const MenuForm = ({ searchable, onAdd }) => ( +
+ + + + Filter + + + + + + + + + + +
+); + +export const AddForm = props => ( + +); + +export const EditForm = props => ( + +); diff --git a/packages/my-joy-beta/src/containers/instances/list.js b/packages/my-joy-beta/src/containers/instances/list.js index b07816f1..0fce5920 100644 --- a/packages/my-joy-beta/src/containers/instances/list.js +++ b/packages/my-joy-beta/src/containers/instances/list.js @@ -30,6 +30,7 @@ import DisableInstanceFw from '@graphql/disable-instance-fw.gql'; import CreateSnapshot from '@graphql/create-snapshot.gql'; import StartSnapshot from '@graphql/start-from-snapshot.gql'; import Index from '@state/gen-index'; +import parseError from '@state/parse-error'; import { default as InstanceList, @@ -264,12 +265,6 @@ export default compose( }) ); - // parses the error to handle existance or not of graphQLErrors - const parseError = ({ graphQLErrors = [], message = '' }) => - graphQLErrors.length - ? graphQLErrors.map(({ message }) => message).join('\n') - : message; - // reverts submitting flag to false and propagates the error if it exists const flipSubmitFalse = stopSubmit(TABLE_FORM_NAME, { _error: err && parseError(err) diff --git a/packages/my-joy-beta/src/containers/instances/metadata.js b/packages/my-joy-beta/src/containers/instances/metadata.js index 04f61943..afc2c1a1 100644 --- a/packages/my-joy-beta/src/containers/instances/metadata.js +++ b/packages/my-joy-beta/src/containers/instances/metadata.js @@ -6,115 +6,121 @@ import { connect } from 'react-redux'; import { SubmissionError, reset, startSubmit, stopSubmit } from 'redux-form'; import ReduxForm from 'declarative-redux-form'; import find from 'lodash.find'; -import sortBy from 'lodash.sortby'; import get from 'lodash.get'; +import intercept from 'apr-intercept'; +import remcalc from 'remcalc'; import { ViewContainer, + Title, StatusLoader, Message, MessageDescription, MessageTitle, - Button + Button, + Divider, + H3 } from 'joyent-ui-toolkit'; import GetMetadata from '@graphql/list-metadata.gql'; import UpdateMetadata from '@graphql/update-metadata.gql'; import DeleteMetadata from '@graphql/delete-metadata.gql'; import { KeyValue } from '@components/instances'; +import parseError from '@state/parse-error'; -const METADATA_FORM_KEY = (name, field) => `instance-metadata-${name}-${field}`; -const CREATE_METADATA_FORM_KEY = name => `instance-create-metadata-${name}`; +import { + MenuForm as MetadataMenuForm, + AddForm as MetadataAddForm, + EditForm as MetadataEditForm +} from '@components/instances/metadata'; + +const MENU_FORM_NAME = 'instance-metadata-list-menu'; +const ADD_FORM_NAME = 'instance-metadata-add-new'; +const METADATA_FORM_KEY = field => `instance-metadata-${paramCase(field)}`; const Metadata = ({ instance, - values = [], + metadata = [], + addOpen, loading, error, - handleRemove, - handleClear, + handleToggleAddOpen, + handleUpdateExpanded, + handleCancel, + handleCreate, handleUpdate, - handleCreate + handleRemove }) => { - const _loading = !(loading && !values.length) ? null : ; + const _loading = !(loading && !metadata.length) ? null : ; + + const _add = addOpen ? ( + handleToggleAddOpen(false)} + > + {MetadataAddForm} + + ) : null; + + const _line = !_loading + ? [ + , + + ] + : null; + + const _count = !_loading ? ( +

+ {metadata.length} key:value pair +

+ ) : null; - // metadata items forms const _metadata = !_loading && - values.map(({ form, initialValues }, i) => ( - - {({ value: expanded, onValueChange }) => ( - handleUpdate(newValues, form)} - destroyOnUnmount - id={form} - onClear={() => handleClear(form)} - onToggleExpanded={() => onValueChange(!expanded)} - onRemove={() => handleRemove(form)} - label="metadata" - last={values.length - 1 === i} - first={i === 0} - expanded={expanded} - textarea - > - {KeyValue} - - )} - + metadata.map(({ form, initialValues, expanded, removing }) => ( + handleUpdateExpanded(form, !expanded)} + onCancel={() => handleCancel(form)} + onRemove={() => handleRemove(form)} + expanded={expanded} + removing={removing} + > + {MetadataEditForm} + )); - // create metadata form - const _addKey = instance && CREATE_METADATA_FORM_KEY(instance.name); - const _add = _metadata && - _addKey && ( - - {({ value: expanded, onValueChange }) => - !expanded ? ( - - ) : ( - handleClear(_addKey)} - onToggleExpanded={() => onValueChange(!expanded)} - onRemove={() => handleRemove(_addKey)} - expanded={expanded} - label="metadata" - create - textarea - > - {KeyValue} - - )} - - ); - - // fetching error const _error = - error && !values.length && !_loading ? ( + error && !_metadata.length && !_loading ? ( Ooops! - An error occurred while loading your instance metadata + An error occurred while loading your metadata ) : null; return ( - - {_loading} + + handleToggleAddOpen(!addOpen)} + > + {MetadataMenuForm} + + + {_line} {_error} - {_metadata} + {_loading} {_add} + {_count} + {_metadata} ); }; @@ -124,7 +130,7 @@ export default compose( graphql(DeleteMetadata, { name: 'deleteMetadata' }), graphql(GetMetadata, { options: ({ match }) => ({ - pollInterval: 1000, + // pollInterval: 1000, variables: { name: get(match, 'params.instance') } @@ -133,23 +139,18 @@ export default compose( const { name } = variables; const instance = find(get(rest, 'machines', []), ['name', name]); - const metadata = get(instance, 'metadata', []); + const values = get(instance, 'metadata', []); - const values = sortBy(metadata, 'name').map(({ name, value }) => { - const field = paramCase(name); - const form = METADATA_FORM_KEY(name, field); - - return { - form, - initialValues: { - name, - value - } - }; - }); + const metadata = values.map(({ name, value }) => ({ + form: METADATA_FORM_KEY(name), + initialValues: { + name, + value + } + })); return { - values, + metadata, instance, loading, error, @@ -157,97 +158,136 @@ export default compose( }; } }), - connect(null, (dispatch, ownProps) => { - const { - instance, - values, - refetch, - updateMetadata, - deleteMetadata - } = ownProps; + connect( + ({ values }, { metadata, ownProps }) => ({ + ...ownProps, + addOpen: get(values, 'add-metadata-open', false), + metadata: metadata.map(({ form, ...metadata }) => ({ + ...metadata, + form, + expanded: get(values, `${form}-expanded`, false), + removing: get(values, `${form}-removing`, false) + })) + }), + (dispatch, ownProps) => { + const { + instance, + metadata, + updateMetadata, + deleteMetadata, + refetch + } = ownProps; - return { - // reset sets values to initialValues - handleClear: form => dispatch(reset(form)), - handleRemove: form => - Promise.resolve( - // set removing=true (so that we can have a specific removing spinner) - // because remove button is not a submit button, we have to manually flip that flag + return { + handleCancel: form => + dispatch([ + set({ name: `${form}-expanded`, value: false }), + dispatch(reset(form)) + ]), + handleToggleAddOpen: value => + dispatch(set({ name: `add-metadata-open`, value })), + handleUpdateExpanded: (form, expanded) => + dispatch(set({ name: `${form}-expanded`, value: expanded })), + handleCreate: async ({ name, value }) => { + // call mutation + const [err] = await intercept( + updateMetadata({ + variables: { + id: instance.id, + metadata: [{ name, value }] + } + }) + ); + + if (err) { + // show mutation error + throw new SubmissionError({ + _error: parseError(err) + }); + } + + dispatch([ + // reset create new metadata form + reset(ADD_FORM_NAME), + stopSubmit(ADD_FORM_NAME), + // close add form + set({ name: `add-metadata-open`, value: false }) + ]); + + // fetch metadata again (even though we are polling) + return refetch(); + }, + handleUpdate: async ({ name, value }, _, { form }) => { + // call mutations + const [err] = await intercept( + Promise.all([ + deleteMetadata({ + variables: { + id: instance.id, + name: get( + find(metadata, ['form', form]), + 'initialValues.name' + ) + } + }), + updateMetadata({ + variables: { + id: instance.id, + metadata: [{ name, value }] + } + }) + ]) + ); + + if (err) { + // show mutation error + throw new SubmissionError({ + _error: parseError(err) + }); + } + + dispatch([ + // reset form + stopSubmit(form), + // close card + set({ name: `${form}-expanded`, value: false }) + ]); + + // fetch metadata again (even though we are polling) + return refetch(); + }, + handleRemove: async form => { dispatch([ set({ name: `${form}-removing`, value: true }), startSubmit(form) - ]) - ) - .then(() => - // call mutation. get key from values' initialValues + ]); + + // call mutation + const [err] = await intercept( deleteMetadata({ variables: { id: instance.id, - name: get(find(values, ['form', form]), 'initialValues.name') + name: get(find(metadata, ['form', form]), 'initialValues.name') } }) - ) - // fetch metadata again - .then(() => refetch()) - // we only flip removing and submitting when there is an error. - // the reason for that is that metadata is updated asyncronously and - // it takes longer to have an efect than the mutation - .catch(error => - dispatch([ - set({ name: `${form}-removing`, value: false }), - stopSubmit(form, { - _error: error.graphQLErrors - .map(({ message }) => message) - .join('\n') - }) - ]) - ), - handleUpdate: ({ name, value }, form) => - // call mutation. delete existing metadata, add new - Promise.all([ - deleteMetadata({ - variables: { - id: instance.id, - name: get(find(values, ['form', form]), 'initialValues.name') - } - }), - updateMetadata({ - variables: { - id: instance.id, - metadata: [{ name, value }] - } - }) - ]) - // fetch metadata again - .then(() => refetch()) - // submit is flipped once the promise is resolved - .catch(error => { + ); + + if (err) { + // show mutation error throw new SubmissionError({ - _error: error.graphQLErrors - .map(({ message }) => message) - .join('\n') + _error: parseError(err) }); - }), - handleCreate: ({ name, value }) => - // call mutation - updateMetadata({ - variables: { - id: instance.id, - metadata: [{ name, value }] } - }) - // fetch metadata again - .then(() => refetch()) - // reset create new metadata form - .then(() => dispatch(reset(CREATE_METADATA_FORM_KEY(instance.name)))) - // submit is flipped once the promise is resolved - .catch(error => { - throw new SubmissionError({ - _error: error.graphQLErrors - .map(({ message }) => message) - .join('\n') - }); - }) - }; - }) + + dispatch([ + stopSubmit(form), + set({ name: `${form}-removing`, value: false }) + ]); + + // fetch metadata again (even though we are polling) + return refetch(); + } + }; + } + ) )(Metadata); diff --git a/packages/ui-toolkit/src/form/group.js b/packages/ui-toolkit/src/form/group.js index 815c35ab..f18affb8 100644 --- a/packages/ui-toolkit/src/form/group.js +++ b/packages/ui-toolkit/src/form/group.js @@ -28,7 +28,7 @@ class FormGroup extends Component { } renderGroup(inputProps) { - const { className, style, children, ...rest } = this.props; + const { className, style, children, fluid = false, ...rest } = this.props; const value = { id: rndId(), @@ -37,7 +37,7 @@ class FormGroup extends Component { }; return ( - + {children}