feat(images): delete image in summary and list

fixes #1204
This commit is contained in:
Sara Vieira 2018-02-14 19:36:31 +00:00 committed by Sérgio Ramos
parent a7283454b8
commit bf5f0463e7
26 changed files with 235 additions and 113 deletions

View File

@ -3,7 +3,7 @@ import { Link } from 'react-router-dom';
import styled from 'styled-components'; import styled from 'styled-components';
import remcalc from 'remcalc'; import remcalc from 'remcalc';
import { Field } from 'redux-form'; import { Field } from 'redux-form';
import Flex, { FlexItem } from 'styled-flex-component'; import Flex from 'styled-flex-component';
import { Padding, Margin } from 'styled-components-spacing'; import { Padding, Margin } from 'styled-components-spacing';
import { import {
@ -19,7 +19,8 @@ import {
PopoverContainer, PopoverContainer,
Radio, Radio,
FormLabel, FormLabel,
FormGroup FormGroup,
StatusLoader
} from 'joyent-ui-toolkit'; } from 'joyent-ui-toolkit';
import { ImageType, OS } from '@root/constants'; import { ImageType, OS } from '@root/constants';
@ -61,54 +62,71 @@ const Actions = styled(Flex)`
min-width: 48px; min-width: 48px;
`; `;
export const Image = ({ name, os, version, type }) => ( export const Image = ({ name, os, version, type, removing, onRemove }) => (
<Margin bottom={3}> <Margin bottom={3}>
<CardAnchor to={`/${name}`} component={Link}> <CardAnchor to={`/${name}`} component={Link}>
<Card radius> <Card radius>
<CardHeader white radius> {removing ? (
<Padding left={2} right={2}> <Padding all={2}>
<Flex full alignCenter> <StatusLoader />
<Margin right={2}>
{React.createElement(OS[os], {
width: '24',
height: '24'
})}
</Margin>
<A to={`/${name}`} component={Link}>
{name}
</A>
</Flex>
</Padding> </Padding>
</CardHeader> ) : (
<Flex justifyBetween> <Fragment>
<Content left={2} top={2} bottom={2}> <CardHeader white radius>
<Max justifyBetween> <Padding left={2} right={2}>
<Max alignCenter> <Flex full alignCenter>
<Flex>{version}</Flex> <Margin right={2}>
<DividerContainer left={2}> {React.createElement(OS[os], {
<Divider width={remcalc(1)} height="100%" /> width: '24',
</DividerContainer> height: '24'
<Type left={2}>{ImageType[type]}</Type> })}
</Max> </Margin>
</Max> <A to={`/${name}`} component={Link}>
</Content> {name}
<PopoverContainer clickable> </A>
<Actions> </Flex>
<PopoverTarget box style={{ borderLeft: '1px solid #D8D8D8' }}> </Padding>
<ActionsIcon /> </CardHeader>
</PopoverTarget> <Flex justifyBetween>
<Popover placement="bottom"> <Content left={2} top={2} bottom={2}>
<PopoverItem disabled={false} onClick={() => {}}> <Max justifyBetween>
Create Instance <Max alignCenter>
</PopoverItem> <Flex>{version}</Flex>
<PopoverDivider /> <DividerContainer left={2}>
<PopoverItem disabled={false} onClick={() => {}}> <Divider width={remcalc(1)} height="100%" />
Remove </DividerContainer>
</PopoverItem> <Type left={2}>{ImageType[type]}</Type>
</Popover> </Max>
</Actions> </Max>
</PopoverContainer> </Content>
</Flex> <PopoverContainer clickable>
<Actions>
<PopoverTarget
box
style={{ borderLeft: '1px solid #D8D8D8' }}
>
<ActionsIcon />
</PopoverTarget>
<Popover placement="bottom">
<PopoverItem disabled={false}>
<Anchor
noUnderline
black
href={`instances/~create/?image=${name}`}
>
Create Instance
</Anchor>
</PopoverItem>
<PopoverDivider />
<PopoverItem disabled={removing} onClick={onRemove}>
Remove
</PopoverItem>
</Popover>
</Actions>
</PopoverContainer>
</Flex>
</Fragment>
)}
</Card> </Card>
</CardAnchor> </CardAnchor>
</Margin> </Margin>

View File

@ -98,7 +98,7 @@ export const Meta = ({ name, version, type, published_at, state, os }) => (
</Fragment> </Fragment>
); );
export default withTheme(({ theme = {}, ...image }) => ( export default withTheme(({ theme = {}, onRemove, removing, ...image }) => (
<Row> <Row>
<Col xs={12} sm={12} md={9}> <Col xs={12} sm={12} md={9}>
<Card> <Card>
@ -113,7 +113,12 @@ export default withTheme(({ theme = {}, ...image }) => (
</Button> </Button>
</SmallOnly> </SmallOnly>
<Medium> <Medium>
<Button type="button" bold icon> <Button
type="button"
href={`instances/~create/?image=${image.name}`}
bold
icon
>
<DuplicateIcon light /> <DuplicateIcon light />
<span>Create Instance</span> <span>Create Instance</span>
</Button> </Button>
@ -126,7 +131,15 @@ export default withTheme(({ theme = {}, ...image }) => (
</Button> </Button>
</SmallOnly> </SmallOnly>
<Medium> <Medium>
<Button type="button" bold icon error right> <Button
type="button"
loading={removing}
onClick={onRemove}
bold
icon
error
right
>
<DeleteIcon fill={theme.red} /> <DeleteIcon fill={theme.red} />
<span>Remove</span> <span>Remove</span>
</Button> </Button>

View File

@ -112,35 +112,23 @@ export default compose(
} }
}), }),
connect(({ form, values }, { match }) => { connect(({ form, values }, { match }) => {
const nameFilled = get(form, `${Forms.FORM_DETAILS}.values.name`, '');
const step = get(match, 'params.step', 'name'); const step = get(match, 'params.step', 'name');
const disabled = const name = get(form, `${Forms.FORM_DETAILS}.values.name`, '');
!get(values, `${Forms.FORM_DETAILS}-proceeded`, false) || const version = get(form, `${Forms.FORM_DETAILS}.values.version`, '');
!nameFilled.length;
const disabled = !(name.length && version.length);
if (disabled) { if (disabled) {
return { disabled, step }; return { disabled, step };
} }
const name = get(
form,
`${Forms.FORM_DETAILS}.values.name`,
'<instance-name>'
);
const description = get( const description = get(
form, form,
`${Forms.FORM_DETAILS}.values.description`, `${Forms.FORM_DETAILS}.values.description`,
'<instance-description>' '<instance-description>'
); );
const version = get(
form,
`${Forms.FORM_DETAILS}.values.version`,
'<instance-version>'
);
const tags = get(values, Forms.CREATE_TAGS, []); const tags = get(values, Forms.CREATE_TAGS, []);
return { return {

View File

@ -8,6 +8,8 @@ import { connect } from 'react-redux';
import get from 'lodash.get'; import get from 'lodash.get';
import find from 'lodash.find'; import find from 'lodash.find';
import Index from '@state/gen-index'; import Index from '@state/gen-index';
import intercept from 'apr-intercept';
import { set } from 'react-redux-values';
import { import {
ViewContainer, ViewContainer,
@ -23,6 +25,8 @@ import Empty from '@components/empty';
import { ImageType } from '@root/constants'; import { ImageType } from '@root/constants';
import ListImages from '@graphql/list-images.gql'; import ListImages from '@graphql/list-images.gql';
import { Image, Filters } from '@components/image'; import { Image, Filters } from '@components/image';
import RemoveImage from '@graphql/remove-image.gql';
import parseError from '@state/parse-error';
const TOGGLE_FORM_DETAILS = 'images-list-toggle'; const TOGGLE_FORM_DETAILS = 'images-list-toggle';
const MENU_FORM_DETAILS = 'images-list-menu'; const MENU_FORM_DETAILS = 'images-list-menu';
@ -33,7 +37,8 @@ export const List = ({
loading = false, loading = false,
error = null, error = null,
history, history,
typeValue typeValue,
handleRemove
}) => ( }) => (
<ViewContainer main> <ViewContainer main>
<Divider height={remcalc(30)} transparent /> <Divider height={remcalc(30)} transparent />
@ -72,9 +77,9 @@ export const List = ({
</ReduxForm> </ReduxForm>
</Margin> </Margin>
<Row> <Row>
{images.map(image => ( {images.map((image) => (
<Col sm={4}> <Col sm={4}>
<Image {...image} /> <Image {...image} onRemove={() => handleRemove(image.id)} />
</Col> </Col>
))} ))}
{!images.length && !loading ? ( {!images.length && !loading ? (
@ -86,7 +91,9 @@ export const List = ({
); );
export default compose( export default compose(
graphql(RemoveImage, { name: 'removeImage' }),
graphql(ListImages, { graphql(ListImages, {
options: { pollInterval: 5000 },
props: ({ data: { images, loading, error, refetch } }) => { props: ({ data: { images, loading, error, refetch } }) => {
return { return {
images, images,
@ -95,42 +102,77 @@ export default compose(
}; };
} }
}), }),
connect(({ form, values }, { index, error, images = [] }) => { connect(
const filter = get(form, `${MENU_FORM_DETAILS}.values.filter`, false); ({ form, values }, { index, error, images = [] }) => {
const typeValue = get( const filter = get(form, `${MENU_FORM_DETAILS}.values.filter`, false);
form, const mutationError = get(values, 'remove-mutation-error', null);
`${TOGGLE_FORM_DETAILS}.values.image-type`,
'all'
);
const virtual = Object.keys(ImageType).filter( const typeValue = get(
i => ImageType[i] === 'Hardware Virtual Machine' form,
); `${TOGGLE_FORM_DETAILS}.values.image-type`,
const container = Object.keys(ImageType).filter( 'all'
i => ImageType[i] === 'Infrastructure Container' );
);
const filtered = filter const virtual = Object.keys(ImageType).filter(
? Index(images) i => ImageType[i] === 'Hardware Virtual Machine'
.search(filter) );
.map(({ ref }) => find(images, ['id', ref]))
: images;
return { const container = Object.keys(ImageType).filter(
images: filtered.filter(image => { i => ImageType[i] === 'Infrastructure Container'
switch (typeValue) { );
case 'all':
return true; const filtered = filter
case 'hardware-virtual-machine': ? Index(images)
return virtual.includes(image.type); .search(filter)
case 'infrastructure-container': .map(({ ref }) => find(images, ['id', ref]))
return container.includes(image.type); : images;
default:
return true; return {
images: filtered.filter(image => {
switch (typeValue) {
case 'all':
return true;
case 'hardware-virtual-machine':
return virtual.includes(image.type);
case 'infrastructure-container':
return container.includes(image.type);
default:
return true;
}
}).map(({ id, ...image }) => ({
...image,
id,
removing: get(values, `remove-mutation-${id}-loading`, false)
})),
allImages: images,
mutationError,
typeValue
};
},
(dispatch, { removeImage, history }) => ({
handleRemove: async id => {
dispatch([set({ name: `remove-mutation-${id}-loading`, value: true })]);
const [err, res] = await intercept(
removeImage({
variables: {
id
}
})
);
if (err) {
dispatch([
set({ name: 'remove-mutation-error', value: parseError(err) }),
set({ name: `remove-mutation-${id}-loading`, value: false })
]);
} }
}),
allImages: images, if (res) {
typeValue dispatch([set({ name: `remove-mutation-${id}-loading`, value: false })]);
}; history.push(`/`);
}) }
}
})
)
)(List); )(List);

View File

@ -4,6 +4,9 @@ import { Margin } from 'styled-components-spacing';
import find from 'lodash.find'; import find from 'lodash.find';
import get from 'lodash.get'; import get from 'lodash.get';
import remcalc from 'remcalc'; import remcalc from 'remcalc';
import { connect } from 'react-redux';
import intercept from 'apr-intercept';
import { set } from 'react-redux-values';
import { import {
ViewContainer, ViewContainer,
@ -16,8 +19,17 @@ import {
import ImageSummary from '@components/summary'; import ImageSummary from '@components/summary';
import GetImage from '@graphql/get-image.gql'; import GetImage from '@graphql/get-image.gql';
import RemoveImage from '@graphql/remove-image.gql';
import parseError from '@state/parse-error';
export const Summary = ({ image, loading = false, error = null }) => ( export const Summary = ({
image,
loading = false,
error = null,
removing,
mutationError,
handleRemove
}) => (
<ViewContainer main> <ViewContainer main>
{loading && !image ? ( {loading && !image ? (
<Fragment> <Fragment>
@ -35,11 +47,24 @@ export const Summary = ({ image, loading = false, error = null }) => (
</Message> </Message>
</Margin> </Margin>
) : null} ) : null}
{image ? <ImageSummary {...image} /> : null} {mutationError ? (
<Margin bottom={4}>
<Message error>
<MessageTitle>Ooops!</MessageTitle>
<MessageDescription>
There was a problem deleting your image
</MessageDescription>
</Message>
</Margin>
) : null}
{image ? (
<ImageSummary removing={removing} onRemove={handleRemove} {...image} />
) : null}
</ViewContainer> </ViewContainer>
); );
export default compose( export default compose(
graphql(RemoveImage, { name: 'removeImage' }),
graphql(GetImage, { graphql(GetImage, {
options: ({ match }) => ({ options: ({ match }) => ({
variables: { variables: {
@ -53,5 +78,42 @@ export default compose(
loading, loading,
error error
}) })
}) }),
connect(
({ values }, ownProps) => {
const removing = get(values, 'remove-mutation-loading', false);
const mutationError = get(values, 'remove-mutation-error', null);
return {
...ownProps,
removing,
mutationError
};
},
(dispatch, { removeImage, image, history }) => ({
handleRemove: async () => {
dispatch([set({ name: 'remove-mutation-loading', value: true })]);
const [err, res] = await intercept(
removeImage({
variables: {
id: image.id
}
})
);
if (err) {
dispatch([
set({ name: 'remove-mutation-error', value: parseError(err) }),
set({ name: 'remove-mutation-loading', value: false })
]);
}
if (res) {
dispatch([set({ name: 'remove-mutation-loading', value: false })]);
history.push(`/`);
}
}
})
)
)(Summary); )(Summary);

View File

@ -0,0 +1,6 @@
mutation removeImage($id: ID!) {
deleteImage(id: $id) {
id
name
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -82,11 +82,7 @@ export default () => (
exact exact
component={InstanceSnapshots} component={InstanceSnapshots}
/> />
<Route <Route path="/instances/:instance/cns" exact component={InstanceCns} />
path="/instances/:instance/cns-dns"
exact
component={InstanceCns}
/>
<Route <Route
path="/instances/:instance/user-script" path="/instances/:instance/user-script"
exact exact

View File

@ -1,7 +1,6 @@
const Inert = require('inert'); const Inert = require('inert');
const Path = require('path'); const Path = require('path');
const Execa = require('execa'); const Execa = require('execa');
const { readFile } = require('mz/fs');
const ROOT = Path.join(__dirname, '../build'); const ROOT = Path.join(__dirname, '../build');

View File

@ -29,7 +29,6 @@
"joyent-react-scripts": "^7.3.0", "joyent-react-scripts": "^7.3.0",
"lodash.chunk": "^4.2.0", "lodash.chunk": "^4.2.0",
"lodash.keys": "^4.2.0", "lodash.keys": "^4.2.0",
"mz": "^2.7.0",
"outy": "^0.1.2", "outy": "^0.1.2",
"param-case": "^2.1.1", "param-case": "^2.1.1",
"pascal-case": "^2.0.1", "pascal-case": "^2.0.1",

View File

@ -1,6 +1,5 @@
import styled from 'styled-components'; import styled from 'styled-components';
import remcalc from 'remcalc'; import remcalc from 'remcalc';
import is from 'styled-is';
export default styled.div` export default styled.div`
width: calc(100% + ${remcalc(36)}); width: calc(100% + ${remcalc(36)});