feat(my-joy-beta): revise snapshots implementation
according to new designs fixes #872
This commit is contained in:
parent
8eecebfe47
commit
3caaebb0e9
@ -4826,6 +4826,7 @@ exports[`renders <KeyValue method="edit" /> without throwing 1`] = `
|
||||
<b>
|
||||
undefined:
|
||||
</b>
|
||||
,
|
||||
<span />
|
||||
</span>
|
||||
</div>
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1871,6 +1871,7 @@ exports[`renders <EditForm /> without throwing 1`] = `
|
||||
<b>
|
||||
undefined:
|
||||
</b>
|
||||
,
|
||||
<span />
|
||||
</span>
|
||||
</div>
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1809,6 +1809,7 @@ exports[`renders <EditForm /> without throwing 1`] = `
|
||||
<b>
|
||||
undefined:
|
||||
</b>
|
||||
,
|
||||
<span />
|
||||
</span>
|
||||
</div>
|
||||
|
@ -3,70 +3,9 @@ import renderer from 'react-test-renderer';
|
||||
import 'jest-styled-components';
|
||||
|
||||
import { Table, TableTbody } from 'joyent-ui-toolkit';
|
||||
import InstanceList, { Actions, Item } from '../list';
|
||||
import InstanceList, { Item } from '../list';
|
||||
import Theme from '@mocks/theme';
|
||||
|
||||
it('renders <Actions /> without throwing', () => {
|
||||
expect(
|
||||
renderer
|
||||
.create(
|
||||
<Theme>
|
||||
<Actions />
|
||||
</Theme>
|
||||
)
|
||||
.toJSON()
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders <Actions submitting /> without throwing', () => {
|
||||
expect(
|
||||
renderer
|
||||
.create(
|
||||
<Theme>
|
||||
<Actions submitting />
|
||||
</Theme>
|
||||
)
|
||||
.toJSON()
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders <Actions submitting statuses /> without throwing', () => {
|
||||
const statuses = {
|
||||
starting: true,
|
||||
stopping: true,
|
||||
restarting: true,
|
||||
deleting: true
|
||||
};
|
||||
|
||||
expect(
|
||||
renderer
|
||||
.create(
|
||||
<Theme>
|
||||
<Actions submitting statuses={statuses} />
|
||||
</Theme>
|
||||
)
|
||||
.toJSON()
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders <Actions allowedActions /> without throwing', () => {
|
||||
const allowedActions = {
|
||||
start: true,
|
||||
stop: true,
|
||||
reboot: true
|
||||
};
|
||||
|
||||
expect(
|
||||
renderer
|
||||
.create(
|
||||
<Theme>
|
||||
<Actions allowedActions={allowedActions} />
|
||||
</Theme>
|
||||
)
|
||||
.toJSON()
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders <Item /> without throwing', () => {
|
||||
expect(
|
||||
renderer
|
||||
|
@ -0,0 +1,128 @@
|
||||
import React from 'react';
|
||||
import renderer from 'react-test-renderer';
|
||||
import 'jest-styled-components';
|
||||
|
||||
import { Table, TableTbody } from 'joyent-ui-toolkit';
|
||||
import SnapshotList, { Item } from '../snapshots';
|
||||
import Theme from '@mocks/theme';
|
||||
|
||||
it('renders <Item /> without throwing', () => {
|
||||
expect(
|
||||
renderer
|
||||
.create(
|
||||
<Theme>
|
||||
<Item />
|
||||
</Theme>
|
||||
)
|
||||
.toJSON()
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders <Item mutating /> without throwing', () => {
|
||||
expect(
|
||||
renderer
|
||||
.create(
|
||||
<Theme>
|
||||
<Table>
|
||||
<TableTbody>
|
||||
<Item mutating />
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Theme>
|
||||
)
|
||||
.toJSON()
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
||||
|
||||
it('renders <Item {...item} /> without throwing', () => {
|
||||
const item = {
|
||||
updated: '12/09/2017',
|
||||
created: '12/09/2017',
|
||||
machineID: '657-sh',
|
||||
name: 'name',
|
||||
state: 'STARTED'
|
||||
};
|
||||
|
||||
expect(
|
||||
renderer
|
||||
.create(
|
||||
<Theme>
|
||||
<Item {...item} />
|
||||
</Theme>
|
||||
)
|
||||
.toJSON()
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders <SnapshotList /> without throwing', () => {
|
||||
expect(
|
||||
renderer
|
||||
.create(
|
||||
<Theme>
|
||||
<SnapshotList />
|
||||
</Theme>
|
||||
)
|
||||
.toJSON()
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders <Actions /> without throwing', () => {
|
||||
expect(
|
||||
renderer
|
||||
.create(
|
||||
<Theme>
|
||||
<SnapshotList selected={[1, 3]} />
|
||||
</Theme>
|
||||
)
|
||||
.toJSON()
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders <SnapshotList sortBy /> without throwing', () => {
|
||||
expect(
|
||||
renderer
|
||||
.create(
|
||||
<Theme>
|
||||
<SnapshotList sortBy="state" />
|
||||
</Theme>
|
||||
)
|
||||
.toJSON()
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders <SnapshotList sortBy sortOrder /> without throwing', () => {
|
||||
expect(
|
||||
renderer
|
||||
.create(
|
||||
<Theme>
|
||||
<SnapshotList sortBy="state" sortOrder="asc" />
|
||||
</Theme>
|
||||
)
|
||||
.toJSON()
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders <SnapshotList submitting /> without throwing', () => {
|
||||
expect(
|
||||
renderer
|
||||
.create(
|
||||
<Theme>
|
||||
<SnapshotList submitting />
|
||||
</Theme>
|
||||
)
|
||||
.toJSON()
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders <SnapshotList allSelected /> without throwing', () => {
|
||||
expect(
|
||||
renderer
|
||||
.create(
|
||||
<Theme>
|
||||
<SnapshotList allSelected />
|
||||
</Theme>
|
||||
)
|
||||
.toJSON()
|
||||
).toMatchSnapshot();
|
||||
});
|
@ -64,11 +64,13 @@ it('renders <Summary state /> without throwing', () => {
|
||||
it('renders <Summary instance /> without throwing', () => {
|
||||
const instance1 = {
|
||||
id: '2252839a-e698-ceec-afac-9549ad0c6624',
|
||||
// eslint-disable-next-line camelcase
|
||||
compute_node: '70bb1cee-dba3-11e3-a799-002590e4f2b0',
|
||||
image: {
|
||||
id: '19aa3328-0025-11e7-a19a-c39077bfd4cf',
|
||||
name: 'Alpine 3'
|
||||
},
|
||||
// eslint-disable-next-line camelcase
|
||||
primary_ip: '72.2.119.146',
|
||||
ips: ['72.2.119.146', '10.112.5.63'],
|
||||
package: {
|
||||
@ -90,10 +92,12 @@ it('renders <Summary instance /> without throwing', () => {
|
||||
|
||||
const instance2 = {
|
||||
id: '2252839a-e698-ceec-afac-9549ad0c6624',
|
||||
// eslint-disable-next-line camelcase
|
||||
compute_node: '70bb1cee-dba3-11e3-a799-002590e4f2b0',
|
||||
image: {
|
||||
id: '19aa3328-0025-11e7-a19a-c39077bfd4cf'
|
||||
},
|
||||
// eslint-disable-next-line camelcase
|
||||
primary_ip: '72.2.119.146',
|
||||
ips: ['72.2.119.146', '10.112.5.63'],
|
||||
package: {
|
||||
|
@ -1,43 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Field } from 'redux-form';
|
||||
|
||||
import {
|
||||
FormGroup,
|
||||
FormLabel,
|
||||
Input,
|
||||
Button,
|
||||
Message,
|
||||
MessageTitle,
|
||||
MessageDescription
|
||||
} from 'joyent-ui-toolkit';
|
||||
|
||||
export default ({
|
||||
submitting = false,
|
||||
error,
|
||||
handleSubmit = () => {},
|
||||
onCancel = () => {}
|
||||
}) => {
|
||||
const _error = error &&
|
||||
!submitting && (
|
||||
<Message error>
|
||||
<MessageTitle>Ooops!</MessageTitle>
|
||||
<MessageDescription>{error}</MessageDescription>
|
||||
</Message>
|
||||
);
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
{_error}
|
||||
<FormGroup name="name" field={Field}>
|
||||
<FormLabel>Name (optional)</FormLabel>
|
||||
<Input placeholder="Snapshot name" />
|
||||
</FormGroup>
|
||||
<Button type="button" disabled={submitting} onClick={onCancel} secondary>
|
||||
Back
|
||||
</Button>
|
||||
<Button type="submit" disabled={submitting} loading={submitting}>
|
||||
Create
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
};
|
168
packages/my-joy-beta/src/components/instances/footer.js
Normal file
168
packages/my-joy-beta/src/components/instances/footer.js
Normal file
@ -0,0 +1,168 @@
|
||||
import React from 'react';
|
||||
import { withTheme } from 'styled-components';
|
||||
|
||||
import {
|
||||
Row,
|
||||
Col,
|
||||
Button,
|
||||
Footer,
|
||||
QueryBreakpoints,
|
||||
StartIcon,
|
||||
StopIcon,
|
||||
ResetIcon,
|
||||
DeleteIcon
|
||||
} from 'joyent-ui-toolkit';
|
||||
|
||||
const { SmallOnly, Medium } = QueryBreakpoints;
|
||||
|
||||
export default withTheme(
|
||||
({
|
||||
submitting = false,
|
||||
statuses = {},
|
||||
allowedActions = {},
|
||||
onStart,
|
||||
onStop,
|
||||
onReboot,
|
||||
onRemove,
|
||||
theme = {}
|
||||
}) => (
|
||||
<Footer fixed bottom>
|
||||
<Row between="xs" middle="xs">
|
||||
<Col xs={7}>
|
||||
{onStart && [
|
||||
<SmallOnly key="small-only">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={onStart}
|
||||
disabled={submitting || !allowedActions.start}
|
||||
loading={submitting && statuses.starting}
|
||||
secondary
|
||||
small
|
||||
icon
|
||||
>
|
||||
<StartIcon disabled={submitting || !allowedActions.start} />
|
||||
</Button>
|
||||
</SmallOnly>,
|
||||
<Medium key="medium">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={onStart}
|
||||
disabled={submitting || !allowedActions.start}
|
||||
loading={submitting && statuses.starting}
|
||||
secondary
|
||||
icon
|
||||
>
|
||||
<StartIcon disabled={submitting || !allowedActions.start} />
|
||||
<span>Start</span>
|
||||
</Button>
|
||||
</Medium>
|
||||
]}
|
||||
{onStop && [
|
||||
<SmallOnly key="small-only">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={onStop}
|
||||
disabled={submitting || !allowedActions.stop}
|
||||
loading={submitting && statuses.stopping}
|
||||
secondary
|
||||
small
|
||||
icon
|
||||
>
|
||||
<StopIcon disabled={submitting || !allowedActions.stop} />
|
||||
</Button>
|
||||
</SmallOnly>,
|
||||
<Medium key="medium">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={onStop}
|
||||
disabled={submitting || !allowedActions.stop}
|
||||
loading={submitting && statuses.stopping}
|
||||
secondary
|
||||
icon
|
||||
>
|
||||
<StopIcon disabled={submitting || !allowedActions.stop} />
|
||||
<span>Stop</span>
|
||||
</Button>
|
||||
</Medium>
|
||||
]}
|
||||
{onReboot && [
|
||||
<SmallOnly key="small-only">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={onReboot}
|
||||
disabled={submitting || !allowedActions.reboot}
|
||||
loading={submitting && statuses.rebooting}
|
||||
secondary
|
||||
small
|
||||
icon
|
||||
>
|
||||
<ResetIcon disabled={submitting || !allowedActions.reboot} />
|
||||
</Button>
|
||||
</SmallOnly>,
|
||||
<Medium key="medium">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={onReboot}
|
||||
disabled={submitting || !allowedActions.reboot}
|
||||
loading={submitting && statuses.rebooting}
|
||||
secondary
|
||||
icon
|
||||
>
|
||||
<ResetIcon disabled={submitting || !allowedActions.reboot} />
|
||||
<span>Reboot</span>
|
||||
</Button>
|
||||
</Medium>
|
||||
]}
|
||||
</Col>
|
||||
{onRemove && (
|
||||
<Col xs={5}>
|
||||
<SmallOnly key="small-only">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={onRemove}
|
||||
disabled={submitting || !allowedActions.remove}
|
||||
loading={submitting && statuses.removing}
|
||||
secondary
|
||||
error
|
||||
right
|
||||
small
|
||||
icon
|
||||
>
|
||||
<DeleteIcon
|
||||
disabled={submitting}
|
||||
fill={
|
||||
!(submitting || !allowedActions.remove)
|
||||
? theme.red
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</Button>
|
||||
</SmallOnly>
|
||||
<Medium key="medium">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={onRemove}
|
||||
disabled={submitting || !allowedActions.remove}
|
||||
loading={submitting && statuses.removing}
|
||||
error
|
||||
secondary
|
||||
right
|
||||
icon
|
||||
>
|
||||
<DeleteIcon
|
||||
disabled={submitting || !allowedActions.remove}
|
||||
fill={
|
||||
!(submitting || !allowedActions.remove)
|
||||
? theme.red
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<span>Remove</span>
|
||||
</Button>
|
||||
</Medium>
|
||||
</Col>
|
||||
)}
|
||||
</Row>
|
||||
</Footer>
|
||||
)
|
||||
);
|
@ -3,5 +3,4 @@ export { default as KeyValue } from './key-value';
|
||||
export { default as Network } from './network';
|
||||
export { default as FirewallRule } from './firewall-rule';
|
||||
export { default as Resize } from './resize';
|
||||
export { default as CreateSnapshot } from './create-snapshot';
|
||||
export { default as Snapshots } from './snapshots';
|
||||
|
@ -98,6 +98,18 @@ const InputKeyValue = ({ type, submitting }) => (
|
||||
</Flex>
|
||||
);
|
||||
|
||||
const InputName = ({ type, submitting }) => (
|
||||
<Flex justifyStart contentStretch>
|
||||
<FlexItem basis="auto">
|
||||
<FormGroup name="name" field={Field} fluid>
|
||||
<FormLabel>{titleCase(type)} Name</FormLabel>
|
||||
<Input type="text" disabled={submitting} />
|
||||
<FormMeta />
|
||||
</FormGroup>
|
||||
</FlexItem>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
export const KeyValue = ({
|
||||
input = 'input',
|
||||
type = 'metadata',
|
||||
@ -111,7 +123,9 @@ export const KeyValue = ({
|
||||
onToggleExpanded = () => null,
|
||||
onCancel = () => null,
|
||||
onRemove = () => null,
|
||||
theme = {}
|
||||
theme = {},
|
||||
onlyName = false,
|
||||
noRemove = false
|
||||
}) => {
|
||||
const handleHeaderClick = method === 'edit' && onToggleExpanded;
|
||||
|
||||
@ -124,7 +138,7 @@ export const KeyValue = ({
|
||||
onClick={handleHeaderClick}
|
||||
>
|
||||
<CardHeaderMeta>
|
||||
{method === 'add' ? (
|
||||
{method === 'add' || method === 'create' ? (
|
||||
<H4>{`${titleCase(method)} ${type}`}</H4>
|
||||
) : (
|
||||
<CollapsedKeyValue>
|
||||
@ -132,8 +146,9 @@ export const KeyValue = ({
|
||||
name="name"
|
||||
type="text"
|
||||
component={({ input = {} }) =>
|
||||
!expanded ? `${input.value}: ` : <b>{`${input.value}: `}</b>}
|
||||
/>
|
||||
!expanded ? `${input.value}: ` : <b>{`${input.value}: `}</b>
|
||||
}
|
||||
/>,
|
||||
<Field
|
||||
name="value"
|
||||
type="text"
|
||||
@ -157,7 +172,11 @@ export const KeyValue = ({
|
||||
</Row>
|
||||
) : null}
|
||||
{input === 'input' ? (
|
||||
onlyName ? (
|
||||
<InputName type={type} submitting={submitting} />
|
||||
) : (
|
||||
<InputKeyValue type={type} submitting={submitting} />
|
||||
)
|
||||
) : (
|
||||
<TextareaKeyValue type={type} submitting={submitting} />
|
||||
)}
|
||||
@ -181,6 +200,7 @@ export const KeyValue = ({
|
||||
<span>{method === 'add' ? 'Create' : 'Save'}</span>
|
||||
</Button>
|
||||
</Col>
|
||||
{!noRemove && (
|
||||
<Col xs={method === 'add' ? false : 5}>
|
||||
<Button
|
||||
type="button"
|
||||
@ -200,6 +220,7 @@ export const KeyValue = ({
|
||||
<span>Delete</span>
|
||||
</Button>
|
||||
</Col>
|
||||
)}
|
||||
</Row>
|
||||
</Padding>
|
||||
</CardOutlet>
|
||||
|
@ -6,36 +6,25 @@ import { Link } from 'react-router-dom';
|
||||
import { Field } from 'redux-form';
|
||||
|
||||
import {
|
||||
Row,
|
||||
Col,
|
||||
Anchor,
|
||||
FormGroup,
|
||||
Checkbox,
|
||||
Button,
|
||||
Table,
|
||||
TableThead,
|
||||
TableTr,
|
||||
TableTh,
|
||||
TableTd,
|
||||
TableTbody,
|
||||
Footer,
|
||||
StatusLoader,
|
||||
Popover,
|
||||
PopoverContainer,
|
||||
PopoverTarget,
|
||||
PopoverItem,
|
||||
PopoverDivider,
|
||||
QueryBreakpoints,
|
||||
DotIcon,
|
||||
StartIcon,
|
||||
StopIcon,
|
||||
ResetIcon,
|
||||
DeleteIcon,
|
||||
ActionsIcon
|
||||
} from 'joyent-ui-toolkit';
|
||||
|
||||
const { SmallOnly, Medium } = QueryBreakpoints;
|
||||
|
||||
const stateColor = {
|
||||
PROVISIONING: 'primary',
|
||||
RUNNING: 'green',
|
||||
@ -45,131 +34,6 @@ const stateColor = {
|
||||
FAILED: 'red'
|
||||
};
|
||||
|
||||
export const Actions = ({
|
||||
submitting = false,
|
||||
statuses = {},
|
||||
allowedActions = {},
|
||||
onStart = () => null,
|
||||
onStop = () => null,
|
||||
onReboot = () => null,
|
||||
onDelete = () => null
|
||||
}) => (
|
||||
<Footer fixed bottom>
|
||||
<Row between="xs" middle="xs">
|
||||
<Col xs={7}>
|
||||
<SmallOnly key="small-only">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={onStart}
|
||||
disabled={submitting || !allowedActions.start}
|
||||
loading={submitting && statuses.starting}
|
||||
secondary
|
||||
small
|
||||
icon
|
||||
>
|
||||
<StartIcon disabled={submitting || !allowedActions.start} />
|
||||
</Button>
|
||||
</SmallOnly>
|
||||
<Medium key="medium">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={onStart}
|
||||
disabled={submitting || !allowedActions.start}
|
||||
loading={submitting && statuses.starting}
|
||||
secondary
|
||||
icon
|
||||
>
|
||||
<StartIcon disabled={submitting || !allowedActions.start} />
|
||||
<span>Start</span>
|
||||
</Button>
|
||||
</Medium>
|
||||
<SmallOnly key="small-only">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={onStop}
|
||||
disabled={submitting || !allowedActions.stop}
|
||||
loading={submitting && statuses.stopping}
|
||||
secondary
|
||||
small
|
||||
icon
|
||||
>
|
||||
<StopIcon disabled={submitting || !allowedActions.stop} />
|
||||
</Button>
|
||||
</SmallOnly>
|
||||
<Medium key="medium">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={onStop}
|
||||
disabled={submitting || !allowedActions.stop}
|
||||
loading={submitting && statuses.stopping}
|
||||
secondary
|
||||
icon
|
||||
>
|
||||
<StopIcon disabled={submitting || !allowedActions.stop} />
|
||||
<span>Stop</span>
|
||||
</Button>
|
||||
</Medium>
|
||||
<SmallOnly key="small-only">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={onReboot}
|
||||
disabled={submitting || !allowedActions.reboot}
|
||||
loading={submitting && statuses.rebooting}
|
||||
secondary
|
||||
small
|
||||
icon
|
||||
>
|
||||
<ResetIcon disabled={submitting || !allowedActions.reboot} />
|
||||
</Button>
|
||||
</SmallOnly>
|
||||
<Medium key="medium">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={onReboot}
|
||||
disabled={submitting || !allowedActions.reboot}
|
||||
loading={submitting && statuses.rebooting}
|
||||
secondary
|
||||
icon
|
||||
>
|
||||
<ResetIcon disabled={submitting || !allowedActions.reboot} />
|
||||
<span>Reboot</span>
|
||||
</Button>
|
||||
</Medium>
|
||||
</Col>
|
||||
<Col xs={5}>
|
||||
<SmallOnly key="small-only">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={onDelete}
|
||||
disabled={submitting}
|
||||
loading={submitting && statuses.deleting}
|
||||
secondary
|
||||
right
|
||||
small
|
||||
icon
|
||||
>
|
||||
<DeleteIcon disabled={submitting} />
|
||||
</Button>
|
||||
</SmallOnly>
|
||||
<Medium key="medium">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={onDelete}
|
||||
disabled={submitting}
|
||||
loading={submitting && statuses.deleting}
|
||||
secondary
|
||||
right
|
||||
icon
|
||||
>
|
||||
<DeleteIcon disabled={submitting} />
|
||||
<span>Delete</span>
|
||||
</Button>
|
||||
</Medium>
|
||||
</Col>
|
||||
</Row>
|
||||
</Footer>
|
||||
);
|
||||
|
||||
export const Item = ({
|
||||
id = '',
|
||||
name,
|
||||
|
@ -1,22 +1,22 @@
|
||||
import React from 'react';
|
||||
import { Row, Col } from 'react-styled-flexboxgrid';
|
||||
import forceArray from 'force-array';
|
||||
import find from 'lodash.find';
|
||||
import { Field } from 'redux-form';
|
||||
import titleCase from 'title-case';
|
||||
import moment from 'moment';
|
||||
import remcalc from 'remcalc';
|
||||
import InstanceListActions from '@components/instances/footer';
|
||||
import { KeyValue } from '@components/instances';
|
||||
import ReduxForm from 'declarative-redux-form';
|
||||
import { Margin } from 'styled-components-spacing';
|
||||
|
||||
import {
|
||||
FormGroup,
|
||||
Input,
|
||||
FormLabel,
|
||||
ViewContainer,
|
||||
StatusLoader,
|
||||
Select,
|
||||
Message,
|
||||
MessageTitle,
|
||||
MessageDescription,
|
||||
Button,
|
||||
QueryBreakpoints,
|
||||
Table,
|
||||
TableThead,
|
||||
TableTr,
|
||||
@ -24,173 +24,174 @@ import {
|
||||
TableTbody,
|
||||
TableTd,
|
||||
Checkbox,
|
||||
P
|
||||
Popover,
|
||||
PopoverContainer,
|
||||
PopoverTarget,
|
||||
PopoverItem,
|
||||
ActionsIcon,
|
||||
DotIcon
|
||||
} from 'joyent-ui-toolkit';
|
||||
|
||||
const { SmallOnly, Medium } = QueryBreakpoints;
|
||||
const stateColor = {
|
||||
QUEUED: 'primary',
|
||||
CREATED: 'green'
|
||||
};
|
||||
|
||||
const Item = ({ name, state, created }) => (
|
||||
export const Item = ({
|
||||
name,
|
||||
state,
|
||||
created,
|
||||
onStart,
|
||||
onRemove,
|
||||
updated,
|
||||
mutating
|
||||
}) => (
|
||||
<TableTr>
|
||||
<TableTd center middle>
|
||||
<FormGroup name={name} field={Field}>
|
||||
<Checkbox />
|
||||
{!mutating ? (
|
||||
[
|
||||
<TableTd padding="0" paddingLeft={remcalc(12)} middle left>
|
||||
<FormGroup paddingTop={remcalc(4)} name={name} field={Field}>
|
||||
<Checkbox noMargin />
|
||||
</FormGroup>
|
||||
</TableTd>,
|
||||
<TableTd middle left>
|
||||
{name}
|
||||
</TableTd>,
|
||||
<TableTd middle left>
|
||||
<DotIcon
|
||||
width={remcalc(11)}
|
||||
height={remcalc(11)}
|
||||
borderRadius={remcalc(11)}
|
||||
color={stateColor[state]}
|
||||
/>{' '}
|
||||
{titleCase(state)}
|
||||
</TableTd>,
|
||||
<TableTd xs="0" sm="160" middle left>
|
||||
{moment.unix(created).fromNow()}
|
||||
</TableTd>,
|
||||
<TableTd xs="0" sm="160" middle left>
|
||||
{moment.unix(updated).fromNow()}
|
||||
</TableTd>,
|
||||
<PopoverContainer clickable>
|
||||
<TableTd padding="0" hasBorder="left">
|
||||
<PopoverTarget box>
|
||||
<ActionsIcon />
|
||||
</PopoverTarget>
|
||||
<Popover placement="top">
|
||||
<PopoverItem onClick={onStart}>Start</PopoverItem>
|
||||
<PopoverItem onClick={onRemove}>Remove</PopoverItem>
|
||||
</Popover>
|
||||
</TableTd>
|
||||
<TableTd>{name}</TableTd>
|
||||
<TableTd>{moment.unix(created).fromNow()}</TableTd>
|
||||
</PopoverContainer>
|
||||
]
|
||||
) : (
|
||||
<TableTd colSpan="6">
|
||||
<StatusLoader />
|
||||
</TableTd>
|
||||
)}
|
||||
</TableTr>
|
||||
);
|
||||
|
||||
export const AddForm = props => (
|
||||
<KeyValue
|
||||
{...props}
|
||||
method="create"
|
||||
input="input"
|
||||
type="snapshot"
|
||||
expanded
|
||||
onlyName
|
||||
noRemove
|
||||
/>
|
||||
);
|
||||
|
||||
export default ({
|
||||
snapshots = [],
|
||||
selected = [],
|
||||
loading,
|
||||
error,
|
||||
handleChange = () => null,
|
||||
onAction = () => null,
|
||||
handleSubmit,
|
||||
submitting = false,
|
||||
pristine = true,
|
||||
sortBy = 'name',
|
||||
sortOrder = 'desc',
|
||||
onSortBy = () => null,
|
||||
allSelected = false,
|
||||
toggleSelectAll = () => null,
|
||||
onStart,
|
||||
onRemove,
|
||||
...rest
|
||||
}) => {
|
||||
const allowedActions = {
|
||||
delete: selected.length > 0,
|
||||
start: selected.length === 1
|
||||
};
|
||||
|
||||
const handleActions = ev => {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
|
||||
onAction({
|
||||
name: ev.target.value,
|
||||
items: selected
|
||||
});
|
||||
};
|
||||
|
||||
const _snapshots = forceArray(snapshots);
|
||||
|
||||
const _loading = !_snapshots.length &&
|
||||
loading && (
|
||||
<ViewContainer center>
|
||||
<StatusLoader />
|
||||
</ViewContainer>
|
||||
);
|
||||
|
||||
const items = _snapshots.map(snapshot => {
|
||||
const { name } = snapshot;
|
||||
const isSelected = Boolean(find(selected, ['name', name]));
|
||||
const isSubmitting = isSelected && submitting;
|
||||
|
||||
return {
|
||||
...snapshot,
|
||||
isSubmitting,
|
||||
isSelected
|
||||
};
|
||||
});
|
||||
|
||||
const _error = error &&
|
||||
!submitting && (
|
||||
<Message error>
|
||||
<MessageTitle>Ooops!</MessageTitle>
|
||||
<MessageDescription>{error}</MessageDescription>
|
||||
</Message>
|
||||
);
|
||||
|
||||
const _table = !items.length ? null : (
|
||||
return (
|
||||
<Table>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh xs="48" />
|
||||
<TableTh left bottom>
|
||||
<P>Name</P>
|
||||
<TableTh xs="32" padding="0" paddingLeft={remcalc(12)} middle left>
|
||||
<FormGroup paddingTop={remcalc(4)}>
|
||||
<Checkbox
|
||||
checked={allSelected}
|
||||
disabled={submitting}
|
||||
onChange={toggleSelectAll}
|
||||
noMargin
|
||||
/>
|
||||
</FormGroup>
|
||||
</TableTh>
|
||||
<TableTh xs="120" left bottom>
|
||||
<P>Created</P>
|
||||
<TableTh
|
||||
onClick={() => onSortBy('name', sortOrder)}
|
||||
sortOrder={sortOrder}
|
||||
showSort={sortBy === 'name'}
|
||||
left
|
||||
middle
|
||||
actionable
|
||||
>
|
||||
<span>Name </span>
|
||||
</TableTh>
|
||||
<TableTh
|
||||
xs="150"
|
||||
onClick={() => onSortBy('state', sortOrder)}
|
||||
sortOrder={sortOrder}
|
||||
showSort={sortBy === 'state'}
|
||||
left
|
||||
middle
|
||||
actionable
|
||||
>
|
||||
<span>Status </span>
|
||||
</TableTh>
|
||||
<TableTh
|
||||
xs="0"
|
||||
sm="160"
|
||||
onClick={() => onSortBy('created', sortOrder)}
|
||||
sortOrder={sortOrder}
|
||||
showSort={sortBy === 'created'}
|
||||
left
|
||||
middle
|
||||
actionable
|
||||
>
|
||||
<span>Created </span>
|
||||
</TableTh>
|
||||
<TableTh
|
||||
xs="0"
|
||||
sm="160"
|
||||
onClick={() => onSortBy('updated', sortOrder)}
|
||||
sortOrder={sortOrder}
|
||||
showSort={sortBy === 'updated'}
|
||||
left
|
||||
middle
|
||||
actionable
|
||||
>
|
||||
<span>Updated </span>
|
||||
</TableTh>
|
||||
<TableTh xs="60" padding="0" />
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{items.map(snapshot => <Item key={snapshot.name} {...snapshot} />)}
|
||||
{snapshots.map(snapshot => (
|
||||
<Item
|
||||
onStart={() => onStart(snapshot)}
|
||||
onRemove={() => onRemove(snapshot)}
|
||||
key={snapshot.id}
|
||||
{...snapshot}
|
||||
/>
|
||||
))}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
);
|
||||
|
||||
return (
|
||||
<form
|
||||
onChange={() => handleSubmit(ctx => handleChange(ctx))}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<Row between="xs">
|
||||
<Col xs={8} sm={8} lg={6}>
|
||||
<Row>
|
||||
<Col xs={7} sm={7} md={6} lg={6}>
|
||||
<FormGroup name="filter" field={Field}>
|
||||
<FormLabel>Filter snapshots</FormLabel>
|
||||
<Input
|
||||
placeholder="Search for name or state"
|
||||
disabled={pristine && !items.length}
|
||||
fluid
|
||||
/>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
<Col xs={5} sm={3} lg={3}>
|
||||
<FormGroup name="sort" field={Field}>
|
||||
<FormLabel>Sort</FormLabel>
|
||||
<Select disabled={!items.length} fluid>
|
||||
<option value="name">Name</option>
|
||||
<option value="state">State</option>
|
||||
<option value="created">Created</option>
|
||||
<option value="updated">Updated</option>
|
||||
</Select>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
<Col xs={4} sm={4} lg={6}>
|
||||
<Row end="xs">
|
||||
<Col xs={6} sm={4} md={3} lg={2}>
|
||||
<FormGroup>
|
||||
<FormLabel>⁣</FormLabel>
|
||||
<Select
|
||||
value="actions"
|
||||
disabled={!items.length || !selected.length}
|
||||
onChange={handleActions}
|
||||
fluid
|
||||
>
|
||||
<option value="actions" selected disabled>
|
||||
≡
|
||||
</option>
|
||||
<option value="delete" disabled={!allowedActions.delete}>
|
||||
Delete
|
||||
</option>
|
||||
<option value="start" disabled={!allowedActions.start}>
|
||||
Start
|
||||
</option>
|
||||
</Select>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
<Col xs={6} sm={6} md={5} lg={2}>
|
||||
<FormGroup>
|
||||
<FormLabel>⁣</FormLabel>
|
||||
<Button
|
||||
type="button"
|
||||
small
|
||||
icon
|
||||
fluid
|
||||
onClick={() => onAction({ name: 'create' })}
|
||||
>
|
||||
<SmallOnly>+</SmallOnly>
|
||||
<Medium>Create</Medium>
|
||||
</Button>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
{_loading}
|
||||
{_error}
|
||||
{_table}
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -4574,6 +4574,7 @@ exports[`renders <Metadata metadata /> without throwing 1`] = `
|
||||
<b>
|
||||
undefined:
|
||||
</b>
|
||||
,
|
||||
<span />
|
||||
</span>
|
||||
</div>
|
||||
@ -4764,6 +4765,7 @@ exports[`renders <Metadata metadata /> without throwing 1`] = `
|
||||
<b>
|
||||
undefined:
|
||||
</b>
|
||||
,
|
||||
<span />
|
||||
</span>
|
||||
</div>
|
||||
@ -4954,6 +4956,7 @@ exports[`renders <Metadata metadata /> without throwing 1`] = `
|
||||
<b>
|
||||
undefined:
|
||||
</b>
|
||||
,
|
||||
<span />
|
||||
</span>
|
||||
</div>
|
||||
|
@ -3171,6 +3171,7 @@ exports[`renders <Tags editing /> without throwing 1`] = `
|
||||
<b>
|
||||
undefined:
|
||||
</b>
|
||||
,
|
||||
<span />
|
||||
</span>
|
||||
</div>
|
||||
@ -4672,6 +4673,7 @@ exports[`renders <Tags editing.removing /> without throwing 1`] = `
|
||||
<b>
|
||||
undefined:
|
||||
</b>
|
||||
,
|
||||
<span />
|
||||
</span>
|
||||
</div>
|
||||
|
@ -54,22 +54,26 @@ it('renders <Metadata addOpen /> without throwing', () => {
|
||||
});
|
||||
|
||||
it('renders <Metadata metadata /> without throwing', () => {
|
||||
const metadata = [{
|
||||
const metadata = [
|
||||
{
|
||||
name: 'name1',
|
||||
value: 'value1',
|
||||
id: 'name1-value1'
|
||||
}, {
|
||||
},
|
||||
{
|
||||
name: 'name2',
|
||||
value: 'value2',
|
||||
id: 'name2-value2',
|
||||
expanded: true
|
||||
}, {
|
||||
},
|
||||
{
|
||||
name: 'name3',
|
||||
value: 'value3',
|
||||
id: 'name3-value3',
|
||||
expanded: true,
|
||||
removing: true
|
||||
}];
|
||||
}
|
||||
];
|
||||
|
||||
expect(
|
||||
renderer
|
||||
|
@ -68,11 +68,13 @@ it('renders <Summary starting stopping rebooting deleting /> without throwing',
|
||||
it('renders <Summary starting stopping rebooting deleting /> without throwing', () => {
|
||||
const instance1 = {
|
||||
id: '2252839a-e698-ceec-afac-9549ad0c6624',
|
||||
// eslint-disable-next-line camelcase
|
||||
compute_node: '70bb1cee-dba3-11e3-a799-002590e4f2b0',
|
||||
image: {
|
||||
id: '19aa3328-0025-11e7-a19a-c39077bfd4cf',
|
||||
name: 'Alpine 3'
|
||||
},
|
||||
// eslint-disable-next-line camelcase
|
||||
primary_ip: '72.2.119.146',
|
||||
ips: ['72.2.119.146', '10.112.5.63'],
|
||||
package: {
|
||||
@ -94,10 +96,12 @@ it('renders <Summary starting stopping rebooting deleting /> without throwing',
|
||||
|
||||
const instance2 = {
|
||||
id: '2252839a-e698-ceec-afac-9549ad0c6624',
|
||||
// eslint-disable-next-line camelcase
|
||||
compute_node: '70bb1cee-dba3-11e3-a799-002590e4f2b0',
|
||||
image: {
|
||||
id: '19aa3328-0025-11e7-a19a-c39077bfd4cf'
|
||||
},
|
||||
// eslint-disable-next-line camelcase
|
||||
primary_ip: '72.2.119.146',
|
||||
ips: ['72.2.119.146', '10.112.5.63'],
|
||||
package: {
|
||||
@ -117,4 +121,3 @@ it('renders <Summary starting stopping rebooting deleting /> without throwing',
|
||||
.toJSON()
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
||||
|
@ -93,19 +93,23 @@ it('renders <Tags editing.removing /> without throwing', () => {
|
||||
});
|
||||
|
||||
it('renders <Tags tags /> without throwing', () => {
|
||||
const tags = [{
|
||||
const tags = [
|
||||
{
|
||||
name: 'name1',
|
||||
value: 'value1',
|
||||
id: 'name1-value1'
|
||||
}, {
|
||||
},
|
||||
{
|
||||
name: 'name2',
|
||||
value: 'value2',
|
||||
id: 'name2-value2'
|
||||
}, {
|
||||
},
|
||||
{
|
||||
name: 'name3',
|
||||
value: 'value3',
|
||||
id: 'name3-value3'
|
||||
}];
|
||||
}
|
||||
];
|
||||
|
||||
expect(
|
||||
renderer
|
||||
|
@ -1,91 +0,0 @@
|
||||
import React from 'react';
|
||||
import { reduxForm, SubmissionError } from 'redux-form';
|
||||
import { connect } from 'react-redux';
|
||||
import { compose, graphql } from 'react-apollo';
|
||||
import find from 'lodash.find';
|
||||
import get from 'lodash.get';
|
||||
|
||||
import {
|
||||
ViewContainer,
|
||||
StatusLoader,
|
||||
Message,
|
||||
MessageTitle,
|
||||
MessageDescription
|
||||
} from 'joyent-ui-toolkit';
|
||||
|
||||
import { CreateSnapshot as InstanceCreateSnapshot } from '@components/instances';
|
||||
import CreateSnapshotMutation from '@graphql/create-snapshot.gql';
|
||||
import GetInstance from '@graphql/get-instance.gql';
|
||||
|
||||
const CreateSnapshot = ({
|
||||
match,
|
||||
instance,
|
||||
loading,
|
||||
error,
|
||||
handleSubmit,
|
||||
handleCancel
|
||||
}) => {
|
||||
const _loading = !(loading && !instance) ? null : <StatusLoader />;
|
||||
|
||||
const CreateSnapshotForm = reduxForm({
|
||||
form: `create-snapshot-${match.params.instance}`
|
||||
})(InstanceCreateSnapshot);
|
||||
|
||||
const _error = error &&
|
||||
!instance &&
|
||||
!_loading && (
|
||||
<Message error>
|
||||
<MessageTitle>Ooops!</MessageTitle>
|
||||
<MessageDescription>
|
||||
An error occurred while loading your instance
|
||||
</MessageDescription>
|
||||
</Message>
|
||||
);
|
||||
|
||||
const _form = !loading &&
|
||||
!_error && (
|
||||
<CreateSnapshotForm onSubmit={handleSubmit} onCancel={handleCancel} />
|
||||
);
|
||||
|
||||
return (
|
||||
<ViewContainer center={Boolean(_loading)} main>
|
||||
{_loading}
|
||||
{_error}
|
||||
{_form}
|
||||
</ViewContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default compose(
|
||||
graphql(CreateSnapshotMutation, { name: 'createSnapshot' }),
|
||||
graphql(GetInstance, {
|
||||
options: ({ match }) => ({
|
||||
variables: {
|
||||
name: get(match, 'params.instance')
|
||||
}
|
||||
}),
|
||||
props: ({ data: { loading, error, variables, ...rest } }) => ({
|
||||
instance: find(get(rest, 'machines', []), ['name', variables.name]),
|
||||
loading,
|
||||
error
|
||||
})
|
||||
}),
|
||||
connect(
|
||||
null,
|
||||
(dispatch, { history, location, instance, createSnapshot }) => ({
|
||||
handleCancel: () => history.push(location.pathname.split(/\/~/).shift()),
|
||||
handleSubmit: ({ name }) =>
|
||||
createSnapshot({
|
||||
variables: { name, id: instance.id }
|
||||
})
|
||||
.catch(error => {
|
||||
throw new SubmissionError({
|
||||
_error: error.graphQLErrors
|
||||
.map(({ message }) => message)
|
||||
.join('\n')
|
||||
});
|
||||
})
|
||||
.then(() => history.push(`/instances/${instance.name}/snapshots`))
|
||||
})
|
||||
)
|
||||
)(CreateSnapshot);
|
@ -15,7 +15,9 @@ import {
|
||||
import ListDNS from '@graphql/list-dns.gql';
|
||||
|
||||
const DNS = ({ instance, loading, error }) => {
|
||||
// eslint-disable-next-line camelcase
|
||||
const { name, dns_names } = instance || {};
|
||||
// eslint-disable-next-line camelcase
|
||||
const _loading = loading && !name && !dns_names && <StatusLoader />;
|
||||
const _summary = !_loading &&
|
||||
instance && <pre>{JSON.stringify(dns_names, null, 2)}</pre>;
|
||||
|
@ -7,4 +7,3 @@ export { default as Firewall } from './firewall';
|
||||
export { default as Dns } from './dns';
|
||||
export { default as Snapshots } from './snapshots';
|
||||
export { default as Resize } from './resize';
|
||||
export { default as CreateSnapshot } from './create-snapshot';
|
||||
|
@ -35,10 +35,11 @@ import parseError from '@state/parse-error';
|
||||
|
||||
import {
|
||||
default as InstanceList,
|
||||
Item as InstanceListItem,
|
||||
Actions as InstanceListActions
|
||||
Item as InstanceListItem
|
||||
} from '@components/instances/list';
|
||||
|
||||
import InstanceListActions from '@components/instances/footer';
|
||||
|
||||
const TABLE_FORM_NAME = 'instance-list-table';
|
||||
const MENU_FORM_NAME = 'instance-list-menu';
|
||||
|
||||
|
@ -2,42 +2,67 @@ import React from 'react';
|
||||
import moment from 'moment';
|
||||
import forceArray from 'force-array';
|
||||
import { connect } from 'react-redux';
|
||||
import { reduxForm, stopSubmit, startSubmit, change, reset } from 'redux-form';
|
||||
import { compose, graphql } from 'react-apollo';
|
||||
import find from 'lodash.find';
|
||||
import sortBy from 'lodash.sortby';
|
||||
import get from 'lodash.get';
|
||||
|
||||
import { reduxForm, stopSubmit, startSubmit, change } from 'redux-form';
|
||||
import sort from 'lodash.sortby';
|
||||
import { set } from 'react-redux-values';
|
||||
import ReduxForm from 'declarative-redux-form';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { Margin } from 'styled-components-spacing';
|
||||
import intercept from 'apr-intercept';
|
||||
|
||||
import {
|
||||
ViewContainer,
|
||||
Message,
|
||||
MessageTitle,
|
||||
MessageDescription
|
||||
MessageDescription,
|
||||
StatusLoader
|
||||
} from 'joyent-ui-toolkit';
|
||||
|
||||
import GetSnapshots from '@graphql/list-snapshots.gql';
|
||||
import StartSnapshot from '@graphql/start-from-snapshot.gql';
|
||||
import RemoveSnapshot from '@graphql/remove-snapshot.gql';
|
||||
import { Snapshots as SnapshotsList } from '@components/instances';
|
||||
import CreateSnapshotMutation from '@graphql/create-snapshot.gql';
|
||||
import GenIndex from '@state/gen-index';
|
||||
import ToolbarForm from '@components/instances/toolbar';
|
||||
import SnapshotsListActions from '@components/instances/footer';
|
||||
import parseError from '@state/parse-error';
|
||||
|
||||
const SnapshotsListForm = reduxForm({
|
||||
form: `snapshots-list`,
|
||||
initialValues: {
|
||||
sort: 'name'
|
||||
}
|
||||
})(SnapshotsList);
|
||||
import {
|
||||
default as SnapshotsList,
|
||||
AddForm as SnapshotAddForm
|
||||
} from '@components/instances/snapshots';
|
||||
|
||||
const MENU_FORM_NAME = 'snapshot-list-menu';
|
||||
const TABLE_FORM_NAME = 'snapshot-list-table';
|
||||
const CREATE_FORM_NAME = 'create-snapshot-form';
|
||||
|
||||
const Snapshots = ({
|
||||
snapshots = [],
|
||||
instance = {},
|
||||
selected = [],
|
||||
loading,
|
||||
submitting,
|
||||
error,
|
||||
handleAction
|
||||
mutationError,
|
||||
allowedActions,
|
||||
statuses,
|
||||
handleAction,
|
||||
handleCreateSnapshot,
|
||||
sortOrder,
|
||||
handleSortBy,
|
||||
sortBy,
|
||||
toggleSelectAll,
|
||||
toggleCreateSnapshotOpen,
|
||||
createSnapshotOpen
|
||||
}) => {
|
||||
const _values = forceArray(snapshots);
|
||||
const _loading = !_values.length && loading;
|
||||
const _loading = !_values.length && loading ? <StatusLoader /> : null;
|
||||
|
||||
const handleStart = selected => handleAction({ name: 'start', selected });
|
||||
const handleRemove = selected => handleAction({ name: 'remove', selected });
|
||||
|
||||
const _error = error &&
|
||||
!_loading &&
|
||||
@ -50,22 +75,86 @@ const Snapshots = ({
|
||||
</Message>
|
||||
);
|
||||
|
||||
const _createSnapshot =
|
||||
!loading && createSnapshotOpen ? (
|
||||
<ReduxForm form={CREATE_FORM_NAME} onSubmit={handleCreateSnapshot}>
|
||||
{props => (
|
||||
<Margin top={5}>
|
||||
<SnapshotAddForm
|
||||
{...props}
|
||||
onCancel={() => toggleCreateSnapshotOpen(false)}
|
||||
/>
|
||||
</Margin>
|
||||
)}
|
||||
</ReduxForm>
|
||||
) : null;
|
||||
|
||||
const _footer =
|
||||
!loading && selected.length > 0 ? (
|
||||
<SnapshotsListActions
|
||||
submitting={submitting}
|
||||
allowedActions={allowedActions}
|
||||
statuses={statuses}
|
||||
onStart={() => handleStart(selected)}
|
||||
onRemove={() => handleRemove(selected)}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
const _mutationError = mutationError ? (
|
||||
<Message error>
|
||||
<MessageTitle>Ooops!</MessageTitle>
|
||||
<MessageDescription>{mutationError}</MessageDescription>
|
||||
</Message>
|
||||
) : null;
|
||||
|
||||
const _items = !_loading ? (
|
||||
<ReduxForm form={TABLE_FORM_NAME}>
|
||||
{props => (
|
||||
<SnapshotsList
|
||||
snapshots={_values}
|
||||
onStart={snapshot => handleStart([snapshot])}
|
||||
onRemove={snapshot => handleRemove([snapshot])}
|
||||
selected={selected}
|
||||
sortBy={sortBy}
|
||||
sortOrder={sortOrder}
|
||||
onSortBy={handleSortBy}
|
||||
toggleSelectAll={toggleSelectAll}
|
||||
allSelected={_values.length && selected.length === _values.length}
|
||||
/>
|
||||
)}
|
||||
</ReduxForm>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<ViewContainer main>
|
||||
{_error}
|
||||
<SnapshotsListForm
|
||||
snapshots={_values}
|
||||
loading={_loading}
|
||||
onAction={handleAction}
|
||||
selected={selected}
|
||||
<ReduxForm form={MENU_FORM_NAME}>
|
||||
{props => (
|
||||
<ToolbarForm
|
||||
{...props}
|
||||
searchLabel="Filter Snapshots"
|
||||
searchPlaceholder="Search for name, created...."
|
||||
searchable={!_loading}
|
||||
actionLabel="Create Snapshot"
|
||||
actionable={!createSnapshotOpen}
|
||||
onActionClick={() => toggleCreateSnapshotOpen(true)}
|
||||
/>
|
||||
)}
|
||||
</ReduxForm>
|
||||
{_loading}
|
||||
{_error}
|
||||
{_mutationError}
|
||||
{_createSnapshot}
|
||||
{_items}
|
||||
{_footer}
|
||||
</ViewContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default compose(
|
||||
withRouter,
|
||||
graphql(StartSnapshot, { name: 'start' }),
|
||||
graphql(RemoveSnapshot, { name: 'remove' }),
|
||||
graphql(CreateSnapshotMutation, { name: 'createSnapshot' }),
|
||||
graphql(GetSnapshots, {
|
||||
options: ({ match }) => ({
|
||||
pollInterval: 1000,
|
||||
@ -73,19 +162,17 @@ export default compose(
|
||||
name: get(match, 'params.instance')
|
||||
}
|
||||
}),
|
||||
props: ({ data: { loading, error, variables, ...rest } }) => {
|
||||
props: ({ data: { loading, error, variables, refetch, ...rest } }) => {
|
||||
const { name } = variables;
|
||||
const instance = find(get(rest, 'machines', []), ['name', name]);
|
||||
|
||||
const snapshots = get(
|
||||
instance,
|
||||
'snapshots',
|
||||
[]
|
||||
).map(({ created, updated, ...rest }) => ({
|
||||
const snapshots = get(instance, 'snapshots', []).map(
|
||||
({ created, updated, ...rest }) => ({
|
||||
...rest,
|
||||
created: moment.utc(created).unix(),
|
||||
updated: moment.utc(updated).unix()
|
||||
}));
|
||||
})
|
||||
);
|
||||
|
||||
const index = GenIndex(
|
||||
snapshots.map(({ name, ...rest }) => ({ ...rest, id: name }))
|
||||
@ -96,85 +183,230 @@ export default compose(
|
||||
snapshots,
|
||||
instance,
|
||||
loading,
|
||||
error
|
||||
error,
|
||||
refetch
|
||||
};
|
||||
}
|
||||
}),
|
||||
connect(
|
||||
(state, { index, snapshots = [], ...rest }) => {
|
||||
const form = get(state, 'form.snapshots-list.values', {});
|
||||
const filter = get(form, 'filter');
|
||||
const sort = get(form, 'sort');
|
||||
({ form, values }, { index, snapshots = [], ...rest }) => {
|
||||
const tableValues = get(form, `${TABLE_FORM_NAME}.values`) || {};
|
||||
const filter = get(form, `${MENU_FORM_NAME}.values.filter`, false);
|
||||
|
||||
const values = filter
|
||||
// check whether the table form has an error
|
||||
const tableMutationError = get(form, `${TABLE_FORM_NAME}.error`, null);
|
||||
// check whether the create form has an error
|
||||
const createMutationError = get(form, `${CREATE_FORM_NAME}.error`, null);
|
||||
// check whether the main form is submitting
|
||||
const submitting = get(form, `${TABLE_FORM_NAME}.submitting`, false);
|
||||
|
||||
const selected = Object.keys(tableValues)
|
||||
.filter(key => Boolean(tableValues[key]))
|
||||
.map(name => find(snapshots, ['name', name]))
|
||||
.filter(Boolean);
|
||||
|
||||
const sortBy = get(values, 'snapshots-list-sort-by', 'name');
|
||||
const sortOrder = get(values, 'snapshots-list-sort-order', 'asc');
|
||||
const createSnapshotOpen = get(values, 'snapshots-create-open', false);
|
||||
|
||||
// if user is searching something, get items that match that query
|
||||
const filtered = filter
|
||||
? index.search(filter).map(({ ref }) => find(snapshots, ['name', ref]))
|
||||
: snapshots;
|
||||
|
||||
const selected = Object.keys(form)
|
||||
.filter(key => Boolean(form[key]))
|
||||
.map(name => find(values, ['name', name]))
|
||||
.filter(Boolean)
|
||||
.map(({ name }) => find(snapshots, ['name', name]))
|
||||
.filter(Boolean);
|
||||
// from filtered instances, sort asc
|
||||
// set's mutating flag
|
||||
const ascSorted = sort(filtered, [sortBy]).map(({ id, ...item }) => ({
|
||||
...item,
|
||||
id,
|
||||
mutating: get(values, `${id}-mutating`, false)
|
||||
}));
|
||||
|
||||
const allowedActions = {
|
||||
start: selected.length === 1,
|
||||
remove: true
|
||||
};
|
||||
|
||||
// get mutating statuses
|
||||
const statuses = {
|
||||
starting: get(values, 'snapshot-list-starting', false),
|
||||
removing: get(values, 'snapshot-list-removeing', false)
|
||||
};
|
||||
|
||||
return {
|
||||
...rest,
|
||||
snapshots: sortBy(values, value => get(value, sort)),
|
||||
selected
|
||||
snapshots: sortOrder === 'asc' ? ascSorted : ascSorted.reverse(),
|
||||
selected,
|
||||
sortBy,
|
||||
sortOrder,
|
||||
submitting,
|
||||
mutationError: tableMutationError || createMutationError,
|
||||
allowedActions,
|
||||
statuses,
|
||||
createSnapshotOpen
|
||||
};
|
||||
},
|
||||
(dispatch, { create, start, remove, instance, history, match }) => ({
|
||||
handleAction: ({ name, items = [] }) => {
|
||||
const form = 'snapshots-list';
|
||||
(dispatch, ownProps) => {
|
||||
const {
|
||||
create,
|
||||
start,
|
||||
remove,
|
||||
instance,
|
||||
history,
|
||||
match,
|
||||
createSnapshot,
|
||||
refetch
|
||||
} = ownProps;
|
||||
|
||||
const types = {
|
||||
start: () =>
|
||||
Promise.resolve(dispatch(startSubmit(form))).then(() =>
|
||||
Promise.all(
|
||||
items.map(({ name }) =>
|
||||
start({ variables: { id: instance.id, snapshot: name } })
|
||||
)
|
||||
)
|
||||
),
|
||||
delete: () =>
|
||||
Promise.resolve(dispatch(startSubmit(form))).then(() =>
|
||||
Promise.all(
|
||||
items.map(({ name }) =>
|
||||
remove({ variables: { id: instance.id, snapshot: name } })
|
||||
)
|
||||
)
|
||||
),
|
||||
create: () =>
|
||||
Promise.resolve(
|
||||
history.push(`/instances/${instance.name}/snapshots/~create`)
|
||||
)
|
||||
};
|
||||
|
||||
const handleError = error => {
|
||||
dispatch(
|
||||
stopSubmit(form, {
|
||||
_error: error.graphQLErrors
|
||||
.map(({ message }) => message)
|
||||
.join('\n')
|
||||
return {
|
||||
handleSortBy: (newSortBy, sortOrder) => {
|
||||
dispatch([
|
||||
set({
|
||||
name: `snapshots-list-sort-order`,
|
||||
value: sortOrder === 'desc' ? 'asc' : 'desc'
|
||||
}),
|
||||
set({
|
||||
name: `snapshots-list-sort-by`,
|
||||
value: newSortBy
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleSuccess = () => {
|
||||
]);
|
||||
},
|
||||
toggleCreateSnapshotOpen: value =>
|
||||
dispatch(
|
||||
items
|
||||
.map(({ name: field }) => change(form, field, false))
|
||||
.concat([stopSubmit(form)])
|
||||
);
|
||||
};
|
||||
set({
|
||||
name: `snapshots-create-open`,
|
||||
value
|
||||
})
|
||||
),
|
||||
toggleSelectAll: ({ selected = [], snapshots = [] }) => () => {
|
||||
const same = selected.length === snapshots.length;
|
||||
const hasSelected = selected.length > 0;
|
||||
|
||||
return (
|
||||
types[name] &&
|
||||
types[name]()
|
||||
.then(handleSuccess)
|
||||
.catch(handleError)
|
||||
// none are selected, toggle to all
|
||||
if (!hasSelected) {
|
||||
return dispatch(
|
||||
snapshots.map(({ name }) => change(TABLE_FORM_NAME, name, true))
|
||||
);
|
||||
}
|
||||
|
||||
// all are selected, toggle to none
|
||||
if (hasSelected && same) {
|
||||
return dispatch(
|
||||
snapshots.map(({ name }) => change(TABLE_FORM_NAME, name, false))
|
||||
);
|
||||
}
|
||||
|
||||
// some are selected, toggle to all
|
||||
if (hasSelected && !same) {
|
||||
return dispatch(
|
||||
snapshots.map(({ name }) => change(TABLE_FORM_NAME, name, true))
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
handleCreateSnapshot: async ({ name }) => {
|
||||
const [err] = await intercept(
|
||||
createSnapshot({
|
||||
variables: { name, id: instance.id }
|
||||
})
|
||||
);
|
||||
|
||||
if (err) {
|
||||
return dispatch(
|
||||
stopSubmit(form, {
|
||||
_error: parseError(error)
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
dispatch(
|
||||
set({
|
||||
name: `snapshots-create-open`,
|
||||
value: false
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
handleAction: async ({ name, selected = [] }) => {
|
||||
const action = ownProps[name];
|
||||
const gerund = `${name}ing`;
|
||||
|
||||
// flips submitting flag to true so that we can disable everything
|
||||
const flipSubmitTrue = startSubmit(TABLE_FORM_NAME);
|
||||
|
||||
// sets (starting/rebooting/etc) to true so that we can, for instance,
|
||||
// have a spinner on the correct button
|
||||
const setIngTrue = set({
|
||||
name: `snapshot-list-${gerund}`,
|
||||
value: true
|
||||
});
|
||||
|
||||
// sets the individual item mutation flags so that we can show a
|
||||
// spinner in the row
|
||||
const setMutatingTrue = selected.map(({ id }) =>
|
||||
set({ name: `${id}-mutating`, value: true })
|
||||
);
|
||||
|
||||
// wait for everything to finish and catch the error
|
||||
const [err] = await intercept(
|
||||
Promise.resolve(
|
||||
dispatch([flipSubmitTrue, setIngTrue, ...setMutatingTrue])
|
||||
).then(() => {
|
||||
// starts all the mutations for all the selected items
|
||||
return Promise.all(
|
||||
selected.map(({ name }) =>
|
||||
action({ variables: { id: instance.id, snapshot: name } })
|
||||
)
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
// reverts submitting flag to false and propagates the error if it exists
|
||||
const flipSubmitFalse = stopSubmit(TABLE_FORM_NAME, {
|
||||
_error: err && parseError(err)
|
||||
});
|
||||
|
||||
// if no error, clears selected
|
||||
const clearSelected = !err && reset(TABLE_FORM_NAME);
|
||||
|
||||
// reverts (starting/rebooting/etc) to false
|
||||
const setIngFalse = set({
|
||||
name: `snapshot-list-${gerund}`,
|
||||
value: false
|
||||
});
|
||||
|
||||
// reverts the individual item mutation flags
|
||||
// when action === remove, let it stay spinning
|
||||
const setMutatingFalse =
|
||||
name !== 'remove' &&
|
||||
selected.map(({ id }) =>
|
||||
set({ name: `${id}-mutating`, value: false })
|
||||
);
|
||||
|
||||
const actions = [
|
||||
flipSubmitFalse,
|
||||
clearSelected,
|
||||
setIngFalse,
|
||||
...setMutatingFalse
|
||||
].filter(Boolean);
|
||||
|
||||
// refetch list - even though we poll anyway - after clearing everything
|
||||
return Promise.resolve(dispatch(actions)).then(() => refetch());
|
||||
}
|
||||
};
|
||||
},
|
||||
(stateProps, dispatchProps, ownProps) => {
|
||||
const { selected, snapshots, sortBy, sortOrder } = stateProps;
|
||||
const { toggleSelectAll } = dispatchProps;
|
||||
|
||||
return {
|
||||
...ownProps,
|
||||
...stateProps,
|
||||
selected,
|
||||
snapshots,
|
||||
...dispatchProps,
|
||||
toggleSelectAll: toggleSelectAll({ selected, snapshots })
|
||||
};
|
||||
}
|
||||
)
|
||||
)(Snapshots);
|
||||
|
@ -1,5 +1,4 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { compose, graphql } from 'react-apollo';
|
||||
import find from 'lodash.find';
|
||||
import get from 'lodash.get';
|
||||
|
@ -3,6 +3,7 @@ query instance($name: String!) {
|
||||
id
|
||||
name
|
||||
snapshots {
|
||||
id
|
||||
name
|
||||
state
|
||||
created
|
||||
|
@ -16,8 +16,7 @@ import {
|
||||
Firewall as InstanceFirewall,
|
||||
Dns as InstanceDns,
|
||||
Snapshots as InstanceSnapshots,
|
||||
Resize as InstanceResize,
|
||||
CreateSnapshot as InstanceCreateSnapshot
|
||||
Resize as InstanceResize
|
||||
} from '@containers/instances';
|
||||
|
||||
export default () => (
|
||||
@ -50,10 +49,6 @@ export default () => (
|
||||
{/* Instance Sections */}
|
||||
<Switch>
|
||||
<Route path="/instances/~:action" component={() => null} />
|
||||
<Route
|
||||
path="/instances/:instance/:section?/~create-snapshot"
|
||||
component={() => null}
|
||||
/>
|
||||
<Route
|
||||
path="/instances/:instance/summary"
|
||||
exact
|
||||
@ -108,16 +103,6 @@ export default () => (
|
||||
exact
|
||||
component={InstanceResize}
|
||||
/>
|
||||
<Route
|
||||
path="/instances/~create-snapshot/:instance"
|
||||
exact
|
||||
component={InstanceCreateSnapshot}
|
||||
/>
|
||||
<Route
|
||||
path="/instances/:instance/snapshots/~create"
|
||||
exact
|
||||
component={InstanceCreateSnapshot}
|
||||
/>
|
||||
</Switch>
|
||||
|
||||
<Route path="/" exact component={() => <Redirect to="/instances" />} />
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "joyent-ui-toolkit",
|
||||
"version": "4.0.0",
|
||||
"version": "4.0.1",
|
||||
"license": "MPL-2.0",
|
||||
"repository": "github:yldio/joyent-portal",
|
||||
"main": "dist/umd/index.js",
|
||||
@ -10,7 +10,6 @@
|
||||
"lint-ci": "eslint . --ext .js --ext .md",
|
||||
"lint": "eslint . --fix --ext .js --ext .md",
|
||||
"test-ci": "redrun test",
|
||||
"test": "redrun -p lint jest",
|
||||
"test": "NODE_ENV=test joyent-react-scripts test --env=jsdom",
|
||||
"compile:es": "babel src --out-dir dist/es --ignore spec.js",
|
||||
"compile:umd": "UMD=1 babel src --out-dir dist/umd --ignore spec.js",
|
||||
|
Loading…
Reference in New Issue
Block a user