parent
bba8c99ee6
commit
8c604df1d2
|
@ -118,9 +118,6 @@ Session.vim
|
||||||
# temporary
|
# temporary
|
||||||
.netrwhist
|
.netrwhist
|
||||||
*~
|
*~
|
||||||
# auto-generated tag files
|
|
||||||
tags
|
|
||||||
|
|
||||||
|
|
||||||
### Windows ###
|
### Windows ###
|
||||||
# Windows image file caches
|
# Windows image file caches
|
||||||
|
|
|
@ -8,6 +8,9 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "packages/my-joy-beta"
|
"path": "packages/my-joy-beta"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "."
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"settings": {}
|
"settings": {}
|
||||||
|
|
|
@ -6,11 +6,14 @@
|
||||||
"repository": "github:yldio/joyent-portal",
|
"repository": "github:yldio/joyent-portal",
|
||||||
"main": "build/",
|
"main": "build/",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "REACT_APP_GQL_PORT=4000 PORT=3069 REACT_APP_GQL_PROTOCOL=http joyent-react-scripts start",
|
"dev":
|
||||||
|
"REACT_APP_GQL_PORT=4000 PORT=3069 REACT_APP_GQL_PROTOCOL=http joyent-react-scripts start",
|
||||||
"start": "PORT=3069 joyent-react-scripts start",
|
"start": "PORT=3069 joyent-react-scripts start",
|
||||||
"build": "NODE_ENV=production joyent-react-scripts build",
|
"build": "NODE_ENV=production joyent-react-scripts build",
|
||||||
"lint-ci": "eslint . --ext .js --ext .md && echo 0 `# stylelint './src/**/*.js'`",
|
"lint-ci":
|
||||||
"lint": "eslint . --fix --ext .js --ext .md && echo 0 `# stylelint './src/**/*.js'`",
|
"eslint . --ext .js --ext .md && echo 0 `# stylelint './src/**/*.js'`",
|
||||||
|
"lint":
|
||||||
|
"eslint . --fix --ext .js --ext .md && echo 0 `# stylelint './src/**/*.js'`",
|
||||||
"test": "NODE_ENV=test joyent-react-scripts test --env=jsdom",
|
"test": "NODE_ENV=test joyent-react-scripts test --env=jsdom",
|
||||||
"test-ci": "redrun test",
|
"test-ci": "redrun test",
|
||||||
"prepublish": "echo 0"
|
"prepublish": "echo 0"
|
||||||
|
@ -43,6 +46,7 @@
|
||||||
"redux-form": "^7.1.2",
|
"redux-form": "^7.1.2",
|
||||||
"remcalc": "^1.0.9",
|
"remcalc": "^1.0.9",
|
||||||
"styled-components": "^2.2.3",
|
"styled-components": "^2.2.3",
|
||||||
|
"styled-flex-component": "^1.1.0",
|
||||||
"title-case": "^2.1.1"
|
"title-case": "^2.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
import React from 'react';
|
||||||
|
import renderer from 'react-test-renderer';
|
||||||
|
import Store from '@mocks/store';
|
||||||
|
import 'jest-styled-components';
|
||||||
|
|
||||||
|
import Tags from '../tags';
|
||||||
|
|
||||||
|
|
||||||
|
it('renders <Tags /> without throwing', () => {
|
||||||
|
const tree = renderer
|
||||||
|
.create(
|
||||||
|
<Store>
|
||||||
|
<Tags />
|
||||||
|
</Store>
|
||||||
|
)
|
||||||
|
.toJSON();
|
||||||
|
|
||||||
|
expect(tree).toMatchSnapshot();
|
||||||
|
});
|
|
@ -206,75 +206,65 @@ export default withTheme(
|
||||||
<CardOutlet big>
|
<CardOutlet big>
|
||||||
<Meta {...instance} />
|
<Meta {...instance} />
|
||||||
<Flex>
|
<Flex>
|
||||||
<Flex>
|
<Button
|
||||||
<Button
|
secondary
|
||||||
secondary
|
bold
|
||||||
bold
|
icon
|
||||||
icon
|
loading={starting}
|
||||||
loading={starting}
|
disabled={instance.state === 'RUNNING'}
|
||||||
disabled={instance.state === 'RUNNING'}
|
onClick={() => onAction('start')}
|
||||||
onClick={() => onAction('start')}
|
>
|
||||||
>
|
<StartIcon disabled={instance.state === 'RUNNING'} />
|
||||||
<Padding right={3} style={{ height: 18 }}>
|
<Padding left={1}>Start</Padding>
|
||||||
<StartIcon disabled={instance.state === 'RUNNING'} />
|
</Button>
|
||||||
</Padding>
|
<Button
|
||||||
<span>Start</span>
|
secondary
|
||||||
</Button>
|
bold
|
||||||
<Button
|
icon
|
||||||
secondary
|
loading={stopping}
|
||||||
bold
|
disabled={instance.state === 'STOPPED'}
|
||||||
icon
|
onClick={() => onAction('stop')}
|
||||||
loading={stopping}
|
>
|
||||||
disabled={instance.state === 'STOPPED'}
|
<StopIcon disabled={instance.state === 'STOPPED'} />
|
||||||
onClick={() => onAction('stop')}
|
<Padding left={1}>Stop</Padding>
|
||||||
>
|
</Button>
|
||||||
<Padding right={3} style={{ height: 18 }}>
|
<Button
|
||||||
<StopIcon disabled={instance.state === 'STOPPED'} />
|
secondary
|
||||||
</Padding>
|
bold
|
||||||
<span>Stop</span>
|
icon
|
||||||
</Button>
|
loading={rebooting}
|
||||||
<Button
|
disabled={instance.state === 'PROVISIONING'}
|
||||||
secondary
|
onClick={() => onAction('reboot')}
|
||||||
bold
|
>
|
||||||
icon
|
<ResetIcon disabled={instance.state === 'PROVISIONING'} />
|
||||||
loading={rebooting}
|
<Padding left={1}>Restart</Padding>
|
||||||
disabled={instance.state === 'PROVISIONING'}
|
</Button>
|
||||||
onClick={() => onAction('reboot')}
|
|
||||||
>
|
|
||||||
<Padding right={3} style={{ height: 18 }}>
|
|
||||||
<ResetIcon disabled={instance.state === 'PROVISIONING'} />
|
|
||||||
</Padding>
|
|
||||||
<span>Restart</span>
|
|
||||||
</Button>
|
|
||||||
</Flex>
|
|
||||||
<FlexEnd>
|
|
||||||
<Button
|
|
||||||
error
|
|
||||||
bold
|
|
||||||
icon
|
|
||||||
loading={deleteing}
|
|
||||||
disabled={instance.state === 'PROVISIONING'}
|
|
||||||
onClick={() => onAction('delete')}
|
|
||||||
>
|
|
||||||
<Padding right={3} style={{ height: 18 }}>
|
|
||||||
<DeleteIcon
|
|
||||||
fill={theme.red}
|
|
||||||
disabled={instance.state === 'PROVISIONING'}
|
|
||||||
/>
|
|
||||||
</Padding>
|
|
||||||
<span>Delete</span>
|
|
||||||
</Button>
|
|
||||||
</FlexEnd>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
<Margin bottom={5} top={4}>
|
<FlexEnd>
|
||||||
<Divider height={remcalc(1)} />
|
<Button
|
||||||
</Margin>
|
error
|
||||||
<CopiableField text={instance.id.split('-')[0]} label="Short ID" />
|
bold
|
||||||
<CopiableField text={instance.id} label="ID" />
|
icon
|
||||||
<CopiableField text={instance.compute_node} label="CN UUID" />
|
loading={deleteing}
|
||||||
{instance.image && (
|
disabled={instance.state === 'PROVISIONING'}
|
||||||
<CopiableField text={instance.image.id} label="Image UUID" />
|
onClick={() => onAction('delete')}
|
||||||
)}
|
>
|
||||||
|
<DeleteIcon fill={theme.red} disabled={instance.state === 'PROVISIONING'} />
|
||||||
|
<Padding left={1}>Delete</Padding>
|
||||||
|
</Button>
|
||||||
|
</FlexEnd>
|
||||||
|
</Flex>
|
||||||
|
<Margin bottom={5} top={4}>
|
||||||
|
<Divider height={remcalc(1)} />
|
||||||
|
</Margin>
|
||||||
|
<CopiableField text={instance.id.split('-')[0]} label="Short ID" />
|
||||||
|
<CopiableField text={instance.id} label="ID" />
|
||||||
|
<CopiableField text={instance.compute_node} label="CN UUID" />
|
||||||
|
{instance.image && (
|
||||||
|
<CopiableField text={instance.image.id} label="Image UUID" />
|
||||||
|
)}
|
||||||
|
<CopiableField text={`$ ssh root@${instance.primary_ip}`} label="Login" />
|
||||||
|
{instance.ips.map((ip, i) => (
|
||||||
<CopiableField
|
<CopiableField
|
||||||
text={`$ ssh root@${instance.primary_ip}`}
|
text={`$ ssh root@${instance.primary_ip}`}
|
||||||
label="Login"
|
label="Login"
|
||||||
|
|
|
@ -5,10 +5,12 @@ import { Field } from 'redux-form';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import remcalc from 'remcalc';
|
import remcalc from 'remcalc';
|
||||||
import titleCase from 'title-case';
|
import titleCase from 'title-case';
|
||||||
|
import { Margin, Padding } from 'styled-components-spacing';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Message,
|
Message,
|
||||||
MessageDescription,
|
MessageDescription,
|
||||||
|
H4,
|
||||||
MessageTitle,
|
MessageTitle,
|
||||||
Card,
|
Card,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
|
@ -17,14 +19,13 @@ import {
|
||||||
CardOutlet,
|
CardOutlet,
|
||||||
ChevronIcon,
|
ChevronIcon,
|
||||||
FormGroup,
|
FormGroup,
|
||||||
Label,
|
FormLabel,
|
||||||
Input,
|
Input,
|
||||||
FormMeta,
|
FormMeta,
|
||||||
Button,
|
Button,
|
||||||
Textarea,
|
Textarea,
|
||||||
Editor,
|
Editor,
|
||||||
Divider,
|
Divider
|
||||||
P
|
|
||||||
} from 'joyent-ui-toolkit';
|
} from 'joyent-ui-toolkit';
|
||||||
|
|
||||||
const CollapsedKeyValue = styled.span`
|
const CollapsedKeyValue = styled.span`
|
||||||
|
@ -74,7 +75,7 @@ const KeyValue = ({
|
||||||
);
|
);
|
||||||
|
|
||||||
const _meta = expanded ? (
|
const _meta = expanded ? (
|
||||||
<P>{create ? `Create ${label}` : `Edit ${label}`}</P>
|
<H4>{create ? `Create ${label}` : `Edit ${label}`}</H4>
|
||||||
) : (
|
) : (
|
||||||
<CollapsedKeyValue>
|
<CollapsedKeyValue>
|
||||||
<Field
|
<Field
|
||||||
|
@ -93,7 +94,12 @@ const KeyValue = ({
|
||||||
);
|
);
|
||||||
|
|
||||||
const _valueField = textarea ? (
|
const _valueField = textarea ? (
|
||||||
<Field name="name" component={ValueTextareaField} props={{ submitting }} />
|
<Field
|
||||||
|
name="name"
|
||||||
|
fluid
|
||||||
|
component={ValueTextareaField}
|
||||||
|
props={{ submitting }}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Input disabled={submitting} />
|
<Input disabled={submitting} />
|
||||||
);
|
);
|
||||||
|
@ -102,6 +108,7 @@ const KeyValue = ({
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
key="cancel"
|
key="cancel"
|
||||||
|
bold
|
||||||
onClick={
|
onClick={
|
||||||
create
|
create
|
||||||
? pristine ? onToggleExpanded : onClear
|
? pristine ? onToggleExpanded : onClear
|
||||||
|
@ -120,9 +127,9 @@ const KeyValue = ({
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
key="submit"
|
key="submit"
|
||||||
|
bold
|
||||||
disabled={pristine || submitting}
|
disabled={pristine || submitting}
|
||||||
loading={submitting && !removing}
|
loading={submitting && !removing}
|
||||||
secondary
|
|
||||||
marginless
|
marginless
|
||||||
>
|
>
|
||||||
{create ? 'Create' : 'Update'}
|
{create ? 'Create' : 'Update'}
|
||||||
|
@ -146,36 +153,40 @@ const KeyValue = ({
|
||||||
onClick={onToggleExpanded}
|
onClick={onToggleExpanded}
|
||||||
actionable
|
actionable
|
||||||
>
|
>
|
||||||
<CardHeaderMeta>{_meta}</CardHeaderMeta>
|
<CardHeaderMeta>
|
||||||
|
<Padding left={1}>{_meta}</Padding>
|
||||||
|
</CardHeaderMeta>
|
||||||
{chevronToggle}
|
{chevronToggle}
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardOutlet>
|
<CardOutlet>
|
||||||
<Row>
|
<Padding all={1}>
|
||||||
<Col xs={12}>{_error}</Col>
|
<Row>
|
||||||
</Row>
|
<Col xs={12}>{_error}</Col>
|
||||||
<Row>
|
</Row>
|
||||||
<Col xs={12}>
|
<Row>
|
||||||
<FormGroup name="name" reduxForm>
|
<Col xs={6}>
|
||||||
<Label>{titleCase(label)} key</Label>
|
<FormGroup name="name" reduxForm fluid>
|
||||||
<Input type="text" disabled={submitting} />
|
<FormLabel>Enter {titleCase(label)} key</FormLabel>
|
||||||
<FormMeta />
|
<Input type="text" disabled={submitting} />
|
||||||
</FormGroup>
|
<FormMeta />
|
||||||
</Col>
|
</FormGroup>
|
||||||
</Row>
|
</Col>
|
||||||
<Row>
|
<Col xs={6}>
|
||||||
<Col xs={12}>
|
<FormGroup name="value" reduxForm fluid>
|
||||||
<FormGroup name="value" reduxForm>
|
<FormLabel>Enter {titleCase(label)} value</FormLabel>
|
||||||
<Label>{titleCase(label)} value</Label>
|
{_valueField}
|
||||||
{_valueField}
|
</FormGroup>
|
||||||
</FormGroup>
|
</Col>
|
||||||
</Col>
|
</Row>
|
||||||
</Row>
|
<Row>
|
||||||
<Row>
|
<Col xs={12}>
|
||||||
<Col xs={12}>
|
<Margin top={2}>
|
||||||
{_cancel}
|
{_cancel}
|
||||||
{_submit}
|
{_submit}
|
||||||
</Col>
|
</Margin>
|
||||||
</Row>
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Padding>
|
||||||
</CardOutlet>
|
</CardOutlet>
|
||||||
</Card>
|
</Card>
|
||||||
<Divider transparent marginBottom={last || expanded ? remcalc(13) : 0} />
|
<Divider transparent marginBottom={last || expanded ? remcalc(13) : 0} />
|
||||||
|
|
|
@ -0,0 +1,178 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { Row, Col } from 'react-styled-flexboxgrid';
|
||||||
|
import { Margin } from 'styled-components-spacing';
|
||||||
|
import Value from 'react-redux-values';
|
||||||
|
import ReduxForm from 'declarative-redux-form';
|
||||||
|
import { KeyValue } from '@components/instances';
|
||||||
|
import { withTheme } from 'styled-components';
|
||||||
|
import remcalc from 'remcalc';
|
||||||
|
|
||||||
|
import {
|
||||||
|
H3,
|
||||||
|
H4,
|
||||||
|
Divider,
|
||||||
|
FormGroup,
|
||||||
|
FormLabel,
|
||||||
|
Input,
|
||||||
|
Button,
|
||||||
|
StatusLoader,
|
||||||
|
CloseIcon,
|
||||||
|
TagItem,
|
||||||
|
TagList,
|
||||||
|
TagItemContainer
|
||||||
|
} from 'joyent-ui-toolkit';
|
||||||
|
|
||||||
|
const FlexEnd = styled(Col)`
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const CloseIconActionable = styled(CloseIcon)`
|
||||||
|
cursor: pointer;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Tags = ({
|
||||||
|
handleRemove,
|
||||||
|
addKey,
|
||||||
|
handleCreate,
|
||||||
|
handleClear,
|
||||||
|
theme,
|
||||||
|
toggleEdit,
|
||||||
|
removeTag,
|
||||||
|
filterTags,
|
||||||
|
state,
|
||||||
|
edit,
|
||||||
|
tags
|
||||||
|
}) => {
|
||||||
|
const _filterTags = (
|
||||||
|
<Col md={4} xs={12}>
|
||||||
|
<FormGroup fluid>
|
||||||
|
<FormLabel>Filter</FormLabel>
|
||||||
|
<Input fluid type="text" onChange={filterTags} />
|
||||||
|
</FormGroup>
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
|
|
||||||
|
const _addTag = (
|
||||||
|
<Value name={`${addKey}-expanded`}>
|
||||||
|
{({ value: expanded, onValueChange }) =>
|
||||||
|
!expanded ? (
|
||||||
|
<FlexEnd mdOffset={4} md={4} xs={12}>
|
||||||
|
<Button secondary onClick={toggleEdit}>
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onValueChange(!expanded)}
|
||||||
|
disabled={edit}
|
||||||
|
>
|
||||||
|
Add tag
|
||||||
|
</Button>
|
||||||
|
</FlexEnd>
|
||||||
|
) : (
|
||||||
|
[
|
||||||
|
<FlexEnd mdOffset={4} md={4} xs={12}>
|
||||||
|
<Button disabled secondary onClick={toggleEdit}>
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onValueChange(!expanded)}
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
Add tag
|
||||||
|
</Button>
|
||||||
|
</FlexEnd>,
|
||||||
|
<Col xs={12}>
|
||||||
|
<ReduxForm
|
||||||
|
form={addKey}
|
||||||
|
onSubmit={handleCreate}
|
||||||
|
id={addKey}
|
||||||
|
onClear={() => handleClear(addKey)}
|
||||||
|
onToggleExpanded={() => onValueChange(!expanded)}
|
||||||
|
onRemove={() => handleRemove(addKey)}
|
||||||
|
expanded={expanded}
|
||||||
|
label="tag"
|
||||||
|
create
|
||||||
|
>
|
||||||
|
{KeyValue}
|
||||||
|
</ReduxForm>
|
||||||
|
</Col>
|
||||||
|
]
|
||||||
|
)}
|
||||||
|
</Value>
|
||||||
|
);
|
||||||
|
|
||||||
|
const _title = (
|
||||||
|
<Margin bottom={3} key="tag-title">
|
||||||
|
<H3>
|
||||||
|
{tags.length} {tags.length === 1 ? 'Tag' : 'Tags'}
|
||||||
|
</H3>
|
||||||
|
</Margin>
|
||||||
|
);
|
||||||
|
|
||||||
|
const _noTags =
|
||||||
|
!tags ||
|
||||||
|
(tags.length === 0 && <H4 key="no-tags">No tags have been added yet</H4>);
|
||||||
|
|
||||||
|
const _list = tags.length > 0 && (
|
||||||
|
<TagList key="tag-list">
|
||||||
|
{tags
|
||||||
|
.sort(
|
||||||
|
(a, b) =>
|
||||||
|
a.initialValues.name.toLowerCase() <
|
||||||
|
b.initialValues.name.toLowerCase()
|
||||||
|
? -1
|
||||||
|
: 1
|
||||||
|
)
|
||||||
|
.map(tag => (
|
||||||
|
<Margin
|
||||||
|
right={1}
|
||||||
|
bottom={1}
|
||||||
|
key={`${tag.initialValues.name}-${tag.initialValues.value}`}
|
||||||
|
>
|
||||||
|
<TagItem>
|
||||||
|
{state[
|
||||||
|
`${tag.initialValues.name}-${tag.initialValues.value}-deleting`
|
||||||
|
] ? (
|
||||||
|
<StatusLoader small />
|
||||||
|
) : (
|
||||||
|
<TagItemContainer>
|
||||||
|
{tag.initialValues.name}: {tag.initialValues.value}
|
||||||
|
{edit && (
|
||||||
|
<Margin left={2}>
|
||||||
|
<CloseIconActionable
|
||||||
|
onClick={() =>
|
||||||
|
removeTag(
|
||||||
|
tag.initialValues.name,
|
||||||
|
tag.initialValues.value
|
||||||
|
)}
|
||||||
|
fill={theme.grey}
|
||||||
|
height={remcalc(9)}
|
||||||
|
/>
|
||||||
|
</Margin>
|
||||||
|
)}
|
||||||
|
</TagItemContainer>
|
||||||
|
)}
|
||||||
|
</TagItem>
|
||||||
|
</Margin>
|
||||||
|
))}
|
||||||
|
</TagList>
|
||||||
|
);
|
||||||
|
|
||||||
|
return [
|
||||||
|
<Row bottom="md" key="tag-row">
|
||||||
|
{_filterTags}
|
||||||
|
{_addTag}
|
||||||
|
</Row>,
|
||||||
|
<Margin key="tag-divider" bottom={4} top={2}>
|
||||||
|
<Divider height="1px" />
|
||||||
|
</Margin>,
|
||||||
|
_title,
|
||||||
|
_list,
|
||||||
|
_noTags
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withTheme(Tags);
|
|
@ -1,124 +1,126 @@
|
||||||
import React from 'react';
|
import React, { Component } from 'react';
|
||||||
import paramCase from 'param-case';
|
import paramCase from 'param-case';
|
||||||
import { compose, graphql } from 'react-apollo';
|
import { compose, graphql } from 'react-apollo';
|
||||||
import Value, { set } from 'react-redux-values';
|
import { set } from 'react-redux-values';
|
||||||
import { SubmissionError, reset, startSubmit, stopSubmit } from 'redux-form';
|
import { SubmissionError, reset, stopSubmit } from 'redux-form';
|
||||||
import ReduxForm from 'declarative-redux-form';
|
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import find from 'lodash.find';
|
import find from 'lodash.find';
|
||||||
import get from 'lodash.get';
|
import get from 'lodash.get';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ViewContainer,
|
ViewContainer,
|
||||||
Title,
|
|
||||||
StatusLoader,
|
StatusLoader,
|
||||||
Message,
|
Message,
|
||||||
MessageDescription,
|
MessageDescription,
|
||||||
MessageTitle,
|
MessageTitle
|
||||||
Button
|
|
||||||
} from 'joyent-ui-toolkit';
|
} from 'joyent-ui-toolkit';
|
||||||
|
|
||||||
|
import TagsComponent from '@components/instances/tags';
|
||||||
|
|
||||||
import GetTags from '@graphql/list-tags.gql';
|
import GetTags from '@graphql/list-tags.gql';
|
||||||
import UpdateTags from '@graphql/update-tags.gql';
|
import UpdateTags from '@graphql/update-tags.gql';
|
||||||
import DeleteTag from '@graphql/delete-tag.gql';
|
import DeleteTag from '@graphql/delete-tag.gql';
|
||||||
import { KeyValue } from '@components/instances';
|
|
||||||
|
|
||||||
const TAG_FORM_KEY = (name, field) => `instance-tag-${name}-${field}`;
|
const TAG_FORM_KEY = (name, field) => `instance-tag-${name}-${field}`;
|
||||||
const CREATE_TAG_FORM_KEY = name => `instance-create-tag-${name}`;
|
const CREATE_TAG_FORM_KEY = name => `instance-create-tag-${name}`;
|
||||||
|
|
||||||
const Tags = ({
|
class Tags extends Component {
|
||||||
instance,
|
constructor(props) {
|
||||||
values = [],
|
super(props);
|
||||||
loading,
|
const { values: tags } = props;
|
||||||
error,
|
this.state = {
|
||||||
handleRemove,
|
tags,
|
||||||
handleClear,
|
edit: false
|
||||||
handleUpdate,
|
};
|
||||||
handleCreate
|
}
|
||||||
}) => {
|
|
||||||
const _title = <Title>Tags</Title>;
|
|
||||||
const _loading = !(loading && !values.length) ? null : <StatusLoader />;
|
|
||||||
|
|
||||||
// tags items forms
|
componentWillReceiveProps = ({ values: tags }) => {
|
||||||
const _tags =
|
this.setState({
|
||||||
!_loading &&
|
tags
|
||||||
values.map(({ form, initialValues }, i) => (
|
});
|
||||||
<Value name={`${form}-expanded`} key={form}>
|
};
|
||||||
{({ value: expanded, onValueChange }) => (
|
|
||||||
<ReduxForm
|
|
||||||
form={form}
|
|
||||||
initialValues={initialValues}
|
|
||||||
onSubmit={newValues => handleUpdate(newValues, form)}
|
|
||||||
destroyOnUnmount
|
|
||||||
id={form}
|
|
||||||
onClear={() => handleClear(form)}
|
|
||||||
onToggleExpanded={() => onValueChange(!expanded)}
|
|
||||||
onRemove={() => handleRemove(form)}
|
|
||||||
label="tag"
|
|
||||||
last={values.length - 1 === i}
|
|
||||||
first={i === 0}
|
|
||||||
expanded={expanded}
|
|
||||||
>
|
|
||||||
{KeyValue}
|
|
||||||
</ReduxForm>
|
|
||||||
)}
|
|
||||||
</Value>
|
|
||||||
));
|
|
||||||
|
|
||||||
// create tags form
|
filterTags = e => {
|
||||||
const _addKey = instance && CREATE_TAG_FORM_KEY(instance.name);
|
const value = e.target.value;
|
||||||
const _add = _tags &&
|
const { values: tags } = this.props;
|
||||||
_addKey && (
|
|
||||||
<Value name={`${_addKey}-expanded`}>
|
this.setState({
|
||||||
{({ value: expanded, onValueChange }) =>
|
tags: tags.filter(
|
||||||
!expanded ? (
|
tag =>
|
||||||
<Button
|
tag.initialValues.value.includes(value) ||
|
||||||
type="button"
|
tag.initialValues.name.includes(value)
|
||||||
onClick={() => onValueChange(!expanded)}
|
)
|
||||||
secondary
|
});
|
||||||
>
|
};
|
||||||
Add tag
|
|
||||||
</Button>
|
removeTag = (name, value) => {
|
||||||
) : (
|
const { handleRemove } = this.props;
|
||||||
<ReduxForm
|
|
||||||
form={_addKey}
|
handleRemove(name);
|
||||||
onSubmit={handleCreate}
|
|
||||||
id={_addKey}
|
this.setState({
|
||||||
onClear={() => handleClear(_addKey)}
|
[`${name}-${value}-deleting`]: true
|
||||||
onToggleExpanded={() => onValueChange(!expanded)}
|
});
|
||||||
onRemove={() => handleRemove(_addKey)}
|
};
|
||||||
expanded={expanded}
|
|
||||||
label="tag"
|
toggleEdit = () => {
|
||||||
create
|
const { edit } = this.state;
|
||||||
>
|
|
||||||
{KeyValue}
|
this.setState({
|
||||||
</ReduxForm>
|
edit: !edit
|
||||||
)
|
});
|
||||||
}
|
};
|
||||||
</Value>
|
|
||||||
|
render = () => {
|
||||||
|
const {
|
||||||
|
instance,
|
||||||
|
values = [],
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
handleRemove,
|
||||||
|
handleClear,
|
||||||
|
handleCreate
|
||||||
|
} = this.props;
|
||||||
|
const _loading = !(loading && !values.length) ? null : <StatusLoader />;
|
||||||
|
const _addKey = instance && CREATE_TAG_FORM_KEY(instance.name);
|
||||||
|
const { edit, tags } = this.state;
|
||||||
|
|
||||||
|
// tags items forms
|
||||||
|
const _tags = !_loading && (
|
||||||
|
<TagsComponent
|
||||||
|
toggleEdit={this.toggleEdit}
|
||||||
|
removeTag={this.removeTag}
|
||||||
|
filterTags={this.filterTags}
|
||||||
|
state={this.state}
|
||||||
|
edit={edit}
|
||||||
|
handleCreate={handleCreate}
|
||||||
|
handleClear={handleClear}
|
||||||
|
addKey={_addKey}
|
||||||
|
tags={tags}
|
||||||
|
handleRemove={handleRemove}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
// fetching error
|
// fetching error
|
||||||
const _error =
|
const _error =
|
||||||
error && !values.length && !_loading ? (
|
error && !values.length && !_loading ? (
|
||||||
<Message error>
|
<Message error>
|
||||||
<MessageTitle>Ooops!</MessageTitle>
|
<MessageTitle>Ooops!</MessageTitle>
|
||||||
<MessageDescription>
|
<MessageDescription>
|
||||||
An error occurred while loading your instance tags
|
An error occurred while loading your instance tags
|
||||||
</MessageDescription>
|
</MessageDescription>
|
||||||
</Message>
|
</Message>
|
||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ViewContainer center={Boolean(_loading)} main>
|
<ViewContainer center={Boolean(_loading)} main>
|
||||||
{_title}
|
{_loading}
|
||||||
{_loading}
|
{_error}
|
||||||
{_error}
|
{_tags}
|
||||||
{_tags}
|
</ViewContainer>
|
||||||
{_add}
|
);
|
||||||
</ViewContainer>
|
};
|
||||||
);
|
}
|
||||||
};
|
|
||||||
|
|
||||||
export default compose(
|
export default compose(
|
||||||
graphql(UpdateTags, { name: 'updateTags' }),
|
graphql(UpdateTags, { name: 'updateTags' }),
|
||||||
|
@ -159,26 +161,23 @@ export default compose(
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
connect(null, (dispatch, ownProps) => {
|
connect(null, (dispatch, ownProps) => {
|
||||||
const { instance, values, refetch, updateTags, deleteTag } = ownProps;
|
const { instance, refetch, updateTags, deleteTag } = ownProps;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// reset sets values to initialValues
|
// reset sets values to initialValues
|
||||||
handleClear: form => dispatch(reset(form)),
|
handleClear: form => dispatch(reset(form)),
|
||||||
handleRemove: form =>
|
handleRemove: name =>
|
||||||
Promise.resolve(
|
Promise.resolve(
|
||||||
// set removing=true (so that we can have a specific removing spinner)
|
// set removing=true (so that we can have a specific removing spinner)
|
||||||
// because remove button is not a submit button, we have to manually flip that flag
|
// because remove button is not a submit button, we have to manually flip that flag
|
||||||
dispatch([
|
dispatch([set({ name: `${name}-removing`, value: true })])
|
||||||
set({ name: `${form}-removing`, value: true }),
|
|
||||||
startSubmit(form)
|
|
||||||
])
|
|
||||||
)
|
)
|
||||||
.then(() =>
|
.then(() =>
|
||||||
// call mutation
|
// call mutation
|
||||||
deleteTag({
|
deleteTag({
|
||||||
variables: {
|
variables: {
|
||||||
id: instance.id,
|
id: instance.id,
|
||||||
name: get(find(values, ['form', form]), 'initialValues.name')
|
name
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
@ -189,40 +188,14 @@ export default compose(
|
||||||
// it takes longer to have an efect than the mutation
|
// it takes longer to have an efect than the mutation
|
||||||
.catch(error =>
|
.catch(error =>
|
||||||
dispatch([
|
dispatch([
|
||||||
set({ name: `${form}-removing`, value: false }),
|
set({ name: `${name}-removing`, value: false }),
|
||||||
stopSubmit(form, {
|
stopSubmit(name, {
|
||||||
_error: error.graphQLErrors
|
_error: error.graphQLErrors
|
||||||
.map(({ message }) => message)
|
.map(({ message }) => message)
|
||||||
.join('\n')
|
.join('\n')
|
||||||
})
|
})
|
||||||
])
|
])
|
||||||
),
|
),
|
||||||
handleUpdate: ({ name, value }, form) =>
|
|
||||||
// delete old tag and add a new one
|
|
||||||
Promise.all([
|
|
||||||
deleteTag({
|
|
||||||
variables: {
|
|
||||||
id: instance.id,
|
|
||||||
name: get(find(values, ['form', form]), 'initialValues.name')
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
updateTags({
|
|
||||||
variables: {
|
|
||||||
id: instance.id,
|
|
||||||
tags: [{ name, value }]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
])
|
|
||||||
// fetch tags again
|
|
||||||
.then(() => refetch())
|
|
||||||
// submit is flipped once the promise is resolved
|
|
||||||
.catch(error => {
|
|
||||||
throw new SubmissionError({
|
|
||||||
_error: error.graphQLErrors
|
|
||||||
.map(({ message }) => message)
|
|
||||||
.join('\n')
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
handleCreate: ({ name, value }) =>
|
handleCreate: ({ name, value }) =>
|
||||||
// call mutation
|
// call mutation
|
||||||
updateTags({
|
updateTags({
|
||||||
|
|
|
@ -88,6 +88,8 @@ export {
|
||||||
Item as SectionListItem
|
Item as SectionListItem
|
||||||
} from './section-list';
|
} from './section-list';
|
||||||
|
|
||||||
|
export { TagItem, TagList, TagItemContainer } from './tags';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Actions as ActionsIcon,
|
Actions as ActionsIcon,
|
||||||
Affinity as AffinityIcon,
|
Affinity as AffinityIcon,
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
export default styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-grow: 1;
|
||||||
|
`;
|
|
@ -0,0 +1,3 @@
|
||||||
|
export { default as TagItem } from './item';
|
||||||
|
export { default as TagList } from './list';
|
||||||
|
export { default as TagItemContainer } from './container';
|
|
@ -0,0 +1,13 @@
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import remcalc from 'remcalc';
|
||||||
|
|
||||||
|
export default styled.li`
|
||||||
|
border: ${remcalc(1)} solid ${props => props.theme.grey};
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-radius: ${remcalc(2)};
|
||||||
|
font-size: ${remcalc(13)};
|
||||||
|
padding: ${remcalc(6)} ${remcalc(12)};
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-grow: 1;
|
||||||
|
`;
|
|
@ -0,0 +1,9 @@
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
export default styled.ul`
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
list-style: none;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
`;
|
23
yarn.lock
23
yarn.lock
|
@ -10163,6 +10163,20 @@ styled-components-spacing@^2.1.3:
|
||||||
react-create-component-from-tag-prop "^1.2.1"
|
react-create-component-from-tag-prop "^1.2.1"
|
||||||
styled-components-breakpoint "^1.0.0-preview.3"
|
styled-components-breakpoint "^1.0.0-preview.3"
|
||||||
|
|
||||||
|
styled-components@2.2.4:
|
||||||
|
version "2.2.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-2.2.4.tgz#dd87fd3dafd359e7a0d570aec1bd07d691c0b5a2"
|
||||||
|
dependencies:
|
||||||
|
buffer "^5.0.3"
|
||||||
|
css-to-react-native "^2.0.3"
|
||||||
|
fbjs "^0.8.9"
|
||||||
|
hoist-non-react-statics "^1.2.0"
|
||||||
|
is-function "^1.0.1"
|
||||||
|
is-plain-object "^2.0.1"
|
||||||
|
prop-types "^15.5.4"
|
||||||
|
stylis "^3.4.0"
|
||||||
|
supports-color "^3.2.3"
|
||||||
|
|
||||||
styled-components@^2.2.3:
|
styled-components@^2.2.3:
|
||||||
version "2.2.4"
|
version "2.2.4"
|
||||||
resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-2.2.4.tgz#dd87fd3dafd359e7a0d570aec1bd07d691c0b5a2"
|
resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-2.2.4.tgz#dd87fd3dafd359e7a0d570aec1bd07d691c0b5a2"
|
||||||
|
@ -10177,7 +10191,14 @@ styled-components@^2.2.3:
|
||||||
stylis "^3.4.0"
|
stylis "^3.4.0"
|
||||||
supports-color "^3.2.3"
|
supports-color "^3.2.3"
|
||||||
|
|
||||||
styled-is@^1.1.0:
|
styled-flex-component@^1.1.0:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/styled-flex-component/-/styled-flex-component-1.1.0.tgz#f897bffecb7650045443481941bfb42ef16d2404"
|
||||||
|
dependencies:
|
||||||
|
styled-components "2.2.4"
|
||||||
|
styled-is "1.1.0"
|
||||||
|
|
||||||
|
styled-is@1.1.0, styled-is@^1.1.0:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/styled-is/-/styled-is-1.1.0.tgz#0cf8d32098fe6559eb0ec889790cc6c84f1f497f"
|
resolved "https://registry.yarnpkg.com/styled-is/-/styled-is-1.1.0.tgz#0cf8d32098fe6559eb0ec889790cc6c84f1f497f"
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue