feat(ui-toolkit, cp-frontend): Add clear status and health messaging and refactor tooltips use

This commit is contained in:
JUDIT GRESKOVITS 2017-08-14 11:21:45 +01:00 committed by Judit Greskovits
parent 24bee629e8
commit bc026b2341
36 changed files with 733 additions and 414 deletions

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import remcalc from 'remcalc';
import styled from 'styled-components';
@ -7,10 +7,16 @@ import titleCase from 'title-case';
import {
Card,
CardInfo,
CardView,
CardMeta,
CardTitle,
CardDescription,
HealthyIcon,
Tooltip,
TooltipLabel,
P,
Label,
typography
} from 'joyent-ui-toolkit';
@ -29,31 +35,18 @@ const STATUSES = [
'UNKNOWN'
];
const Span = styled.span`
${typography.fontFamily};
${typography.normal};
`;
const Dot = styled.div`
const Dot = styled.span`
margin-right: ${remcalc(6)};
width: ${remcalc(6)};
height: ${remcalc(6)};
border-radius: ${remcalc(3)};
display: inline-block;
${isOr('provisioning', 'ready', 'active')`
background-color: ${props => props.theme.primary};
`};
${is('running')`
${isOr('provisioning', 'ready', 'active', 'running')`
background-color: ${props => props.theme.green};
`};
${is('stopping')`
background-color: orange;
`};
${is('stopped')`
${isOr('stopping', 'stopped')`
background-color: ${props => props.theme.grey};
`};
@ -66,23 +59,6 @@ const Dot = styled.div`
`};
`;
const StatusBadge = ({ status }) => {
const props = STATUSES.reduce(
(acc, name) =>
Object.assign(acc, {
[name.toLowerCase()]: name === status
}),
{}
);
return (
<Span>
<Dot {...props} />
{titleCase(status)}
</Span>
);
};
const StyledCard = Card.extend`
flex-direction: row;
@ -92,37 +68,91 @@ const StyledCard = Card.extend`
border-bottom-width: 0;
}
&:nth-child(odd) {
background-color: ${props => props.theme.white};
${isOr('stopping', 'stopped', 'offline', 'destroyed', 'failed', 'deleted', 'incomplete', 'unknown')`
background-color: ${props => props.theme.background};
& [name="card-options"] > button {
background-color: ${props => props.theme.background};
}
}`
}
`;
const InstanceCard = ({
instance = {},
onOptionsClick = () => null,
toggleCollapsed = () => null
}) =>
<StyledCard collapsed={true} key={instance.uuid}>
<CardView>
<CardMeta onClick={toggleCollapsed}>
toggleCollapsed = () => null,
onHealthMouseOver,
onStatusMouseOver,
onMouseOut
}) => {
const statusProps = STATUSES.reduce(
(acc, name) =>
Object.assign(acc, {
[name.toLowerCase()]: name === instance.status
}),
{}
);
const label = instance.healthy.toLowerCase();
const icon = <HealthyIcon healthy={instance.healthy} />;
const handleHealthMouseOver = (evt) => {
onHealthMouseOver(evt, instance);
}
const handleStatusMouseOver = (evt) => {
onStatusMouseOver(evt, instance);
}
const handleMouseOut = (evt) => {
onMouseOut(evt);
}
return (
<StyledCard collapsed={true} key={instance.uuid} {...statusProps}>
<CardView>
<CardTitle>
{instance.name}
</CardTitle>
<CardDescription>
<StatusBadge status={instance.status} />
<div
onMouseOver={handleHealthMouseOver}
onMouseOut={handleMouseOut}
>
<CardInfo
icon={icon}
iconPosition='left'
label={label}
color='dark'
/>
</div>
</CardDescription>
</CardMeta>
</CardView>
</StyledCard>;
<CardDescription>
<div
onMouseOver={handleStatusMouseOver}
onMouseOut={handleMouseOut}
>
<Label>
<Dot {...statusProps} />
{titleCase(instance.status)}
</Label>
</div>
</CardDescription>
</CardView>
</StyledCard>
)
};
InstanceCard.propTypes = {
instance: PropTypes.object,
onOptionsClick: PropTypes.func,
toggleCollapsed: PropTypes.func
toggleCollapsed: PropTypes.func,
onHealthMouseOver: PropTypes.func,
onStatusMouseOver: PropTypes.func,
onMouseOut: PropTypes.func
};
export default InstanceCard;

View File

@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import styled from 'styled-components';
import forceArray from 'force-array';
import sortBy from 'lodash.sortby';
import { isNot } from 'styled-is';
import { InstancesIcon, HealthyIcon, UnhealthyIcon } from 'joyent-ui-toolkit';
import Status from './status';
@ -27,10 +28,16 @@ const StyledCardHeader = styled(CardHeader)`
const TitleInnerContainer = styled.div`
display: flex;
flex-direction: row;
justify-content: center;
justify-content: left;
align-items: center;
`;
const StyledAnchor = styled(Anchor)`
${isNot('active')`
color: ${props => props.theme.text}
`};
`;
const ServiceListItem = ({
onQuickActionsClick = () => {},
deploymentGroup = '',
@ -42,7 +49,7 @@ const ServiceListItem = ({
};
const children = sortBy(forceArray(service.children), ['slug']);
const isServiceInactive = service.status && service.status !== 'ACTIVE';
// const isServiceInactive = service.status && service.status !== 'ACTIVE';
const to = `/deployment-groups/${deploymentGroup}/services/${service.slug}`;
const instancesCount = children.length
@ -64,9 +71,9 @@ const ServiceListItem = ({
</CardTitle>
: <CardTitle>
<TitleInnerContainer>
<Anchor to={to} disabled={isServiceInactive} secondary>
<StyledAnchor to={to} secondary active={service.instancesActive}>
{service.name}
</Anchor>
</StyledAnchor>
</TitleInnerContainer>
</CardTitle>;
@ -87,28 +94,29 @@ const ServiceListItem = ({
label={`${instancesCount} ${instancesCount > 1
? 'instances'
: 'instance'}`}
color={isServiceInactive ? 'disabled' : 'light'}
color={!service.instancesActive ? 'disabled' : 'light'}
/>
</CardDescription>
<CardOptions onClick={handleCardOptionsClick} />
</StyledCardHeader>
: null;
const healthyInfo = isServiceInactive
? null
: service.instancesHealthy
? <CardInfo
icon={<HealthyIcon />}
iconPosition="left"
label="Healthy"
color="dark"
/>
: <CardInfo
icon={<UnhealthyIcon />}
iconPosition="left"
label="Unhealthy"
color="dark"
/>;
let healthyInfo = null;
if(service.instancesActive) {
const { total, healthy } = service.instancesHealthy;
const iconHealthy = total === healthy ? 'HEALTHY' : 'NOT HEALTHY';
const icon = <HealthyIcon healthy={iconHealthy} />;
const label = `${healthy} of ${total} healthy`;
healthyInfo = (
<CardInfo
icon={icon}
iconPosition='left'
label={label}
color='dark'
/>
)
}
const view = childrenItems.length
? <CardGroupView>
@ -126,7 +134,7 @@ const ServiceListItem = ({
return (
<Card
collapsed={service.collapsed}
disabled={isServiceInactive}
active={service.instancesActive}
flat={isChild}
headed={!isChild}
key={service.id}

View File

@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Tooltip, TooltipButton, TooltipDivider } from 'joyent-ui-toolkit';
import { Tooltip, TooltipButton, TooltipDivider, TooltipList } from 'joyent-ui-toolkit';
const ServicesQuickActions = ({
show,
@ -18,14 +18,14 @@ const ServicesQuickActions = ({
return null;
}
const p = Object.keys(position).reduce((p, key) => {
/* const p = Object.keys(position).reduce((p, key) => {
if (typeof position[key] === 'number') {
p[key] = `${position[key]}px`;
} else {
p[key] = position[key];
}
return p;
}, {});
}, {}); */
const handleRestartClick = evt => {
onRestartClick(evt, service);
@ -58,31 +58,43 @@ const ServicesQuickActions = ({
const startService =
status === 'RUNNING'
? null
: <TooltipButton onClick={handleStartClick} disabled={disabled}>
Start
</TooltipButton>;
: <li>
<TooltipButton onClick={handleStartClick} disabled={disabled}>
Start
</TooltipButton>
</li>;
const stopService =
status === 'STOPPED'
? null
: <TooltipButton onClick={handleStopClick} disabled={disabled}>
Stop
</TooltipButton>;
: <li>
<TooltipButton onClick={handleStopClick} disabled={disabled}>
Stop
</TooltipButton>
</li>;
return (
<Tooltip {...p} onBlur={onBlur}>
<TooltipButton onClick={handleScaleClick} disabled={disabled}>
Scale
</TooltipButton>
<TooltipButton onClick={handleRestartClick} disabled={disabled}>
Restart
</TooltipButton>
{startService}
{stopService}
<TooltipDivider />
<TooltipButton onClick={handleDeleteClick} disabled={disabled}>
Delete
</TooltipButton>
<Tooltip {...position} onBlur={onBlur}>
<TooltipList>
<li>
<TooltipButton onClick={handleScaleClick} disabled={disabled}>
Scale
</TooltipButton>
</li>
<li>
<TooltipButton onClick={handleRestartClick} disabled={disabled}>
Restart
</TooltipButton>
</li>
{startService}
{stopService}
<TooltipDivider />
<li>
<TooltipButton onClick={handleDeleteClick} disabled={disabled}>
Delete
</TooltipButton>
</li>
</TooltipList>
</Tooltip>
);
};

View File

@ -6,13 +6,13 @@ import { StatusLoader, P } from 'joyent-ui-toolkit';
const StyledStatusContainer = styled.div`
display: inline-block;
margin: 0;
margin: 0 0 ${remcalc(15)} 0;
height: ${remcalc(54)};
width: ${remcalc(200)};
`;
const StyledStatus = P.extend`
margin: 0;
margin: 0 0 ${remcalc(6)} 0;
font-size: ${remcalc(13)};
line-height: ${remcalc(13)};
`;

View File

@ -1 +1,2 @@
export { default as InstanceList } from './list';
export { default as InstancesTooltip } from './tooltip';

View File

@ -1,5 +1,7 @@
import React from 'react';
import React, { Component } from 'react';
import styled from 'styled-components';
import { compose, graphql } from 'react-apollo';
import { connect } from 'react-redux';
import InstancesQuery from '@graphql/Instances.gql';
import forceArray from 'force-array';
import sortBy from 'lodash.sortby';
@ -8,10 +10,18 @@ import { LayoutContainer } from '@components/layout';
import { Title } from '@components/navigation';
import { Loader, ErrorMessage } from '@components/messaging';
import { InstanceListItem, EmptyInstances } from '@components/instances';
import { toggleInstancesTooltip } from '@root/state/actions';
import { withNotFound, GqlPaths } from '@containers/navigation';
const InstanceList = ({ deploymentGroup, instances = [], loading, error }) => {
const InstanceList = ({
deploymentGroup,
instances = [],
loading,
error,
instancesTooltip,
toggleInstancesTooltip
}) => {
const _title = <Title>Instances</Title>;
if (loading && !forceArray(instances).length) {
@ -44,11 +54,48 @@ const InstanceList = ({ deploymentGroup, instances = [], loading, error }) => {
);
}
const handleHealthMouseOver = (evt, instance) => {
handleMouseOver(evt, instance, 'healthy');
};
const handleStatusMouseOver = (evt, instance) => {
handleMouseOver(evt, instance, 'status');
};
const handleMouseOver = (evt, instance, type) => {
const label = evt.currentTarget;
const labelRect = label.getBoundingClientRect();
const offset = type === 'healthy'
? 48 : type === 'status' ? 36 : 0;
const position = {
left:
`${window.scrollX + labelRect.left + offset}px`,
top: `${window.scrollY + labelRect.bottom}px`
};
const tooltipData = {
instance,
position,
type
}
toggleInstancesTooltip(tooltipData);
};
const handleMouseOut = (evt) => {
toggleInstancesTooltip({ show: false });
};
const instanceList = instances.map((instance, index) =>
<InstanceListItem
instance={instance}
key={instance.id}
toggleCollapsed={() => null}
onHealthMouseOver={handleHealthMouseOver}
onStatusMouseOver={handleStatusMouseOver}
onMouseOut={handleMouseOut}
/>
);
@ -60,7 +107,17 @@ const InstanceList = ({ deploymentGroup, instances = [], loading, error }) => {
{_instances}
</LayoutContainer>
);
};
}
const mapStateToProps = (state, ownProps) => ({
instancesTooltip: state.ui.instances.tooltip
});
const mapDispatchToProps = dispatch => ({
toggleInstancesTooltip: data => dispatch(toggleInstancesTooltip(data))
});
const UiConnect = connect(mapStateToProps, mapDispatchToProps);
const InstanceListGql = graphql(InstancesQuery, {
options(props) {
@ -94,6 +151,7 @@ const InstanceListGql = graphql(InstancesQuery, {
});
export default compose(
UiConnect,
InstanceListGql,
withNotFound([
GqlPaths.DEPLOYMENT_GROUP,

View File

@ -0,0 +1,68 @@
import React from 'react';
import { connect } from 'react-redux';
import styled from 'styled-components';
import { Tooltip, TooltipLabel } from 'joyent-ui-toolkit';
import { ServicesQuickActions } from '@components/services';
const StyledContainer = styled.div`
position: absolute;
top: 0;
left: 0;
`;
const healthMessages = {
healthy: 'Your instance is operating as expected',
unhealthy: 'Your instance is not operating as expected',
maintenance: 'You\'ve set your instance to this manually, use the Container Pilot CLI to change',
unknown: 'We\'ve connected to your instance but we have no health information',
unavailable: 'We cannot connect to your instance',
};
const statusMessages = {
running: 'Your instance is operating',
provisioning: 'Your instance is downloading dependencies and compiling',
ready: 'Your instance finished provisioning and is ready to be run, it\'ll be running soon',
stopping: 'Your instance is going to be stopped soon',
stopped: 'Your instance isn\'t doing anything, you can start it',
offline: 'We have no idea what this means, do you??????',
failed: 'Your instance has crashed',
unknown: 'We cannot work out what status your instance is in',
};
const InstancesTooltip = ({
instancesTooltip
}) => {
if(instancesTooltip.show) {
const {
type,
instance
} = instancesTooltip;
const message = type === 'healthy'
? healthMessages[instance.healthy.toLowerCase()]
: type === 'status'
? statusMessages[instance.status.toLowerCase()]
: '';
return (
<StyledContainer>
<Tooltip {...instancesTooltip.position} secondary>
<TooltipLabel>{message}</TooltipLabel>
</Tooltip>
</StyledContainer>
)
}
return null;
};
const mapStateToProps = (state, ownProps) => ({
instancesTooltip: state.ui.instances.tooltip
});
const mapDispatchToProps = dispatch => ({});
const UiConnect = connect(mapStateToProps, mapDispatchToProps);
export default UiConnect(InstancesTooltip);

View File

@ -1,3 +1,4 @@
export { default as ServiceList } from './list';
export { default as ServicesTopology } from './topology';
export { default as ServicesMenu } from './menu';
export { default as ServicesQuickActions } from './quick-actions';

View File

@ -6,21 +6,14 @@ import forceArray from 'force-array';
import sortBy from 'lodash.sortby';
import ServicesQuery from '@graphql/Services.gql';
import ServicesRestartMutation from '@graphql/ServicesRestartMutation.gql';
import ServicesStopMutation from '@graphql/ServicesStopMutation.gql';
import ServicesStartMutation from '@graphql/ServicesStartMutation.gql';
import { processServices } from '@root/state/selectors';
import { toggleServicesQuickActions } from '@root/state/actions';
import { withNotFound, GqlPaths } from '@containers/navigation';
import { LayoutContainer } from '@components/layout';
import { Loader, ErrorMessage } from '@components/messaging';
import { ServiceListItem } from '@components/services';
import { ServicesQuickActions } from '@components/services';
import { withNotFound, GqlPaths } from '@containers/navigation';
const StyledContainer = styled.div`
position: relative;
`;
@ -34,28 +27,13 @@ class ServiceList extends Component {
};
}
ref(name) {
this._refs = this._refs || {};
return el => {
this._refs[name] = el;
};
}
render() {
const {
deploymentGroup,
services,
loading,
error,
servicesQuickActions,
toggleServicesQuickActions,
url,
push,
restartServices,
stopServices,
startServices,
location
} = this.props;
if (loading && !forceArray(services).length) {
@ -90,17 +68,13 @@ class ServiceList extends Component {
}
const handleQuickActionsClick = (evt, service) => {
const list = this._refs.container;
const listRect = list.getBoundingClientRect();
const button = evt.currentTarget;
const buttonRect = button.getBoundingClientRect();
const position = {
left:
buttonRect.left -
listRect.left +
(buttonRect.right - buttonRect.left) / 2,
top: buttonRect.bottom - listRect.top
`${buttonRect.left + window.scrollX + (buttonRect.right - buttonRect.left) / 2}px`,
top: `${buttonRect.bottom + window.scrollY}px`
};
toggleServicesQuickActions({
@ -109,41 +83,6 @@ class ServiceList extends Component {
});
};
const handleRestartClick = (evt, service) => {
this.setState({ errors: {} });
restartServices(service.id).catch(err => {
this.setState({ errors: { restart: err } });
});
};
const handleStopClick = (evt, service) => {
this.setState({ errors: {} });
stopServices(service.id).catch(err => {
this.setState({ errors: { stop: err } });
});
};
const handleStartClick = (evt, service) => {
this.setState({ errors: {} });
startServices(service.id).catch(err => {
this.setState({ errors: { start: err } });
});
};
const handleScaleClick = (evt, service) => {
toggleServicesQuickActions({ show: false });
push(`${url}/${service.slug}/scale`);
};
const handleDeleteClick = (evt, service) => {
toggleServicesQuickActions({ show: false });
push(`${url}/${service.slug}/delete`);
};
const handleQuickActionsBlur = o => {
toggleServicesQuickActions({ show: false });
};
let renderedError = null;
if (
@ -181,31 +120,14 @@ class ServiceList extends Component {
<LayoutContainer>
{renderedError}
<StyledContainer>
<div ref={this.ref('container')}>
{serviceList}
<ServicesQuickActions
position={servicesQuickActions.position}
service={servicesQuickActions.service}
show={servicesQuickActions.show}
onBlur={handleQuickActionsBlur}
onRestartClick={handleRestartClick}
onStopClick={handleStopClick}
onStartClick={handleStartClick}
onScaleClick={handleScaleClick}
onDeleteClick={handleDeleteClick}
/>
</div>
{serviceList}
</StyledContainer>
</LayoutContainer>
);
}
}
const mapStateToProps = (state, ownProps) => ({
servicesQuickActions: state.ui.services.quickActions,
url: ownProps.match.url.replace(/\/$/, ''),
push: ownProps.history.push
});
const mapStateToProps = (state, ownProps) => ({});
const mapDispatchToProps = dispatch => ({
toggleServicesQuickActions: data => dispatch(toggleServicesQuickActions(data))
@ -232,29 +154,7 @@ const ServicesGql = graphql(ServicesQuery, {
})
});
const ServicesRestartGql = graphql(ServicesRestartMutation, {
props: ({ mutate }) => ({
restartServices: serviceId => mutate({ variables: { ids: [serviceId] } })
})
});
const ServicesStopGql = graphql(ServicesStopMutation, {
props: ({ mutate }) => ({
stopServices: serviceId => mutate({ variables: { ids: [serviceId] } })
})
});
const ServicesStartGql = graphql(ServicesStartMutation, {
props: ({ mutate }) => ({
startServices: serviceId => mutate({ variables: { ids: [serviceId] } })
})
});
const ServiceListWithData = compose(
ServicesGql,
ServicesRestartGql,
ServicesStopGql,
ServicesStartGql,
ServicesGql,
UiConnect,
withNotFound([ GqlPaths.DEPLOYMENT_GROUP ])

View File

@ -0,0 +1,172 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { compose, graphql } from 'react-apollo';
import styled from 'styled-components';
import ServicesRestartMutation from '@graphql/ServicesRestartMutation.gql';
import ServicesStopMutation from '@graphql/ServicesStopMutation.gql';
import ServicesStartMutation from '@graphql/ServicesStartMutation.gql';
import { Tooltip, TooltipLabel } from 'joyent-ui-toolkit';
import { toggleServicesQuickActions } from '@root/state/actions';
import { ServicesQuickActions as QuickActions } from '@components/services';
import { ErrorMessage } from '@components/messaging';
import { LayoutContainer } from '@components/layout';
const StyledContainer = styled.div`
position: absolute;
top: 0;
left: 0;
`;
class ServicesQuickActions extends Component {
constructor(props) {
super(props);
this.state = {
errors: {}
}
}
render() {
const {
servicesQuickActions,
toggleServicesQuickActions,
restartServices,
stopServices,
startServices,
url,
push
} = this.props;
let errorMessage = null;
let quickActions = null;
if (
this.state.errors.stop ||
this.state.errors.start ||
this.state.errors.restart
) {
const message = this.state.errors.stop
? 'An error occurred while attempting to stop your service.'
: this.state.errors.start
? 'An error occurred while attempting to start your service.'
: this.state.errors.restart
? 'An error occurred while attempting to restart your service.'
: '';
errorMessage = (
<LayoutContainer>
<ErrorMessage title="Ooops!" message={message} />
</LayoutContainer>
);
}
if(servicesQuickActions.show) {
const handleTooltipBlur = evt => {
toggleServicesQuickActions({ show: false });
};
const handleRestartClick = (evt, service) => {
this.setState({errors: {}});
toggleServicesQuickActions({ show: false });
restartServices(service.id).catch(err => {
this.setState({ errors: { restart: err } });
});
};
const handleStopClick = (evt, service) => {
this.setState({errors: {}});
toggleServicesQuickActions({ show: false });
stopServices(service.id).catch(err => {
this.setState({ errors: { stop: err } });
});
};
const handleStartClick = (evt, service) => {
this.setState({errors: {}});
toggleServicesQuickActions({ show: false });
startServices(service.id).catch(err => {
this.setState({ errors: { start: err } });
});
};
const handleScaleClick = (evt, service) => {
this.setState({errors: {}});
toggleServicesQuickActions({ show: false });
push(`${url}/${service.slug}/scale`);
};
const handleDeleteClick = (evt, service) => {
this.setState({errors: {}});
toggleServicesQuickActions({ show: false });
push(`${url}/${service.slug}/delete`);
};
quickActions = (
<StyledContainer>
<QuickActions
service={servicesQuickActions.service}
show={servicesQuickActions.show}
position={servicesQuickActions.position}
onBlur={handleTooltipBlur}
onRestartClick={handleRestartClick}
onStopClick={handleStopClick}
onStartClick={handleStartClick}
onScaleClick={handleScaleClick}
onDeleteClick={handleDeleteClick}
/>
</StyledContainer>
)
}
if(quickActions || errorMessage) {
return (
<div>
{errorMessage}
{quickActions}
</div>
)
}
return null;
}
}
const mapStateToProps = (state, ownProps) => ({
servicesQuickActions: state.ui.services.quickActions,
url: ownProps.match.url.replace(/\/$/, ''),
push: ownProps.history.push
});
const mapDispatchToProps = dispatch => ({
toggleServicesQuickActions: data => dispatch(toggleServicesQuickActions(data))
});
const UiConnect = connect(mapStateToProps, mapDispatchToProps);
const ServicesRestartGql = graphql(ServicesRestartMutation, {
props: ({ mutate }) => ({
restartServices: serviceId => mutate({ variables: { ids: [serviceId] } })
})
});
const ServicesStopGql = graphql(ServicesStopMutation, {
props: ({ mutate }) => ({
stopServices: serviceId => mutate({ variables: { ids: [serviceId] } })
})
});
const ServicesStartGql = graphql(ServicesStartMutation, {
props: ({ mutate }) => ({
startServices: serviceId => mutate({ variables: { ids: [serviceId] } })
})
});
const ConnectedServicesQuickActions = compose(
ServicesRestartGql,
ServicesStopGql,
ServicesStartGql,
UiConnect
)(ServicesQuickActions);
export default ConnectedServicesQuickActions;

View File

@ -3,23 +3,16 @@ import { compose, graphql } from 'react-apollo';
import { connect } from 'react-redux';
import styled from 'styled-components';
import forceArray from 'force-array';
import ServicesQuery from '@graphql/Services.gql';
import ServicesRestartMutation from '@graphql/ServicesRestartMutation.gql';
import ServicesStopMutation from '@graphql/ServicesStopMutation.gql';
import ServicesStartMutation from '@graphql/ServicesStartMutation.gql';
import unitcalc from 'unitcalc';
import ServicesQuery from '@graphql/Services.gql';
import { processServicesForTopology } from '@root/state/selectors';
import { toggleServicesQuickActions } from '@root/state/actions';
import { withNotFound, GqlPaths } from '@containers/navigation';
import { LayoutContainer } from '@components/layout';
import { Loader, ErrorMessage } from '@components/messaging';
import { ServicesQuickActions } from '@components/services';
import { Topology } from 'joyent-ui-toolkit';
import { withNotFound, GqlPaths } from '@containers/navigation';
const StyledBackground = styled.div`
padding: ${unitcalc(4)};
background-color: ${props => props.theme.whiteActive};
@ -38,6 +31,14 @@ class ServicesTopology extends Component {
};
}
ref(name) {
this._refs = this._refs || {};
return el => {
this._refs[name] = el;
};
}
render() {
const {
url,
@ -46,12 +47,7 @@ class ServicesTopology extends Component {
services,
loading,
error,
servicesQuickActions,
toggleServicesQuickActions,
restartServices,
stopServices,
startServices,
location
} = this.props;
if (loading && !forceArray(services).length) {
@ -86,42 +82,17 @@ class ServicesTopology extends Component {
}
const handleQuickActionsClick = (evt, tooltipData) => {
toggleServicesQuickActions(tooltipData);
};
const handleTooltipBlur = evt => {
toggleServicesQuickActions({ show: false });
};
const handleRestartClick = (evt, service) => {
this.setState({ errors: {} });
restartServices(service.id).catch(err => {
this.setState({ errors: { restart: err } });
});
};
const handleStopClick = (evt, service) => {
this.setState({ errors: {} });
stopServices(service.id).catch(err => {
this.setState({ errors: { stop: err } });
});
};
const handleStartClick = (evt, service) => {
this.setState({ errors: {} });
startServices(service.id).catch(err => {
this.setState({ errors: { start: err } });
});
};
const handleScaleClick = (evt, service) => {
toggleServicesQuickActions({ show: false });
push(`${url}/${service.slug}/scale`);
};
const handleDeleteClick = (evt, service) => {
toggleServicesQuickActions({ show: false });
push(`${url}/${service.slug}/delete`);
const container = this._refs.container;
const containerRect = container.getBoundingClientRect();
const position = {
top: `${containerRect.top + window.scrollY + tooltipData.position.top}px`,
left: `${containerRect.left + window.scrollX + tooltipData.position.left}px`
}
const data = {
...tooltipData,
position
}
toggleServicesQuickActions(data);
};
const handleNodeTitleClick = (evt, { service }) => {
@ -155,22 +126,13 @@ class ServicesTopology extends Component {
{renderedError}
<StyledBackground>
<StyledContainer>
<Topology
services={services}
onQuickActionsClick={handleQuickActionsClick}
onNodeTitleClick={handleNodeTitleClick}
/>
<ServicesQuickActions
service={servicesQuickActions.service}
show={servicesQuickActions.show}
position={servicesQuickActions.position}
onBlur={handleTooltipBlur}
onRestartClick={handleRestartClick}
onStopClick={handleStopClick}
onStartClick={handleStartClick}
onScaleClick={handleScaleClick}
onDeleteClick={handleDeleteClick}
/>
<div ref={this.ref('container')}>
<Topology
services={services}
onQuickActionsClick={handleQuickActionsClick}
onNodeTitleClick={handleNodeTitleClick}
/>
</div>
</StyledContainer>
</StyledBackground>
</div>
@ -179,7 +141,6 @@ class ServicesTopology extends Component {
}
const mapStateToProps = (state, ownProps) => ({
servicesQuickActions: state.ui.services.quickActions,
url: ownProps.match.url.replace(/\/$/, ''),
push: ownProps.history.push
});
@ -209,28 +170,7 @@ const ServicesGql = graphql(ServicesQuery, {
})
});
const ServicesRestartGql = graphql(ServicesRestartMutation, {
props: ({ mutate }) => ({
restartServices: serviceId => mutate({ variables: { ids: [serviceId] } })
})
});
const ServicesStopGql = graphql(ServicesStopMutation, {
props: ({ mutate }) => ({
stopServices: serviceId => mutate({ variables: { ids: [serviceId] } })
})
});
const ServicesStartGql = graphql(ServicesStartMutation, {
props: ({ mutate }) => ({
startServices: serviceId => mutate({ variables: { ids: [serviceId] } })
})
});
const ServicesTopologyWithData = compose(
ServicesRestartGql,
ServicesStopGql,
ServicesStartGql,
ServicesGql,
UiConnect,
withNotFound([ GqlPaths.DEPLOYMENT_GROUP ])

View File

@ -4,7 +4,6 @@ import styled from 'styled-components';
import { Header, Breadcrumb, Menu } from '@containers/navigation';
import { ServiceScale, ServiceDelete } from '@containers/service';
import { InstanceList } from '@containers/instances';
import Manifest from '@containers/manifest';
import Environment from '@containers/environment';
@ -17,9 +16,19 @@ import {
import {
ServiceList,
ServicesTopology,
ServicesMenu
ServicesMenu,
ServicesQuickActions
} from '@containers/services';
import {
InstanceList,
InstancesTooltip
} from '@containers/instances';
import {
Tooltip
} from '@containers/tooltip';
import { DeploymentGroupDelete } from '@containers/deployment-group';
import { NotFound } from '@components/navigation';
@ -88,6 +97,28 @@ const App = p =>
component={ServicesMenu}
/>
<Route
path="/deployment-groups/:deploymentGroup/services-list"
component={ServicesQuickActions}
/>
<Route
path="/deployment-groups/:deploymentGroup/services-topology"
component={ServicesQuickActions}
/>
<Route
path="/deployment-groups/:deploymentGroup/instances"
exact
component={InstancesTooltip}
/>
<Route
path="/deployment-groups/:deploymentGroup/services/:service/instances"
exact
component={InstancesTooltip}
/>
<Switch>
<Route
path="/deployment-groups/:deploymentGroup/delete"

View File

@ -8,3 +8,7 @@ const APP = constantCase(process.env.APP_NAME);
export const toggleServicesQuickActions = createAction(
`${APP}/TOGGLE_SERVICES_QUICK_ACTIONS`
);
export const toggleInstancesTooltip = createAction(
`${APP}/TOGGLE_INSTANCES_TOOLTIP`
);

View File

@ -1,35 +1,67 @@
import { handleActions } from 'redux-actions';
import { toggleServicesQuickActions } from '@state/actions';
import { toggleServicesQuickActions, toggleInstancesTooltip } from '@state/actions';
const _toggleServicesQuickActions = (state, action) => {
const { position, service, show } = action.payload;
const s =
show === undefined
? !state.services.quickActions.service ||
service.id !== state.services.quickActions.service.id
: show;
const quickActions = s
? {
show: s,
position,
service
}
: {
show: false
};
return {
...state,
services: {
...state.services,
quickActions
}
};
};
const _toggleInstancesTooltip = (state, action) => {
const { position, instance, show, type } = action.payload;
const s =
show === undefined
? !state.instances.tooltip.instance ||
instance.id !== state.instances.tooltip.instance.id
: show;
const tooltip = s
? {
show: true,
position,
instance,
type
}
: {
show: false
};
return {
...state,
instances: {
...state.instances,
tooltip
}
};
};
export default handleActions(
{
[toggleServicesQuickActions.toString()]: (state, action) => {
const { position, service, show } = action.payload;
const s =
show === undefined
? !state.services.quickActions.service ||
service.id !== state.services.quickActions.service.id
: show;
const quickActions = s
? {
show: s,
position,
service
}
: {
show: false
};
return {
...state,
services: {
...state.services,
quickActions
}
};
}
[toggleServicesQuickActions.toString()]: _toggleServicesQuickActions,
[toggleInstancesTooltip.toString()]: _toggleInstancesTooltip
},
{}
);

View File

@ -61,7 +61,6 @@ const activeInstanceStatuses = [
'READY',
'ACTIVE',
'RUNNING',
'STOPPING',
'INCOMPLETE'
];
@ -103,8 +102,12 @@ const getInstancesActive = instanceStatuses => {
const getInstancesHealthy = instances => {
return instances.reduce(
(healthy, instance) => (instance.healthy === 'HEALTHY' ? healthy : false),
true
(healthy, instance) => ({
total: healthy.total + 1,
healthy: instance.healthy === 'HEALTHY' ?
healthy.healthy + 1 : healthy.healthy
}),
{total: 0, healthy: 0}
);
};

View File

@ -30,6 +30,11 @@ const state = {
quickActions: {
show: false
}
},
instances: {
tooltip: {
show: false
}
}
}
};

View File

@ -76,6 +76,7 @@ const getUnfilteredServices = query => {
const getServices = query => {
// get all services
const services = getUnfilteredServices(query)
// get all instances
.then(services =>
@ -95,9 +96,10 @@ const getServices = query => {
);
// get all the serviceIds of the available instances
// and then get the servcies with those ids
return uniq(
const ret = uniq(
availableInstances.map(({ serviceId }) => serviceId)
).map(serviceId => lfind(services, ['id', serviceId]));
return ret;
});
return Promise.resolve(services)
@ -331,12 +333,12 @@ const updateServiceAndInstancesStatus = (
instancesStatus
) => {
return Promise.all([
getServices({ id: serviceId }),
getServices({ parentId: serviceId })
getServices({ id: serviceId })/*,
getServices({ parentId: serviceId })*/
])
.then(services =>
services.reduce((services, service) => services.concat(service), [])
)
.then(services => {
return services.reduce((services, service) => services.concat(service), [])
})
.then(services => {
updateServiceStatus(services, serviceStatus);
return Promise.all(
@ -356,8 +358,8 @@ const updateServiceAndInstancesStatus = (
})
.then(() =>
Promise.all([
getUnfilteredServices({ id: serviceId }),
getUnfilteredServices({ parentId: serviceId })
getUnfilteredServices({ id: serviceId })/*,
getUnfilteredServices({ parentId: serviceId })*/
])
)
.then(services =>

View File

@ -114,8 +114,8 @@
"serviceId": "6d31aff4-de1e-4042-a983-fbd23d5c530c",
"deploymentGroupId": "e0ea0c02-55cc-45fe-8064-3e5176a59401",
"machineId": "8d8a2238-d981-4849-b523-a37456fbe20b",
"status": "RUNNING",
"healthy": "HEALTHY"
"status": "STOPPING",
"healthy": "MAINTENANCE"
},
{
"id": "68d3046e-8e34-4f5d-a0e5-db3795a250fd",
@ -132,8 +132,8 @@
"serviceId": "6d31aff4-de1e-4042-a983-fbd23d5c530c",
"deploymentGroupId": "e0ea0c02-55cc-45fe-8064-3e5176a59401",
"machineId": "d6871ac4-6433-40c3-89e8-8853ce7f8571",
"status": "RUNNING",
"healthy": "HEALTHY"
"status": "OFFLINE",
"healthy": "UNAVAILABLE"
},
{
"id": "25f6bc62-63b8-4959-908e-1f6d7ff6341d",
@ -141,8 +141,8 @@
"serviceId": "6d31aff4-de1e-4042-a983-fbd23d5c530c",
"deploymentGroupId": "e0ea0c02-55cc-45fe-8064-3e5176a59401",
"machineId": "d89612c8-0578-474a-b45d-98a1dcf6dd18",
"status": "RUNNING",
"healthy": "HEALTHY"
"status": "FAILED",
"healthy": "UNHEALTHY"
},
{
"id": "8be01042-0281-4a77-a357-25979e87bf3d",
@ -151,7 +151,7 @@
"deploymentGroupId": "e0ea0c02-55cc-45fe-8064-3e5176a59401",
"machineId": "3a9fbaf8-722b-463a-86bd-8d3afe0dd759",
"status": "RUNNING",
"healthy": "HEALTHY"
"healthy": "UNKNOWN"
},
{
"id": "3d652e9d-73e8-4a6f-8171-84fa83740662",

View File

@ -14,5 +14,6 @@ export const border = {
checked: css`${remcalc(1)} solid ${props => props.theme.primary}`,
unchecked: css`${remcalc(1)} solid ${props => props.theme.grey}`,
confirmed: css`${remcalc(1)} solid ${props => props.theme.grey}`,
error: css`${remcalc(1)} solid ${props => props.theme.red}`
error: css`${remcalc(1)} solid ${props => props.theme.red}`,
secondary: css`${remcalc(1)} solid ${props => props.theme.secondaryActive}`,
};

View File

@ -3,7 +3,7 @@ import Baseline from '../baseline';
import paperEffect from '../paper-effect';
import { bottomShaddow, bottomShaddowDarker } from '../boxes';
import remcalc from 'remcalc';
import is from 'styled-is';
import is, { isNot } from 'styled-is';
import { Row } from 'react-styled-flexboxgrid';
import PropTypes from 'prop-types';
import React from 'react';
@ -38,6 +38,10 @@ const StyledCard = Row.extend`
${is('stacked')`
${paperEffect}
`};
${isNot('active')`
background-color: ${props => props.theme.disabled};
`};
`;
/**
@ -47,7 +51,7 @@ const Card = ({
children,
collapsed = false,
headed = false,
disabled = false,
active = true,
...rest
}) => {
const render = value => {
@ -55,14 +59,14 @@ const Card = ({
fromHeader: (value || {}).fromHeader,
headed,
collapsed,
disabled
active
};
return (
<Broadcast channel="card" value={newValue}>
<StyledCard
name="card"
disabled={disabled}
active={active}
collapsed={collapsed}
headed={headed}
{...rest}

View File

@ -12,7 +12,8 @@ const StyledTitle = Title.extend`
${typography.fontFamily};
${typography.normal};
flex-grow: 2;
flex-grow: 1;
flex-basis: ${remcalc(90)};
${isNot('collapsed')`
padding-bottom: ${remcalc(12)};
@ -21,11 +22,6 @@ const StyledTitle = Title.extend`
const InnerDescription = styled.div`
justify-content: flex-start;
${is('collapsed')`
justify-content: flex-end;
margin-left: auto;
`};
`;
const Description = ({ children, ...rest }) => {

View File

@ -2,7 +2,7 @@ import React from 'react';
import { Broadcast, Subscriber } from 'react-broadcast';
import remcalc from 'remcalc';
import PropTypes from 'prop-types';
import is from 'styled-is';
import is, { isNot } from 'styled-is';
import Baseline from '../baseline';
import Card from './card';
@ -17,16 +17,15 @@ const StyledCard = Card.extend`
width: calc(100% + ${remcalc(2)});
margin: ${remcalc(-1)} ${remcalc(-1)} 0 ${remcalc(-1)};
${is('disabled')`
${isNot('active')`
background-color: ${props => props.theme.disabled};
border-color: ${props => props.theme.grey};
color: ${props => props.theme.grey};
`};
`;
const Header = ({ children, ...rest }) => {
const render = value => {
const { disabled } = value;
const { active } = value;
const newValue = {
...value,
@ -37,7 +36,7 @@ const Header = ({ children, ...rest }) => {
<Broadcast channel="card" value={newValue}>
<StyledCard
name="card-header"
disabled={disabled}
active={active}
collapsed
headed
{...rest}

View File

@ -5,9 +5,13 @@ import remcalc from 'remcalc';
import Label from '../label';
const StyledLabel = Label.extend`
display: inline-block;
${props => (props.color === 'light' ? `color: ${props.theme.white};` : '')};
${props => (props.color === 'disabled' ? `color: ${props.theme.grey};` : '')};
${props => (props.color === 'disabled' ? `color: ${props.theme.text};` : '')};
margin-left: ${props => (props.iconPosition === 'left' ? remcalc(24) : 0)};
&::first-letter {
text-transform: capitalize;
}
`;
const StyledIconContainer = styled.div`
@ -15,7 +19,7 @@ const StyledIconContainer = styled.div`
> svg {
${props => (props.color === 'light' ? `fill: ${props.theme.white};` : '')};
${props => (props.color === 'disabled' ? `fill: ${props.theme.grey};` : '')};
${props => (props.color === 'disabled' ? `fill: ${props.theme.text};` : '')};
}
`;

View File

@ -3,7 +3,7 @@ import styled from 'styled-components';
import { Nav } from 'normalized-styled-components';
import Baseline from '../baseline';
import remcalc from 'remcalc';
import is from 'styled-is';
import is, { isNot } from 'styled-is';
import PropTypes from 'prop-types';
import Button from '../button';
import React from 'react';
@ -17,7 +17,7 @@ const StyledNav = Nav.extend`
border-left-color: ${props => props.theme.primaryDesaturatedActive};
`};
${is('disabled')`
${isNot('active')`
border-left-color: ${props => props.theme.grey};
`};
`;
@ -54,6 +54,19 @@ const StyledButton = Button.extend`
&:active:focus {
border-width: 0;
}
${isNot('active')`
background-color: ${props => props.theme.disabled};
border-color: ${props => props.theme.grey};
&:focus,
&:hover,
&:active,
&:active:hover,
&:active:focus {
background-color: ${props => props.theme.grey};
}
`}
`;
const StyledContainer = styled.div`
@ -73,8 +86,8 @@ const StyledCircle = styled.div`
background-color: ${props => props.theme.secondary};
`};
${is('disabled')`
background-color: ${props => props.theme.grey};
${isNot('active')`
background-color: ${props => props.theme.text};
`};
`;
@ -82,20 +95,20 @@ const Options = ({ children, ...rest }) => {
const render = ({
fromHeader = false,
collapsed = false,
disabled = false
active = true
}) =>
<StyledNav disabled={disabled} fromHeader={fromHeader} name="card-options">
<StyledNav active={active} fromHeader={fromHeader} name="card-options">
<StyledButton
secondary={!fromHeader}
collapsed={collapsed}
disabled={disabled}
active={active}
rect
{...rest}
>
<StyledContainer>
<StyledCircle disabled={disabled} secondary={!fromHeader} />
<StyledCircle disabled={disabled} secondary={!fromHeader} />
<StyledCircle disabled={disabled} secondary={!fromHeader} />
<StyledCircle active={active} secondary={!fromHeader} />
<StyledCircle active={active} secondary={!fromHeader} />
<StyledCircle active={active} secondary={!fromHeader} />
</StyledContainer>
</StyledButton>
</StyledNav>;

View File

@ -2,7 +2,7 @@ import { Subscriber } from 'react-broadcast';
import typography from '../typography';
import Baseline from '../baseline';
import { Col } from 'react-styled-flexboxgrid';
import is from 'styled-is';
import is, { isNot } from 'styled-is';
import remcalc from 'remcalc';
import PropTypes from 'prop-types';
import React from 'react';
@ -20,16 +20,16 @@ const StyledCol = Col.extend`
display: none;
`};
${is('disabled')`
${isNot('active')`
color: ${props => props.theme.grey};
`};
`;
const Outlet = ({ children, ...rest }) => {
const render = ({ disabled = false, collapsed = false }) =>
const render = ({ active = true, collapsed = false }) =>
<StyledCol
name="card-outlet"
disabled={disabled}
active={active}
collapsed={collapsed}
xs={6}
{...rest}

View File

@ -3,7 +3,7 @@ import styled from 'styled-components';
import Baseline from '../baseline';
import typography from '../typography';
import remcalc from 'remcalc';
import is from 'styled-is';
import is, { isNot } from 'styled-is';
import PropTypes from 'prop-types';
import Title from './title';
import React from 'react';
@ -27,10 +27,6 @@ const Span = styled.span`
${is('fromHeader')`
color: ${props => props.theme.white};
`};
${is('disabled')`
color: ${props => props.theme.grey};
`};
`;
const StyledTitle = Title.extend`
@ -48,7 +44,7 @@ const StyledTitle = Title.extend`
const Subtitle = ({ children, ...props }) => {
const render = ({
disabled = false,
active = true,
fromHeader = false,
collapsed = false
}) =>
@ -56,7 +52,7 @@ const Subtitle = ({ children, ...props }) => {
name="card-subtitle"
fromHeader={fromHeader}
collapsed={collapsed}
disabled={disabled}
active={active}
{...props}
>
<Span fromHeader={fromHeader} collapsed={collapsed}>

View File

@ -3,7 +3,7 @@ import isString from 'lodash.isstring';
import typography from '../typography';
import Baseline from '../baseline';
import remcalc from 'remcalc';
import is from 'styled-is';
import is, { isNot } from 'styled-is';
import styled from 'styled-components';
import PropTypes from 'prop-types';
import React from 'react';
@ -21,6 +21,7 @@ const Container = styled.div`
justify-content: flex-start;
flex-grow: 2;
flex-basis: ${remcalc(90)};
width: 100%;
padding: ${remcalc(12)} ${remcalc(18)} 0 ${remcalc(18)};
@ -29,12 +30,8 @@ const Container = styled.div`
color: ${props => props.theme.white};
`};
${is('disabled')`
color: ${props => props.theme.grey};
`};
${is('collapsed')`
flex-grow: 0;
flex-grow: 6;
flex-direction: column;
width: auto;
justify-content: center;
@ -61,16 +58,17 @@ const Title = ({ children, ...rest }) => {
const render = ({
collapsed = false,
disabled = false,
active = true,
fromHeader = false
}) =>
<Container
collapsed={collapsed}
fromHeader={fromHeader}
disabled={disabled}
active={active}
name="card-title"
xs={collapsed ? 6 : 12}
{...rest}
name='container'
>
{_children}
</Container>;

View File

@ -18,6 +18,9 @@ const StyledSelectList = styled(Tooltip)`
position: relative;
display: block;
left: auto;
margin: 0;
padding: 0;
list-style-type: none;
}
ul:after, ul:before {
left: 97%;
@ -107,15 +110,19 @@ class Dropdown extends Component {
<StyledArrowIcon onClick={this.toggleDropdown} />
{this.state.isDroppedDown &&
<StyledSelectList>
{data.map((val, index) =>
<DropdownItem
key={index}
value={val}
onClick={this.dropdownOnChange}
>
{val}
</DropdownItem>
)}
<ul>
{data.map((val, index) =>
<li>
<DropdownItem
key={index}
value={val}
onClick={this.dropdownOnChange}
>
{val}
</DropdownItem>
</li>
)}
</ul>
</StyledSelectList>}
</Container>
);

View File

@ -1,7 +1,13 @@
import Baseline from '../baseline';
// eslint-disable-next-line no-unused-vars
import React from 'react';
import styled from 'styled-components';
import HealthyIcon from './svg/icon_healthy.svg';
export default Baseline(HealthyIcon);
const StyledHealthyIcon = styled(HealthyIcon)`
fill: ${props => !props.healthy || props.healthy === 'HEALTHY'
? props.theme.green : props.theme.orange};
`;
export default Baseline(StyledHealthyIcon);

View File

@ -4,12 +4,12 @@
<title>icon: state</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Page-1" stroke="none" stroke-width="1" fill-rule="evenodd">
<g id="topology" transform="translate(-489.000000, -441.000000)">
<g id="services" transform="translate(0.000000, 348.000000)">
<g id="service:-nginx" transform="translate(476.000000, 36.000000)">
<g id="icon:-state" transform="translate(13.000000, 57.000000)">
<circle id="Oval" fill="#00AF66" cx="9" cy="9" r="9"></circle>
<circle id="Oval" cx="9" cy="9" r="9"></circle>
<path d="M9.47745233,6.60270759 L8.95496861,7.04565311 L8.51133742,6.60270759 C7.70841297,5.79909747 6.40563205,5.79909747 5.60270759,6.60270759 C4.79909747,7.40631772 4.79909747,8.70841297 5.60270759,9.5120231 L8.95496861,12.8642841 L12.3668833,9.5120231 C13.1698077,8.70841297 13.2301471,7.40631772 12.4265369,6.60270759 C11.6229268,5.79909747 10.2810625,5.79909747 9.47745233,6.60270759 Z" id="icon:-health" fill="#FFFFFF"></path>
</g>
</g>

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -17,7 +17,7 @@ export { default as Chevron } from './chevron';
export { default as CloseButton } from './close-button';
export { default as Divider } from './divider';
export { default as IconButton } from './icon-button';
export { Tooltip, TooltipButton, TooltipDivider } from './tooltip';
export { Tooltip, TooltipButton, TooltipDivider, TooltipList, TooltipLabel } from './tooltip';
export { Dropdown } from './dropdown';
export { default as StatusLoader } from './status-loader';
export { default as Message } from './message';

View File

@ -45,8 +45,6 @@ const StyledButton = styled(Button)`
`;
const TooltipButton = props =>
<li>
<StyledButton {...props} />
</li>;
<StyledButton {...props} />;
export default TooltipButton;

View File

@ -1,3 +1,5 @@
export { default as Tooltip } from './tooltip';
export { default as TooltipButton } from './button';
export { default as TooltipDivider } from './divider';
export { default as TooltipList } from './list';
export { default as TooltipLabel } from './label';

View File

@ -0,0 +1,9 @@
import styled from 'styled-components';
import remcalc from 'remcalc';
import P from '../text/p';
export default styled(P)`
margin: 0 ${remcalc(18)};
color: ${props => props.theme.white};
white-space: nowrap;
`;

View File

@ -0,0 +1,7 @@
import styled from 'styled-components';
export default styled.ul`
margin: 0;
padding: 0;
list-style-type: none;
`;

View File

@ -18,19 +18,18 @@ const StyledContainer = styled.div`
}
`;
const StyledList = styled.ul`
const StyledInnerContainer = styled.div`
position: relative;
display: inline-block;
top: ${remcalc(5)};
left: -50%;
margin: 0;
padding: ${unitcalc(2)} 0;
list-style-type: none;
background-color: ${theme.white};
border: ${border.unchecked};
background-color: ${props => props.secondary ? props.theme.secondary : props.theme.white};
border: ${props => props.secondary ? border.secondary : border.unchecked};
box-shadow: ${tooltipShadow};
border-radius: ${borderRadius};
z-index: 1;
z-index: 1000;
&:after,
&:before {
@ -44,13 +43,13 @@ const StyledList = styled.ul`
}
&:after {
border-bottom-color: ${theme.white};
border-bottom-color: ${props => props.secondary ? props.theme.secondary : theme.white};
border-width: ${remcalc(3)};
margin-left: ${remcalc(-3)};
}
&:before {
border-bottom-color: ${theme.grey};
border-bottom-color: ${props => props.secondary ? props.theme.secondaryActive : theme.grey};
border-width: ${remcalc(5)};
margin-left: ${remcalc(-5)};
}
@ -84,27 +83,39 @@ class Tooltip extends Component {
}
render() {
const {
let {
children,
top = 'auto',
left = 'auto',
bottom = 'auto',
right = 'auto',
className,
secondary,
...rest
} = this.props;
if(typeof top === 'number') {
top = `${top}px`
}
if(typeof left === 'number') {
left = `${left}px`
}
if(typeof bottom === 'number') {
bottom = `${bottom}px`
}
if(typeof right === 'number') {
right = `${right}px`
}
return (
<StyledContainer
className={className}
top={top}
left={left}
bottom={bottom}
right={right}
{...rest}
>
<StyledList>
<StyledInnerContainer secondary={secondary}>
{children}
</StyledList>
</StyledInnerContainer>
</StyledContainer>
);
}
@ -116,7 +127,8 @@ Tooltip.propTypes = {
left: PropTypes.string,
bottom: PropTypes.string,
right: PropTypes.string,
onBlur: PropTypes.func
onBlur: PropTypes.func,
secondary: PropTypes.boolean
};
export default Tooltip;