import React, { Fragment } from 'react'; import paramCase from 'param-case'; import { Margin } from 'styled-components-spacing'; import { set } from 'react-redux-values'; import { compose, graphql } from 'react-apollo'; 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 get from 'lodash.get'; import intercept from 'apr-intercept'; import remcalc from 'remcalc'; import Fuse from 'fuse.js'; import { ViewContainer, StatusLoader, Message, MessageDescription, MessageTitle, 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 parseError from '@state/parse-error'; import ToolbarForm from '@components/instances/toolbar'; import Confirm from '@state/confirm'; import { addMetadata as validateMetadata } from '@state/validators'; import { 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)}`; export const Metadata = ({ metadata = [], addOpen, loading, error, shouldAsyncValidate, handleAsyncValidate, handleToggleAddOpen, handleUpdateExpanded, handleCancel, handleCreate, handleUpdate, handleRemove }) => { const _loading = loading && !metadata.length ? : null; const _add = addOpen ? ( {props => ( handleToggleAddOpen(false)} /> )} ) : null; const _line = _loading ? null : ( ); const _count = _loading ? null : (

{metadata.length} key-value pair{metadata.length === 1 ? '' : 's'}

); const _metadata = !_loading && metadata.map(({ form, initialValues, expanded, removing }) => ( {props => ( handleUpdateExpanded(form, !expanded)} onCancel={() => handleCancel(form)} onRemove={() => handleRemove(form)} expanded={expanded} removing={removing} /> )} )); const _error = error && !_metadata.length && !_loading ? ( Ooops! An error occurred while loading your metadata ) : null; return ( {props => ( handleToggleAddOpen(!addOpen)} /> )} {_line} {_error} {_loading} {_count} {_add} {_metadata} ); }; export default compose( graphql(UpdateMetadata, { name: 'updateMetadata' }), graphql(DeleteMetadata, { name: 'deleteMetadata' }), graphql(GetMetadata, { options: ({ match }) => ({ ssr: false, pollInterval: 1000, variables: { id: get(match, 'params.instance') } }), props: ({ data: { loading, error, machine, refetch, ...rest } }) => { const values = get(machine, 'metadata', []).filter( ({ name = '' }) => name !== 'user-script' ); const metadata = values.map(({ id, name, value }) => ({ id, form: METADATA_FORM_KEY(name), initialValues: { name, value } })); const index = new Fuse(values, { keys: ['name', 'value'] }); return { index, metadata, instance: machine, loading, error, refetch }; } }), connect( ({ values, form }, { metadata, index, ownProps }) => { // get search value const filter = get(form, `${MENU_FORM_NAME}.values.filter`, false); // if user is searching something, get items that match that query const filtered = filter ? index.search(filter).map(({ id }) => find(metadata, ['id', id])) : metadata; return { ...ownProps, addOpen: get(values, 'add-metadata-open', false), metadata: filtered.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 { handleCancel: form => { return dispatch([ set({ name: `${form}-expanded`, value: false }), dispatch(reset(form)) ]); }, handleToggleAddOpen: value => { return dispatch(set({ name: `add-metadata-open`, value })); }, handleUpdateExpanded: (form, expanded) => { return dispatch(set({ name: `${form}-expanded`, value: expanded })); }, shouldAsyncValidate: ({ trigger }) => { return trigger === 'submit'; }, handleAsyncValidate: validateMetadata, 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 => { const name = get( find(metadata, ['form', form]), 'initialValues.name' ); if (!await Confirm(`Do you want to remove "${name}"?`)) { return; } dispatch([ set({ name: `${form}-removing`, value: true }), startSubmit(form) ]); // call mutation const [err] = await intercept( deleteMetadata({ variables: { id: instance.id, name } }) ); const flipSubmitFalse = stopSubmit(form, { _error: err && parseError(err) }); dispatch([ flipSubmitFalse, set({ name: `${form}-removing`, value: false }) ]); // fetch metadata again (even though we are polling) return refetch(); } }; } ) )(Metadata);