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

View File

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

View File

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

View File

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

View File

@ -1 +1,2 @@
export { default as InstanceList } from './list'; 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 { compose, graphql } from 'react-apollo';
import { connect } from 'react-redux';
import InstancesQuery from '@graphql/Instances.gql'; import InstancesQuery from '@graphql/Instances.gql';
import forceArray from 'force-array'; import forceArray from 'force-array';
import sortBy from 'lodash.sortby'; import sortBy from 'lodash.sortby';
@ -8,10 +10,18 @@ import { LayoutContainer } from '@components/layout';
import { Title } from '@components/navigation'; import { Title } from '@components/navigation';
import { Loader, ErrorMessage } from '@components/messaging'; import { Loader, ErrorMessage } from '@components/messaging';
import { InstanceListItem, EmptyInstances } from '@components/instances'; import { InstanceListItem, EmptyInstances } from '@components/instances';
import { toggleInstancesTooltip } from '@root/state/actions';
import { withNotFound, GqlPaths } from '@containers/navigation'; 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>; const _title = <Title>Instances</Title>;
if (loading && !forceArray(instances).length) { 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) => const instanceList = instances.map((instance, index) =>
<InstanceListItem <InstanceListItem
instance={instance} instance={instance}
key={instance.id} key={instance.id}
toggleCollapsed={() => null} toggleCollapsed={() => null}
onHealthMouseOver={handleHealthMouseOver}
onStatusMouseOver={handleStatusMouseOver}
onMouseOut={handleMouseOut}
/> />
); );
@ -60,7 +107,17 @@ const InstanceList = ({ deploymentGroup, instances = [], loading, error }) => {
{_instances} {_instances}
</LayoutContainer> </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, { const InstanceListGql = graphql(InstancesQuery, {
options(props) { options(props) {
@ -94,6 +151,7 @@ const InstanceListGql = graphql(InstancesQuery, {
}); });
export default compose( export default compose(
UiConnect,
InstanceListGql, InstanceListGql,
withNotFound([ withNotFound([
GqlPaths.DEPLOYMENT_GROUP, 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 ServiceList } from './list';
export { default as ServicesTopology } from './topology'; export { default as ServicesTopology } from './topology';
export { default as ServicesMenu } from './menu'; 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 sortBy from 'lodash.sortby';
import ServicesQuery from '@graphql/Services.gql'; 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 { processServices } from '@root/state/selectors';
import { toggleServicesQuickActions } from '@root/state/actions'; import { toggleServicesQuickActions } from '@root/state/actions';
import { withNotFound, GqlPaths } from '@containers/navigation';
import { LayoutContainer } from '@components/layout'; import { LayoutContainer } from '@components/layout';
import { Loader, ErrorMessage } from '@components/messaging'; import { Loader, ErrorMessage } from '@components/messaging';
import { ServiceListItem } from '@components/services'; import { ServiceListItem } from '@components/services';
import { ServicesQuickActions } from '@components/services';
import { withNotFound, GqlPaths } from '@containers/navigation';
const StyledContainer = styled.div` const StyledContainer = styled.div`
position: relative; position: relative;
`; `;
@ -34,28 +27,13 @@ class ServiceList extends Component {
}; };
} }
ref(name) {
this._refs = this._refs || {};
return el => {
this._refs[name] = el;
};
}
render() { render() {
const { const {
deploymentGroup, deploymentGroup,
services, services,
loading, loading,
error, error,
servicesQuickActions,
toggleServicesQuickActions, toggleServicesQuickActions,
url,
push,
restartServices,
stopServices,
startServices,
location
} = this.props; } = this.props;
if (loading && !forceArray(services).length) { if (loading && !forceArray(services).length) {
@ -90,17 +68,13 @@ class ServiceList extends Component {
} }
const handleQuickActionsClick = (evt, service) => { const handleQuickActionsClick = (evt, service) => {
const list = this._refs.container;
const listRect = list.getBoundingClientRect();
const button = evt.currentTarget; const button = evt.currentTarget;
const buttonRect = button.getBoundingClientRect(); const buttonRect = button.getBoundingClientRect();
const position = { const position = {
left: left:
buttonRect.left - `${buttonRect.left + window.scrollX + (buttonRect.right - buttonRect.left) / 2}px`,
listRect.left + top: `${buttonRect.bottom + window.scrollY}px`
(buttonRect.right - buttonRect.left) / 2,
top: buttonRect.bottom - listRect.top
}; };
toggleServicesQuickActions({ 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; let renderedError = null;
if ( if (
@ -181,31 +120,14 @@ class ServiceList extends Component {
<LayoutContainer> <LayoutContainer>
{renderedError} {renderedError}
<StyledContainer> <StyledContainer>
<div ref={this.ref('container')}> {serviceList}
{serviceList}
<ServicesQuickActions
position={servicesQuickActions.position}
service={servicesQuickActions.service}
show={servicesQuickActions.show}
onBlur={handleQuickActionsBlur}
onRestartClick={handleRestartClick}
onStopClick={handleStopClick}
onStartClick={handleStartClick}
onScaleClick={handleScaleClick}
onDeleteClick={handleDeleteClick}
/>
</div>
</StyledContainer> </StyledContainer>
</LayoutContainer> </LayoutContainer>
); );
} }
} }
const mapStateToProps = (state, ownProps) => ({ const mapStateToProps = (state, ownProps) => ({});
servicesQuickActions: state.ui.services.quickActions,
url: ownProps.match.url.replace(/\/$/, ''),
push: ownProps.history.push
});
const mapDispatchToProps = dispatch => ({ const mapDispatchToProps = dispatch => ({
toggleServicesQuickActions: data => dispatch(toggleServicesQuickActions(data)) 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( const ServiceListWithData = compose(
ServicesGql,
ServicesRestartGql,
ServicesStopGql,
ServicesStartGql,
ServicesGql, ServicesGql,
UiConnect, UiConnect,
withNotFound([ GqlPaths.DEPLOYMENT_GROUP ]) 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 { connect } from 'react-redux';
import styled from 'styled-components'; import styled from 'styled-components';
import forceArray from 'force-array'; 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 unitcalc from 'unitcalc';
import ServicesQuery from '@graphql/Services.gql';
import { processServicesForTopology } from '@root/state/selectors'; import { processServicesForTopology } from '@root/state/selectors';
import { toggleServicesQuickActions } from '@root/state/actions'; import { toggleServicesQuickActions } from '@root/state/actions';
import { withNotFound, GqlPaths } from '@containers/navigation';
import { LayoutContainer } from '@components/layout'; import { LayoutContainer } from '@components/layout';
import { Loader, ErrorMessage } from '@components/messaging'; import { Loader, ErrorMessage } from '@components/messaging';
import { ServicesQuickActions } from '@components/services';
import { Topology } from 'joyent-ui-toolkit'; import { Topology } from 'joyent-ui-toolkit';
import { withNotFound, GqlPaths } from '@containers/navigation';
const StyledBackground = styled.div` const StyledBackground = styled.div`
padding: ${unitcalc(4)}; padding: ${unitcalc(4)};
background-color: ${props => props.theme.whiteActive}; 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() { render() {
const { const {
url, url,
@ -46,12 +47,7 @@ class ServicesTopology extends Component {
services, services,
loading, loading,
error, error,
servicesQuickActions,
toggleServicesQuickActions, toggleServicesQuickActions,
restartServices,
stopServices,
startServices,
location
} = this.props; } = this.props;
if (loading && !forceArray(services).length) { if (loading && !forceArray(services).length) {
@ -86,42 +82,17 @@ class ServicesTopology extends Component {
} }
const handleQuickActionsClick = (evt, tooltipData) => { const handleQuickActionsClick = (evt, tooltipData) => {
toggleServicesQuickActions(tooltipData); const container = this._refs.container;
}; const containerRect = container.getBoundingClientRect();
const position = {
const handleTooltipBlur = evt => { top: `${containerRect.top + window.scrollY + tooltipData.position.top}px`,
toggleServicesQuickActions({ show: false }); left: `${containerRect.left + window.scrollX + tooltipData.position.left}px`
}; }
const data = {
const handleRestartClick = (evt, service) => { ...tooltipData,
this.setState({ errors: {} }); position
restartServices(service.id).catch(err => { }
this.setState({ errors: { restart: err } }); toggleServicesQuickActions(data);
});
};
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 handleNodeTitleClick = (evt, { service }) => { const handleNodeTitleClick = (evt, { service }) => {
@ -155,22 +126,13 @@ class ServicesTopology extends Component {
{renderedError} {renderedError}
<StyledBackground> <StyledBackground>
<StyledContainer> <StyledContainer>
<Topology <div ref={this.ref('container')}>
services={services} <Topology
onQuickActionsClick={handleQuickActionsClick} services={services}
onNodeTitleClick={handleNodeTitleClick} onQuickActionsClick={handleQuickActionsClick}
/> onNodeTitleClick={handleNodeTitleClick}
<ServicesQuickActions />
service={servicesQuickActions.service} </div>
show={servicesQuickActions.show}
position={servicesQuickActions.position}
onBlur={handleTooltipBlur}
onRestartClick={handleRestartClick}
onStopClick={handleStopClick}
onStartClick={handleStartClick}
onScaleClick={handleScaleClick}
onDeleteClick={handleDeleteClick}
/>
</StyledContainer> </StyledContainer>
</StyledBackground> </StyledBackground>
</div> </div>
@ -179,7 +141,6 @@ class ServicesTopology extends Component {
} }
const mapStateToProps = (state, ownProps) => ({ const mapStateToProps = (state, ownProps) => ({
servicesQuickActions: state.ui.services.quickActions,
url: ownProps.match.url.replace(/\/$/, ''), url: ownProps.match.url.replace(/\/$/, ''),
push: ownProps.history.push 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( const ServicesTopologyWithData = compose(
ServicesRestartGql,
ServicesStopGql,
ServicesStartGql,
ServicesGql, ServicesGql,
UiConnect, UiConnect,
withNotFound([ GqlPaths.DEPLOYMENT_GROUP ]) withNotFound([ GqlPaths.DEPLOYMENT_GROUP ])

View File

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

View File

@ -8,3 +8,7 @@ const APP = constantCase(process.env.APP_NAME);
export const toggleServicesQuickActions = createAction( export const toggleServicesQuickActions = createAction(
`${APP}/TOGGLE_SERVICES_QUICK_ACTIONS` `${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 { 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( export default handleActions(
{ {
[toggleServicesQuickActions.toString()]: (state, action) => { [toggleServicesQuickActions.toString()]: _toggleServicesQuickActions,
const { position, service, show } = action.payload; [toggleInstancesTooltip.toString()]: _toggleInstancesTooltip
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
}
};
}
}, },
{} {}
); );

View File

@ -61,7 +61,6 @@ const activeInstanceStatuses = [
'READY', 'READY',
'ACTIVE', 'ACTIVE',
'RUNNING', 'RUNNING',
'STOPPING',
'INCOMPLETE' 'INCOMPLETE'
]; ];
@ -103,8 +102,12 @@ const getInstancesActive = instanceStatuses => {
const getInstancesHealthy = instances => { const getInstancesHealthy = instances => {
return instances.reduce( return instances.reduce(
(healthy, instance) => (instance.healthy === 'HEALTHY' ? healthy : false), (healthy, instance) => ({
true 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: { quickActions: {
show: false show: false
} }
},
instances: {
tooltip: {
show: false
}
} }
} }
}; };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,9 +5,13 @@ import remcalc from 'remcalc';
import Label from '../label'; import Label from '../label';
const StyledLabel = Label.extend` const StyledLabel = Label.extend`
display: inline-block;
${props => (props.color === 'light' ? `color: ${props.theme.white};` : '')}; ${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)}; margin-left: ${props => (props.iconPosition === 'left' ? remcalc(24) : 0)};
&::first-letter {
text-transform: capitalize;
}
`; `;
const StyledIconContainer = styled.div` const StyledIconContainer = styled.div`
@ -15,7 +19,7 @@ const StyledIconContainer = styled.div`
> svg { > svg {
${props => (props.color === 'light' ? `fill: ${props.theme.white};` : '')}; ${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 { Nav } from 'normalized-styled-components';
import Baseline from '../baseline'; import Baseline from '../baseline';
import remcalc from 'remcalc'; import remcalc from 'remcalc';
import is from 'styled-is'; import is, { isNot } from 'styled-is';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Button from '../button'; import Button from '../button';
import React from 'react'; import React from 'react';
@ -17,7 +17,7 @@ const StyledNav = Nav.extend`
border-left-color: ${props => props.theme.primaryDesaturatedActive}; border-left-color: ${props => props.theme.primaryDesaturatedActive};
`}; `};
${is('disabled')` ${isNot('active')`
border-left-color: ${props => props.theme.grey}; border-left-color: ${props => props.theme.grey};
`}; `};
`; `;
@ -54,6 +54,19 @@ const StyledButton = Button.extend`
&:active:focus { &:active:focus {
border-width: 0; 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` const StyledContainer = styled.div`
@ -73,8 +86,8 @@ const StyledCircle = styled.div`
background-color: ${props => props.theme.secondary}; background-color: ${props => props.theme.secondary};
`}; `};
${is('disabled')` ${isNot('active')`
background-color: ${props => props.theme.grey}; background-color: ${props => props.theme.text};
`}; `};
`; `;
@ -82,20 +95,20 @@ const Options = ({ children, ...rest }) => {
const render = ({ const render = ({
fromHeader = false, fromHeader = false,
collapsed = false, collapsed = false,
disabled = false active = true
}) => }) =>
<StyledNav disabled={disabled} fromHeader={fromHeader} name="card-options"> <StyledNav active={active} fromHeader={fromHeader} name="card-options">
<StyledButton <StyledButton
secondary={!fromHeader} secondary={!fromHeader}
collapsed={collapsed} collapsed={collapsed}
disabled={disabled} active={active}
rect rect
{...rest} {...rest}
> >
<StyledContainer> <StyledContainer>
<StyledCircle disabled={disabled} secondary={!fromHeader} /> <StyledCircle active={active} secondary={!fromHeader} />
<StyledCircle disabled={disabled} secondary={!fromHeader} /> <StyledCircle active={active} secondary={!fromHeader} />
<StyledCircle disabled={disabled} secondary={!fromHeader} /> <StyledCircle active={active} secondary={!fromHeader} />
</StyledContainer> </StyledContainer>
</StyledButton> </StyledButton>
</StyledNav>; </StyledNav>;

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,13 @@
import Baseline from '../baseline'; import Baseline from '../baseline';
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
import React from 'react'; import React from 'react';
import styled from 'styled-components';
import HealthyIcon from './svg/icon_healthy.svg'; 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> <title>icon: state</title>
<desc>Created with Sketch.</desc> <desc>Created with Sketch.</desc>
<defs></defs> <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="topology" transform="translate(-489.000000, -441.000000)">
<g id="services" transform="translate(0.000000, 348.000000)"> <g id="services" transform="translate(0.000000, 348.000000)">
<g id="service:-nginx" transform="translate(476.000000, 36.000000)"> <g id="service:-nginx" transform="translate(476.000000, 36.000000)">
<g id="icon:-state" transform="translate(13.000000, 57.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> <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>
</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 CloseButton } from './close-button';
export { default as Divider } from './divider'; export { default as Divider } from './divider';
export { default as IconButton } from './icon-button'; 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 { Dropdown } from './dropdown';
export { default as StatusLoader } from './status-loader'; export { default as StatusLoader } from './status-loader';
export { default as Message } from './message'; export { default as Message } from './message';

View File

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

View File

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