feat(images): add tags

This commit is contained in:
Sara Vieira 2018-02-12 15:13:14 +00:00
parent 7d0ce55070
commit 6af4765910
11 changed files with 231 additions and 166 deletions

View File

@ -0,0 +1,46 @@
import React from 'react';
import styled from 'styled-components';
import remcalc from 'remcalc';
const List = styled.ul`
display: flex;
list-style: none;
padding: ${remcalc(12)} ${remcalc(18)};
border-top: ${remcalc(1)} solid ${props => props.theme.grey};
width: 100%;
justify-content: flex-end;
position: absolute;
box-sizing: border-box;
margin: 0;
bottom: 0;
`;
const ListItem = styled.li`
color: ${props => props.theme.greyDark};
&:not(:last-child) {
padding-right: ${remcalc(24)};
}
`;
const Link = styled.a`
color: ${props => props.theme.greyDark};
text-decoration: none;
`;
export default () => (
<List>
<ListItem>
<Link href="https://www.joyent.com/about/policies" target="_blank">
Policies
</Link>
</ListItem>
<ListItem>
<Link href="https://www.joyent.com/networking-and-security/security-compliance">
Compliance
</Link>
</ListItem>
<ListItem>
<b>© {new Date().getFullYear()} Joyent, Inc.</b>
</ListItem>
</List>
);

View File

@ -22,3 +22,11 @@ export const OS = {
ILLUMOS: Illumos,
OTHER: Placeholder
};
export const Forms = {
FORM_TAGS_CREATE: 'CREATE-IMAGE-TAGS-ADD',
FORM_TAGS_EDIT: i => `CREATE-IMAGE-TAGS-EDIT-${i}`,
FORM_DETAILS: 'CREATE-IMAGE-DETAILS',
CREATE_FORM: 'CREATE-IMAGE',
CREATE_TAGS: 'CREATE-IMAGE-TAGS'
}

View File

@ -7,6 +7,7 @@ import { Breadcrumb, BreadcrumbItem } from 'joyent-ui-toolkit';
export default ({ match }) => {
const image = get(match, 'params.image');
const create = get(match, 'params.step');
const links = [
{
@ -14,6 +15,14 @@ export default ({ match }) => {
pathname: '/'
}
]
.concat(
create && [
{
name: 'Create Image',
pathname: `/~create`
}
]
)
.concat(
image && [
{

View File

@ -8,18 +8,17 @@ import { connect } from 'react-redux';
import intercept from 'apr-intercept';
import get from 'lodash.get';
import punycode from 'punycode';
import { Row, Col } from 'joyent-react-styled-flexboxgrid';
import { NameIcon, H3, Button, H4, P } from 'joyent-ui-toolkit';
import Title from '@components/create-image/title';
import Animated from '@containers/create-image/animated';
import Name from '@components/create-image/name';
import Details from '@components/create-image/details';
import Description from '@components/description';
import GetRandomName from '@graphql/get-random-name.gql';
import { client } from '@state/store';
import { Row, Col } from 'joyent-react-styled-flexboxgrid';
const FORM_NAME = 'create-image-details';
import { Forms } from '../../constants';
const NameContainer = ({
expanded,
@ -51,7 +50,7 @@ const NameContainer = ({
</Description>
) : null}
<ReduxForm
form={FORM_NAME}
form={Forms.FORM_DETAILS}
destroyOnUnmount={false}
forceUnregisterOnUnmount={true}
onSubmit={handleNext}
@ -60,7 +59,7 @@ const NameContainer = ({
>
{props =>
expanded ? (
<Name
<Details
{...props}
placeholderName={placeholderName}
randomizing={randomizing}
@ -69,9 +68,14 @@ const NameContainer = ({
) : name ? (
<Margin top={3}>
<H3 bold noMargin>{name}</H3>
<Margin top={2}><H4 bold noMargin>{version}</H4></Margin>
<Row>
<Col xs={12} sm={8}><Margin top={1}><P>{description}</P></Margin></Col></Row>
{version ? <Margin top={2}><H4 bold noMargin>{version}</H4></Margin> : null}
{description ? <Row>
<Col xs={12} sm={8}>
<Margin top={1}>
<P>{description}</P>
</Margin>
</Col>
</Row> : null}
</Margin>
) : null
}
@ -80,13 +84,13 @@ const NameContainer = ({
<Margin top={4} bottom={7}>
<Button type="button" disabled={!name} onClick={handleNext}>
Next
</Button>
</Button>
</Margin>
) : proceeded ? (
<Margin top={4} bottom={7}>
<Button type="button" onClick={handleEdit} secondary>
Edit
</Button>
</Button>
</Margin>
) : null}
</Fragment>
@ -102,11 +106,11 @@ export default compose(
}),
connect(
({ form, values }, ownProps) => {
const name = get(form, `${FORM_NAME}.values.name`, '');
const version = get(form, `${FORM_NAME}.values.version`, '');
const description = get(form, `${FORM_NAME}.values.description`, '');
const name = get(form, `${Forms.FORM_DETAILS}.values.name`, '');
const version = get(form, `${Forms.FORM_DETAILS}.values.version`, '');
const description = get(form, `${Forms.FORM_DETAILS}.values.description`, '');
const proceeded = get(values, 'create-image-name-proceeded', false);
const proceeded = get(values, `${Forms.FORM_DETAILS}-proceeded`, false);
const randomizing = get(
values,
@ -143,7 +147,7 @@ export default compose(
}
},
handleNext: () => {
dispatch(set({ name: 'create-image-name-proceeded', value: true }));
dispatch(set({ name: `${Forms.FORM_DETAILS}-proceeded`, value: true }));
return history.push(`/~create/tag`);
},
@ -172,7 +176,7 @@ export default compose(
const { data } = res;
const { rndName } = data;
return dispatch(change(FORM_NAME, 'name', rndName));
return dispatch(change(Forms.FORM_DETAILS, 'name', rndName));
}
})
)

View File

@ -7,6 +7,7 @@ import ReduxForm from 'declarative-redux-form';
import { connect } from 'react-redux';
import get from 'lodash.get';
import remcalc from 'remcalc';
import Flex from 'styled-flex-component';
import {
TagsIcon,
@ -21,9 +22,7 @@ import Title from '@components/create-image/title';
import Animated from '@containers/create-image/animated';
import Description from '@components/description';
import Tag from '@components/tags';
const FORM_NAME_CREATE = 'CREATE-IMAGE-TAGS-ADD';
const FORM_NAME_EDIT = i => `CREATE-IMAGE-TAGS-EDIT-${i}`;
import { Forms } from '../../constants';
export const Tags = ({
tags = [],
@ -38,122 +37,123 @@ export const Tags = ({
handleChangeAddOpen,
handleNext,
step,
handleEdit
handleEdit,
children
}) => (
<Fragment>
<Title
id={step}
onClick={!expanded && !proceeded && handleEdit}
collapsed={!expanded && !proceeded}
icon={<TagsIcon />}
>
Tags
<Fragment>
<Title
id={step}
onClick={!expanded && !proceeded && handleEdit}
collapsed={!expanded && !proceeded}
icon={<TagsIcon />}
>
Tags
</Title>
{expanded ? (
<Description>
Tags can be used to identify your images, group multiple images
{expanded ? (
<Description>
Tags can be used to identify your images, group multiple images
together, define firewall and affinity rules, and more.{' '}
<a
target="__blank"
href="https://docs.joyent.com/public-cloud/tags-metadata/tags"
rel="noopener noreferrer"
>
Read the docs
<a
target="__blank"
href="https://docs.joyent.com/public-cloud/tags-metadata/tags"
rel="noopener noreferrer"
>
Read the docs
</a>
</Description>
) : null}
{proceeded || expanded ? (
<Fragment>
<Margin bottom={4}>
<H3>
{tags.length} Tag{tags.length === 1 ? '' : 's'}
</H3>
</Description>
) : null}
{proceeded || expanded ? (
<Fragment>
<Margin bottom={4}>
<H3>
{tags.length} Tag{tags.length === 1 ? '' : 's'}
</H3>
</Margin>
<TagList>
{tags.map(({ name, value }, index) => (
<Tag
key={index}
name={name}
value={value}
onRemoveClick={expanded && (() => handleRemoveTag(index))}
/>
))}
</TagList>
</Fragment>
) : null}
<ReduxForm
form={Forms.FORM_TAGS_CREATE}
destroyOnUnmount={false}
forceUnregisterOnUnmount={true}
onSubmit={handleAddTag}
>
{props =>
expanded && addOpen ? (
<Fragment>
<KeyValue
{...props}
method="add"
input="input"
type="tag"
expanded
onCancel={() => handleChangeAddOpen(false)}
/>
<Divider height={remcalc(18)} transparent />
</Fragment>
) : null
}
</ReduxForm>
<Margin top={1}>
<Flex alignCenter>
{expanded ? (<Button
type="button"
onClick={() => handleChangeAddOpen(true)}
secondary
>
Add Tag
</Button>) : null}
<Margin left={1}>{children}</Margin>
</Flex>
</Margin>
{proceeded ? (
<Margin top={1} bottom={7}>
<Button type="button" onClick={handleEdit} secondary>
Edit
</Button>
</Margin>
<TagList>
{tags.map(({ name, value }, index) => (
<Tag
key={index}
name={name}
value={value}
onRemoveClick={expanded && (() => handleRemoveTag(index))}
/>
))}
</TagList>
</Fragment>
) : null}
<ReduxForm
form={FORM_NAME_CREATE}
destroyOnUnmount={false}
forceUnregisterOnUnmount={true}
onSubmit={handleAddTag}
>
{props =>
expanded && addOpen ? (
<Fragment>
<KeyValue
{...props}
method="add"
input="input"
type="tag"
expanded
onCancel={() => handleChangeAddOpen(false)}
/>
<Divider height={remcalc(18)} transparent />
</Fragment>
) : null
}
</ReduxForm>
{expanded ? (
<Margin top={1} bottom={7}>
<Button
type="button"
onClick={() => handleChangeAddOpen(true)}
secondary
>
Add Tag
</Button>
<Button type="button" onClick={handleNext}>
Next
</Button>
</Margin>
) : proceeded ? (
<Margin top={1} bottom={7}>
<Button type="button" onClick={handleEdit} secondary>
Edit
</Button>
</Margin>
) : null}
</Fragment>
);
) : null}
</Fragment>
);
export default compose(
Animated,
connect(({ values }, ownProps) => ({
proceeded: get(values, 'create-image-tags-proceeded', false),
addOpen: get(values, 'create-image-tags-add-open', false),
tags: get(values, 'create-image-tags', [])
proceeded: get(values, `${Forms.CREATE_TAGS}-proceeded`, false),
addOpen: get(values, `${Forms.CREATE_TAGS}-add-open`, false),
tags: get(values, Forms.CREATE_TAGS, [])
})),
connect(null, (dispatch, { tags = [], history }) => ({
handleNext: () => {
dispatch(set({ name: 'create-image-tags-proceeded', value: true }));
dispatch(set({ name: `${Forms.CREATE_TAGS}-proceeded`, value: true }));
return history.push(`/~create/create`);
},
handleEdit: () => {
return history.push(`/~create/tags`);
return history.push(`/~create/tag`);
},
handleAddTag: value => {
const toggleToClosed = set({
name: `create-image-tags-add-open`,
name: `${Forms.CREATE_TAGS}-add-open`,
value: false
});
const appendTag = set({
name: `create-image-tags`,
name: Forms.CREATE_TAGS,
value: tags.concat([{ ...value, expanded: false }])
});
return dispatch([destroy(FORM_NAME_CREATE), toggleToClosed, appendTag]);
return dispatch([destroy(Forms.FORM_TAGS_CREATE), toggleToClosed, appendTag]);
},
handleUpdateTag: (index, newTag) => {
tags[index] = {
@ -162,14 +162,14 @@ export default compose(
};
return dispatch([
destroy(FORM_NAME_EDIT(index)),
set({ name: `create-image-tags`, value: tags.slice() })
destroy(Forms.FORM_TAGS_EDIT(index)),
set({ name: Forms.CREATE_TAGS, value: tags.slice() })
]);
},
handleChangeAddOpen: value => {
return dispatch([
reset(FORM_NAME_CREATE),
set({ name: `create-image-tags-add-open`, value })
reset(Forms.FORM_TAGS_CREATE),
set({ name: `${Forms.CREATE_TAGS}-add-open`, value })
]);
},
handleToggleExpanded: index => {
@ -180,7 +180,7 @@ export default compose(
return dispatch(
set({
name: `create-image-tags`,
name: Forms.CREATE_TAGS,
value: tags.slice()
})
);
@ -192,16 +192,16 @@ export default compose(
};
return dispatch([
reset(FORM_NAME_EDIT(index)),
set({ name: `create-image-tags`, value: tags.slice() })
reset(Forms.FORM_TAGS_EDIT(index)),
set({ name: Forms.CREATE_TAGS, value: tags.slice() })
]);
},
handleRemoveTag: index => {
tags.splice(index, 1);
return dispatch([
destroy(FORM_NAME_EDIT(index)),
set({ name: `create-image-tags`, value: tags.slice() })
destroy(Forms.FORM_TAGS_EDIT(index)),
set({ name: Forms.CREATE_TAGS, value: tags.slice() })
]);
}
}))

View File

@ -9,77 +9,73 @@ import { compose } from 'react-apollo';
import get from 'lodash.get';
import uniqBy from 'lodash.uniqby';
import omit from 'lodash.omit';
import remcalc from 'remcalc';
import { ViewContainer, H2, Button, Divider } from 'joyent-ui-toolkit';
import { ViewContainer, H2, Button } from 'joyent-ui-toolkit';
import Name from '@containers/create-image/name';
import Details from '@containers/create-image/details';
import Tags from '@containers/create-image/tags';
const CREATE_FORM = 'CREATE-IMAGE';
import { Forms } from '../constants';
const CreateImage = ({ step, history, location, match, disabled, handleSubmit }) => (
<ViewContainer>
<Margin top={4} bottom={4}>
<H2>Create Image</H2>
</Margin>
<Name
<Details
history={history}
match={match}
step="name"
expanded={match.params.step === 'name'}
expanded={step === 'name'}
/>
<Tags
history={history}
match={match}
step="tag"
expanded={match.params.step === 'tag'}
/>
<Margin top={7}><Divider height={remcalc(1)} /></Margin>
<Margin top={4} bottom={10}>
<ReduxForm form={CREATE_FORM} onSubmit={handleSubmit}>
expanded={step === 'tag'}>
<ReduxForm form={Forms.CREATE_FORM} onSubmit={handleSubmit} >
{({ handleSubmit, submitting }) => (
<form onSubmit={handleSubmit}>
<Button disabled={disabled} loading={submitting}>
Create Image
</Button>
</Button>
</form>
)}
</ReduxForm>
</Margin>
</Tags>
</ViewContainer>
);
export default compose(
connect(({ form, values }) => {
const FORM_NAME = 'create-image-details';
const nameFilled = get(form, `${FORM_NAME}.values.name`, '');
connect(({ form, values }, { match }) => {
const nameFilled = get(form, `${Forms.FORM_DETAILS}.values.name`, '');
const step = get(match, 'params.step', 'name');
const disabled = !get(values, `create-image-name-proceeded`, false) || !nameFilled.length
const disabled = !get(values, `${Forms.FORM_DETAILS}-proceeded`, false) || !nameFilled.length
if (disabled) {
return { disabled };
return { disabled, step };
}
const name = get(
form,
`${FORM_NAME}.values.name`,
`${Forms.FORM_DETAILS}.values.name`,
'<instance-name>'
);
const description = get(
form,
`${FORM_NAME}.values.description`,
`${Forms.FORM_DETAILS}.values.description`,
'<instance-description>'
);
const version = get(
form,
`${FORM_NAME}.values.version`,
`${Forms.FORM_DETAILS}.values.version`,
'<instance-version>'
);
const tags = get(values, 'create-image-tags', []);
const tags = get(values, Forms.CREATE_TAGS, []);
return {
forms: Object.keys(form), // improve this
@ -87,7 +83,8 @@ export default compose(
description,
version,
tags,
disabled
disabled,
step
};
}),
connect(null, (dispatch, {
@ -111,7 +108,6 @@ export default compose(
// name: _name,
// version: _version,
// description: _description,
// package: pkg,
// tags: _tags,
// }
// })
@ -132,7 +128,7 @@ export default compose(
description: ${_description}
version: ${_version}
tags: ${_tags.map(tag => tag.name)}
`)
`)
dispatch([destroyAll(), forms.map(name => destroy(name))]);

View File

@ -26,8 +26,8 @@ import ListImages from '@graphql/list-images.gql';
import { Image, Filters } from '@components/image';
const TOGGLE_FORM_NAME = 'images-list-toggle';
const MENU_FORM_NAME = 'images-list-menu';
const TOGGLE_FORM_DETAILS = 'images-list-toggle';
const MENU_FORM_DETAILS = 'images-list-menu';
export const List = ({
images = [],
@ -38,7 +38,7 @@ export const List = ({
}) => (
<ViewContainer main>
<Divider height={remcalc(30)} transparent />
<ReduxForm form={MENU_FORM_NAME}>
<ReduxForm form={MENU_FORM_DETAILS}>
{props => <ToolbarForm {...props} actionable={!loading} />}
</ReduxForm>
<Divider height={remcalc(1)} />
@ -62,7 +62,7 @@ export const List = ({
<Fragment>
<Margin bottom={4}>
<ReduxForm
form={TOGGLE_FORM_NAME}
form={TOGGLE_FORM_DETAILS}
initialValues={{ 'image-type': 'all' }}
>
{props => (allImages.length ? <Filters {...props} /> : null)}
@ -93,8 +93,8 @@ export default compose(
}
}),
connect(({ form, values }, { index, error, images = [] }) => {
const filter = get(form, `${MENU_FORM_NAME}.values.filter`, false);
const typeValue = get(form, `${TOGGLE_FORM_NAME}.values.image-type`, 'all');
const filter = get(form, `${MENU_FORM_DETAILS}.values.filter`, false);
const typeValue = get(form, `${TOGGLE_FORM_DETAILS}.values.image-type`, 'all');
const virtual = Object.keys(ImageType).filter(
i => ImageType[i] === 'Hardware Virtual Machine'

View File

@ -10,7 +10,7 @@ const SECTIONS = [
export default ({ match }) => {
const imageSlug = get(match, 'params.image');
const sections = imageSlug ? SECTIONS : [];
const sections = imageSlug !== '~create' ? SECTIONS : [];
const links = sections.map(({ name, pathname }) => ({
name,

View File

@ -21,11 +21,11 @@ import Tag from '@components/tags';
import ToolbarForm from '@components/toolbar';
import GetTags from '@graphql/get-tags.gql';
const MENU_FORM_NAME = 'image-tags-list-menu';
const MENU_FORM_DETAILS = 'image-tags-list-menu';
export const Tags = ({ tags = [], loading = false, error = null }) => (
<ViewContainer main>
<ReduxForm form={MENU_FORM_NAME}>
<ReduxForm form={MENU_FORM_DETAILS}>
{props => (
<Margin bottom="4">
<ToolbarForm

View File

@ -9,28 +9,20 @@ import List from '@containers/list';
import Summary from '@containers/summary';
import Create from '@containers/create';
import Tags from '@containers/tags';
import Footer from '@components/footer';
export default () => (
<BrowserRouter>
<PageContainer>
{/* Breadcrumb */}
<Switch>
<Route path="/~create" component={Breadcrumb} />
<Route path="/~create/:step" exact component={Breadcrumb} />
<Route path="/:image?" component={Breadcrumb} />
</Switch>
{/* Menu */}
<Switch>
<Route path="/~create" component={Menu} />
<Route path="/:image/:section?" component={Menu} />
</Switch>
{/* Create Image */}
<Switch>
<Route
path="/~create/"
exact
component={() => <Redirect to="/~create/name" />}
/>
<Route path="/~create/:step" component={Create} />
<Route path="/~create/:step" component={() => {}} />
</Switch>
{/* Images */}
<Switch>
@ -41,10 +33,20 @@ export default () => (
path="/:image"
exact
component={({ match }) => (
<Redirect to={`/${get(match, 'params.image')}/summary`} />
<Redirect to={`/${get(match, 'params.image')}/summary`} />
)}
/>
</Switch>
{/* Create Image */}
<Switch>
<Route
path="/~create/"
exact
component={() => <Redirect to="/~create/name" />}
/>
<Route path="/~create/:step" component={Create} />
</Switch>
<Footer />
</PageContainer>
</BrowserRouter>
);