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

View File

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

View File

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

View File

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

View File

@ -4,6 +4,9 @@ import { Margin } from 'styled-components-spacing';
import find from 'lodash.find';
import get from 'lodash.get';
import remcalc from 'remcalc';
import { connect } from 'react-redux';
import intercept from 'apr-intercept';
import { set } from 'react-redux-values';
import {
ViewContainer,
@ -16,8 +19,17 @@ import {
import ImageSummary from '@components/summary';
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>
{loading && !image ? (
<Fragment>
@ -35,11 +47,24 @@ export const Summary = ({ image, loading = false, error = null }) => (
</Message>
</Margin>
) : 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>
);
export default compose(
graphql(RemoveImage, { name: 'removeImage' }),
graphql(GetImage, {
options: ({ match }) => ({
variables: {
@ -53,5 +78,42 @@ export default compose(
loading,
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);

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
component={InstanceSnapshots}
/>
<Route
path="/instances/:instance/cns-dns"
exact
component={InstanceCns}
/>
<Route path="/instances/:instance/cns" exact component={InstanceCns} />
<Route
path="/instances/:instance/user-script"
exact

View File

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

View File

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

View File

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