@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
6
packages/images/src/graphql/remove-image.gql
Normal file
@ -0,0 +1,6 @@
|
||||
mutation removeImage($id: ID!) {
|
||||
deleteImage(id: $id) {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 9.2 KiB |
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 9.1 KiB |
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 9.1 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
@ -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
|
||||
|
@ -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');
|
||||
|
||||
|
@ -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",
|
||||
|
@ -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)});
|
||||
|