feat(my-joy-beta): revise snapshots implementation

according to new designs

fixes #872
This commit is contained in:
Sara Vieira 2018-01-05 15:42:09 +00:00 committed by Sérgio Ramos
parent 8eecebfe47
commit 3caaebb0e9
29 changed files with 4903 additions and 6209 deletions

View File

@ -4826,6 +4826,7 @@ exports[`renders <KeyValue method="edit" /> without throwing 1`] = `
<b>
undefined:
</b>
,
<span />
</span>
</div>

View File

@ -1871,6 +1871,7 @@ exports[`renders <EditForm /> without throwing 1`] = `
<b>
undefined:
</b>
,
<span />
</span>
</div>

View File

@ -1809,6 +1809,7 @@ exports[`renders <EditForm /> without throwing 1`] = `
<b>
undefined:
</b>
,
<span />
</span>
</div>

View File

@ -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

View File

@ -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();
});

View File

@ -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: {

View File

@ -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>
);
};

View 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>
)
);

View File

@ -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';

View File

@ -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' ? (
<InputKeyValue type={type} submitting={submitting} />
onlyName ? (
<InputName type={type} submitting={submitting} />
) : (
<InputKeyValue type={type} submitting={submitting} />
)
) : (
<TextareaKeyValue type={type} submitting={submitting} />
)}
@ -181,25 +200,27 @@ export const KeyValue = ({
<span>{method === 'add' ? 'Create' : 'Save'}</span>
</Button>
</Col>
<Col xs={method === 'add' ? false : 5}>
<Button
type="button"
onClick={onRemove}
disabled={submitting}
loading={removing}
secondary
right
icon
error
marginless
>
<DeleteIcon
{!noRemove && (
<Col xs={method === 'add' ? false : 5}>
<Button
type="button"
onClick={onRemove}
disabled={submitting}
fill={submitting ? undefined : theme.red}
/>
<span>Delete</span>
</Button>
</Col>
loading={removing}
secondary
right
icon
error
marginless
>
<DeleteIcon
disabled={submitting}
fill={submitting ? undefined : theme.red}
/>
<span>Delete</span>
</Button>
</Col>
)}
</Row>
</Padding>
</CardOutlet>

View File

@ -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,

View File

@ -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 />
</FormGroup>
</TableTd>
<TableTd>{name}</TableTd>
<TableTd>{moment.unix(created).fromNow()}</TableTd>
{!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>
</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>&#8291;</FormLabel>
<Select
value="actions"
disabled={!items.length || !selected.length}
onChange={handleActions}
fluid
>
<option value="actions" selected disabled>
&#8801;
</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>&#8291;</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>
);
};

View File

@ -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>

View File

@ -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>

View File

@ -54,22 +54,26 @@ it('renders <Metadata addOpen /> without throwing', () => {
});
it('renders <Metadata metadata /> without throwing', () => {
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
}];
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

View File

@ -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();
});

View File

@ -93,19 +93,23 @@ it('renders <Tags editing.removing /> without throwing', () => {
});
it('renders <Tags tags /> without throwing', () => {
const tags = [{
name: 'name1',
value: 'value1',
id: 'name1-value1'
}, {
name: 'name2',
value: 'value2',
id: 'name2-value2'
}, {
name: 'name3',
value: 'value3',
id: 'name3-value3'
}];
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

View File

@ -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);

View File

@ -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>;

View File

@ -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';

View File

@ -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';

View File

@ -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>
<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}
<SnapshotsListForm
snapshots={_values}
loading={_loading}
onAction={handleAction}
selected={selected}
/>
{_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 }) => ({
...rest,
created: moment.utc(created).unix(),
updated: moment.utc(updated).unix()
}));
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 => {
return {
handleSortBy: (newSortBy, sortOrder) => {
dispatch([
set({
name: `snapshots-list-sort-order`,
value: sortOrder === 'desc' ? 'asc' : 'desc'
}),
set({
name: `snapshots-list-sort-by`,
value: newSortBy
})
]);
},
toggleCreateSnapshotOpen: value =>
dispatch(
stopSubmit(form, {
_error: error.graphQLErrors
.map(({ message }) => message)
.join('\n')
set({
name: `snapshots-create-open`,
value
})
),
toggleSelectAll: ({ selected = [], snapshots = [] }) => () => {
const same = selected.length === snapshots.length;
const hasSelected = selected.length > 0;
// 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 }
})
);
};
const handleSuccess = () => {
if (err) {
return dispatch(
stopSubmit(form, {
_error: parseError(error)
})
);
}
dispatch(
items
.map(({ name: field }) => change(form, field, false))
.concat([stopSubmit(form)])
set({
name: `snapshots-create-open`,
value: false
})
);
};
},
return (
types[name] &&
types[name]()
.then(handleSuccess)
.catch(handleError)
);
}
})
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);

View File

@ -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';

View File

@ -3,6 +3,7 @@ query instance($name: String!) {
id
name
snapshots {
id
name
state
created

View File

@ -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" />} />

View File

@ -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",