@ -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,10 +62,16 @@ 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>
|
||||||
|
{removing ? (
|
||||||
|
<Padding all={2}>
|
||||||
|
<StatusLoader />
|
||||||
|
</Padding>
|
||||||
|
) : (
|
||||||
|
<Fragment>
|
||||||
<CardHeader white radius>
|
<CardHeader white radius>
|
||||||
<Padding left={2} right={2}>
|
<Padding left={2} right={2}>
|
||||||
<Flex full alignCenter>
|
<Flex full alignCenter>
|
||||||
@ -94,21 +101,32 @@ export const Image = ({ name, os, version, type }) => (
|
|||||||
</Content>
|
</Content>
|
||||||
<PopoverContainer clickable>
|
<PopoverContainer clickable>
|
||||||
<Actions>
|
<Actions>
|
||||||
<PopoverTarget box style={{ borderLeft: '1px solid #D8D8D8' }}>
|
<PopoverTarget
|
||||||
|
box
|
||||||
|
style={{ borderLeft: '1px solid #D8D8D8' }}
|
||||||
|
>
|
||||||
<ActionsIcon />
|
<ActionsIcon />
|
||||||
</PopoverTarget>
|
</PopoverTarget>
|
||||||
<Popover placement="bottom">
|
<Popover placement="bottom">
|
||||||
<PopoverItem disabled={false} onClick={() => {}}>
|
<PopoverItem disabled={false}>
|
||||||
|
<Anchor
|
||||||
|
noUnderline
|
||||||
|
black
|
||||||
|
href={`instances/~create/?image=${name}`}
|
||||||
|
>
|
||||||
Create Instance
|
Create Instance
|
||||||
|
</Anchor>
|
||||||
</PopoverItem>
|
</PopoverItem>
|
||||||
<PopoverDivider />
|
<PopoverDivider />
|
||||||
<PopoverItem disabled={false} onClick={() => {}}>
|
<PopoverItem disabled={removing} onClick={onRemove}>
|
||||||
Remove
|
Remove
|
||||||
</PopoverItem>
|
</PopoverItem>
|
||||||
</Popover>
|
</Popover>
|
||||||
</Actions>
|
</Actions>
|
||||||
</PopoverContainer>
|
</PopoverContainer>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
</Fragment>
|
||||||
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
</CardAnchor>
|
</CardAnchor>
|
||||||
</Margin>
|
</Margin>
|
||||||
|
@ -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>
|
||||||
|
@ -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 {
|
||||||
|
@ -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,8 +102,11 @@ export default compose(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
connect(({ form, values }, { index, error, images = [] }) => {
|
connect(
|
||||||
|
({ form, values }, { index, error, images = [] }) => {
|
||||||
const filter = get(form, `${MENU_FORM_DETAILS}.values.filter`, false);
|
const filter = get(form, `${MENU_FORM_DETAILS}.values.filter`, false);
|
||||||
|
const mutationError = get(values, 'remove-mutation-error', null);
|
||||||
|
|
||||||
const typeValue = get(
|
const typeValue = get(
|
||||||
form,
|
form,
|
||||||
`${TOGGLE_FORM_DETAILS}.values.image-type`,
|
`${TOGGLE_FORM_DETAILS}.values.image-type`,
|
||||||
@ -106,6 +116,7 @@ export default compose(
|
|||||||
const virtual = Object.keys(ImageType).filter(
|
const virtual = Object.keys(ImageType).filter(
|
||||||
i => ImageType[i] === 'Hardware Virtual Machine'
|
i => ImageType[i] === 'Hardware Virtual Machine'
|
||||||
);
|
);
|
||||||
|
|
||||||
const container = Object.keys(ImageType).filter(
|
const container = Object.keys(ImageType).filter(
|
||||||
i => ImageType[i] === 'Infrastructure Container'
|
i => ImageType[i] === 'Infrastructure Container'
|
||||||
);
|
);
|
||||||
@ -128,9 +139,40 @@ export default compose(
|
|||||||
default:
|
default:
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}),
|
}).map(({ id, ...image }) => ({
|
||||||
|
...image,
|
||||||
|
id,
|
||||||
|
removing: get(values, `remove-mutation-${id}-loading`, false)
|
||||||
|
})),
|
||||||
allImages: images,
|
allImages: images,
|
||||||
|
mutationError,
|
||||||
typeValue
|
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 })
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
dispatch([set({ name: `remove-mutation-${id}-loading`, value: false })]);
|
||||||
|
history.push(`/`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
)(List);
|
)(List);
|
||||||
|
@ -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);
|
||||||
|
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
|
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
|
||||||
|
@ -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');
|
||||||
|
|
||||||
|
@ -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",
|
||||||
|
@ -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)});
|
||||||
|