feat(joyent-ui-toolkit,joyent-cp-frontend): Quick actions show and hide

This commit is contained in:
JUDIT GRESKOVITS 2017-06-01 10:28:59 +01:00 committed by Sérgio Ramos
parent c3b0dac605
commit cab7411454
25 changed files with 139757 additions and 101926 deletions

View File

@ -36,6 +36,7 @@
"react-styled-flexboxgrid": "^1.1.2", "react-styled-flexboxgrid": "^1.1.2",
"redux": "^3.6.0", "redux": "^3.6.0",
"redux-actions": "^2.0.3", "redux-actions": "^2.0.3",
"redux-batched-actions": "^0.2.0",
"redux-form": "^6.7.0", "redux-form": "^6.7.0",
"remcalc": "^1.0.5", "remcalc": "^1.0.5",
"reselect": "^3.0.1", "reselect": "^3.0.1",

View File

@ -1,3 +1,3 @@
export { default as EmptyServices } from './empty'; export { default as EmptyServices } from './empty';
export { default as ServiceListItem } from './list-item'; export { default as ServiceListItem } from './list-item';
export { default as ServicesTooltip } from './tooltip'; export { default as ServicesQuickActions } from './quick-actions';

View File

@ -21,6 +21,12 @@ import {
// InstancesMultipleIcon // InstancesMultipleIcon
} from 'joyent-ui-toolkit'; } from 'joyent-ui-toolkit';
import { ServicesQuickActions } from '@components/services';
const StyledCardHeader = styled(CardHeader)`
position: relative;
`;
const TitleInnerContainer = styled.div` const TitleInnerContainer = styled.div`
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -29,7 +35,9 @@ const TitleInnerContainer = styled.div`
`; `;
const ServiceListItem = ({ const ServiceListItem = ({
// OnQuickActions=() => {}, showQuickActions,
onQuickActionsClick = () => {},
onQuickActionsBlur = () => {},
deploymentGroup = '', deploymentGroup = '',
service = {} service = {}
}) => { }) => {
@ -61,13 +69,17 @@ const ServiceListItem = ({
const subtitle = <CardSubTitle>{service.instances} instances</CardSubTitle>; const subtitle = <CardSubTitle>{service.instances} instances</CardSubTitle>;
const onOptionsClick = evt => { const handleCardOptionsClick = evt => {
// OnQuickActions(evt, service.uuid); onQuickActionsClick({ service });
};
const handleQuickActionsBlur = evt => {
onQuickActionsBlur({ show: false });
}; };
const header = isChild const header = isChild
? null ? null
: <CardHeader> : <StyledCardHeader>
<CardMeta> <CardMeta>
{title} {title}
<CardDescription> <CardDescription>
@ -82,8 +94,14 @@ const ServiceListItem = ({
/> */} /> */}
</CardDescription> </CardDescription>
</CardMeta> </CardMeta>
<CardOptions onClick={onOptionsClick} /> <CardOptions onClick={handleCardOptionsClick} />
</CardHeader>; <ServicesQuickActions
position={{ top: '47px', right: '-98px' }}
service={service}
show={showQuickActions}
onBlur={handleQuickActionsBlur}
/>
</StyledCardHeader>;
const view = children const view = children
? <CardGroupView> ? <CardGroupView>
@ -117,7 +135,9 @@ const ServiceListItem = ({
}; };
ServiceListItem.propTypes = { ServiceListItem.propTypes = {
// OnQuickActions: PropTypes.func, showQuickActions: PropTypes.bool,
onQuickActionsClick: PropTypes.func,
onQuickActionsBlur: PropTypes.func,
deploymentGroup: PropTypes.string, deploymentGroup: PropTypes.string,
service: PropTypes.object.isRequired // Define better service: PropTypes.object.isRequired // Define better
}; };

View File

@ -0,0 +1,37 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Tooltip, TooltipButton, TooltipDivider } from 'joyent-ui-toolkit';
const ServicesQuickActions = ({ show, position, service, onBlur }) => {
if (!show) {
return null;
}
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;
}, {});
return (
<Tooltip {...p} onBlur={onBlur}>
<TooltipButton onClick={() => {}}>Scale</TooltipButton>
<TooltipButton>Restart</TooltipButton>
<TooltipButton>Stop</TooltipButton>
<TooltipDivider />
<TooltipButton>Delete</TooltipButton>
</Tooltip>
);
};
ServicesQuickActions.propTypes = {
service: PropTypes.object,
position: PropTypes.object,
show: PropTypes.bool,
onBlur: PropTypes.func
};
export default ServicesQuickActions;

View File

@ -1,41 +0,0 @@
// Import React from 'react';
import PropTypes from 'prop-types';
// Import Tooltip, { TooltipButton, TooltipDivider } from 'joyent-ui-toolkit';
const ServicesTooltip = ({ show, position, data, ...rest }) => {
if (!show) {
return null;
}
return null;
// Return (
// <Tooltip {...position} {...rest}>
// <li>
// <TooltipButton>Scale</TooltipButton>
// </li>
// <li>
// <TooltipButton>Start</TooltipButton>
// </li>
// <li>
// <TooltipButton>Restart</TooltipButton>
// </li>
// <TooltipDivider />
// <li>
// <TooltipButton>Stop</TooltipButton>
// </li>
// <li>
// <TooltipButton>Delete</TooltipButton>
// </li>
// </Tooltip>
// );
};
ServicesTooltip.propTypes = {
data: PropTypes.object,
position: PropTypes.object,
show: PropTypes.bool
};
export default ServicesTooltip;

View File

@ -1,11 +1,11 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { compose, graphql } from 'react-apollo'; 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 { Link } from 'react-router-dom';
import ServicesQuery from '@graphql/Services.gql'; import ServicesQuery from '@graphql/Services.gql';
import { processServices } from '@root/state/selectors'; import { processServices } from '@root/state/selectors';
import { toggleServicesQuickActions } from '@root/state/actions';
import { LayoutContainer } from '@components/layout'; import { LayoutContainer } from '@components/layout';
import { Loader, ErrorMessage } from '@components/messaging'; import { Loader, ErrorMessage } from '@components/messaging';
@ -17,7 +17,14 @@ const StyledContainer = styled.div`
class ServiceList extends Component { class ServiceList extends Component {
render() { render() {
const { deploymentGroup, services, loading, error } = this.props; const {
deploymentGroup,
services,
loading,
error,
servicesQuickActions,
toggleServicesQuickActions
} = this.props;
if (loading) { if (loading) {
return ( return (
@ -33,13 +40,25 @@ class ServiceList extends Component {
); );
} }
const handleQuickActionsClick = o => {
toggleServicesQuickActions(o);
};
const handleQuickActionsBlur = o => {
toggleServicesQuickActions(o);
};
const serviceList = services.map(service => ( const serviceList = services.map(service => (
<ServiceListItem <ServiceListItem
key={service.uuid} key={service.uuid}
onQuickActions={null /* onQuickActions */}
deploymentGroup={deploymentGroup.slug} deploymentGroup={deploymentGroup.slug}
service={service} service={service}
uiTooltip={null /* uiTooltip */} showQuickActions={
servicesQuickActions.service &&
servicesQuickActions.service.uuid === service.uuid
}
onQuickActionsClick={handleQuickActionsClick}
onQuickActionsBlur={handleQuickActionsBlur}
/> />
)); ));
@ -57,6 +76,16 @@ class ServiceList extends Component {
} }
} }
const mapStateToProps = (state, ownProps) => ({
servicesQuickActions: state.ui.services.quickActions
});
const mapDispatchToProps = dispatch => ({
toggleServicesQuickActions: data => dispatch(toggleServicesQuickActions(data))
});
const UiConnect = connect(mapStateToProps, mapDispatchToProps);
const ServicesGql = graphql(ServicesQuery, { const ServicesGql = graphql(ServicesQuery, {
options(props) { options(props) {
return { return {
@ -75,6 +104,6 @@ const ServicesGql = graphql(ServicesQuery, {
}) })
}); });
const ServiceListWithData = compose(ServicesGql)(ServiceList); const ServiceListWithData = compose(ServicesGql, UiConnect)(ServiceList);
export default ServiceListWithData; export default ServiceListWithData;

View File

@ -1,31 +1,38 @@
import React from 'react'; import React from 'react';
import { compose, graphql } from 'react-apollo'; 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 ServicesQuery from '@graphql/ServicesTopology.gql'; import ServicesQuery from '@graphql/ServicesTopology.gql';
import unitcalc from 'unitcalc'; import unitcalc from 'unitcalc';
import { processServices } from '@root/state/selectors'; import { processServices } from '@root/state/selectors';
import { toggleServicesQuickActions } from '@root/state/actions';
import { LayoutContainer } from '@components/layout'; import { LayoutContainer } from '@components/layout';
import { Loader, ErrorMessage } from '@components/messaging'; import { Loader, ErrorMessage } from '@components/messaging';
// Import { ServicesTooltip } from '@components/services'; import { ServicesQuickActions } from '@components/services';
import { Topology } from 'joyent-ui-toolkit'; import { Topology } from 'joyent-ui-toolkit';
/* Import ServicesTooltip from '@components/services/tooltip';
import { toggleTooltip } from '@state/actions'; */
const StyledBackground = styled.div` const StyledBackground = styled.div`
padding: ${unitcalc(4)};
background-color: ${props => props.theme.whiteActive}; background-color: ${props => props.theme.whiteActive};
`; `;
const StyledContainer = styled.div` const StyledContainer = styled.div`
position: relative; position: relative;
padding: ${unitcalc(4)};
`; `;
const ServicesTopology = ({ push, services, datacenter, loading, error }) => { const ServicesTopology = ({
url,
push,
services,
datacenter,
loading,
error,
servicesQuickActions,
toggleServicesQuickActions
}) => {
if (loading) { if (loading) {
return ( return (
<LayoutContainer> <LayoutContainer>
@ -40,15 +47,48 @@ const ServicesTopology = ({ push, services, datacenter, loading, error }) => {
); );
} }
const handleQuickActionsClick = (evt, tooltipData) => {
toggleServicesQuickActions(tooltipData);
};
const handleTooltipBlur = evt => {
toggleServicesQuickActions({ show: false });
};
const handleNodeTitleClick = (evt, { service }) => {
push(`${url.split('/').slice(0, 3).join('/')}/services/${service.slug}`);
};
return ( return (
<StyledBackground> <StyledBackground>
<StyledContainer> <StyledContainer>
<Topology services={services} /> <Topology
services={services}
onQuickActionsClick={handleQuickActionsClick}
onNodeTitleClick={handleNodeTitleClick}
/>
<ServicesQuickActions
show={servicesQuickActions.show}
position={servicesQuickActions.position}
onBlur={handleTooltipBlur}
/>
</StyledContainer> </StyledContainer>
</StyledBackground> </StyledBackground>
); );
}; };
const mapStateToProps = (state, ownProps) => ({
servicesQuickActions: state.ui.services.quickActions,
url: ownProps.match.url,
push: ownProps.history.push
});
const mapDispatchToProps = dispatch => ({
toggleServicesQuickActions: data => dispatch(toggleServicesQuickActions(data))
});
const UiConnect = connect(mapStateToProps, mapDispatchToProps);
const ServicesGql = graphql(ServicesQuery, { const ServicesGql = graphql(ServicesQuery, {
options(props) { options(props) {
return { return {
@ -66,6 +106,8 @@ const ServicesGql = graphql(ServicesQuery, {
}) })
}); });
const ServicesTopologyWithData = compose(ServicesGql)(ServicesTopology); const ServicesTopologyWithData = compose(ServicesGql, UiConnect)(
ServicesTopology
);
export default ServicesTopologyWithData; export default ServicesTopologyWithData;

View File

@ -5,4 +5,6 @@ const APP = constantCase(process.env.APP_NAME);
/** ***************************** UI ****************************** */ /** ***************************** UI ****************************** */
export const addMemberToProject = createAction(`${APP}/PROJECT_ADD_MEMBER`); export const toggleServicesQuickActions = createAction(
`${APP}/TOGGLE_SERVICES_QUICK_ACTIONS`
);

View File

@ -0,0 +1 @@
export { default as ui } from './ui';

View File

@ -0,0 +1,36 @@
import { handleActions } from 'redux-actions';
import { toggleServicesQuickActions } from '@state/actions';
export default handleActions(
{
[toggleServicesQuickActions.toString()]: (state, action) => {
const { position, service, show } = action.payload;
const s = show === undefined
? !state.services.quickActions.service ||
service.uuid !== state.services.quickActions.service.uuid
: show;
const quickActions = s
? {
show: s,
position,
service
}
: {
show: false
};
return {
...state,
services: {
...state.services,
quickActions
}
};
return state;
}
},
{}
);

View File

@ -21,6 +21,11 @@ const state = {
name: 'Instances' name: 'Instances'
} }
] ]
},
services: {
quickActions: {
show: false
}
} }
} }
}; };

View File

@ -1,6 +1,8 @@
import { createStore, combineReducers, applyMiddleware, compose } from 'redux'; import { createStore, combineReducers, applyMiddleware, compose } from 'redux';
import { enableBatching } from 'redux-batched-actions';
import { ApolloClient, createNetworkInterface } from 'react-apollo'; import { ApolloClient, createNetworkInterface } from 'react-apollo';
import state from './state'; import state from './state';
import { ui } from './reducers';
export const client = new ApolloClient({ export const client = new ApolloClient({
dataIdFromObject: o => { dataIdFromObject: o => {
@ -22,12 +24,10 @@ export const client = new ApolloClient({
export const store = createStore( export const store = createStore(
combineReducers({ combineReducers({
ui: s => { ui,
return s ? s : state.ui;
},
apollo: client.reducer() apollo: client.reducer()
}), }),
{}, // Initial state state, // Initial state
compose( compose(
applyMiddleware(client.middleware()), applyMiddleware(client.middleware()),
// If you are using the devToolsExtension, you can add it here also // If you are using the devToolsExtension, you can add it here also

View File

@ -12,7 +12,8 @@ export { default as Small } from './text/small';
export { default as theme } from './theme'; export { default as theme } from './theme';
export { default as typography, fonts } from './typography'; export { default as typography, fonts } from './typography';
export { default as Topology } from './topology'; export { default as Topology } from './topology';
export { default as Tooltip } from './tooltip';
export { Tooltip, TooltipButton, TooltipDivider } from './tooltip';
export { export {
borderRadius, borderRadius,

View File

@ -0,0 +1,14 @@
import React, { Component } from 'react';
import styled from 'styled-components';
class Modal extends Component {
render() {
return (
<div>
<p>Modal</p>
</div>
);
}
}
export default Modal;

View File

@ -1,79 +1,3 @@
import React from 'react'; export { default as Tooltip } from './tooltip';
import PropTypes from 'prop-types'; export { default as TooltipButton } from './button';
import styled from 'styled-components'; export { default as TooltipDivider } from './divider';
import unitcalc from 'unitcalc';
import remcalc from 'remcalc';
import theme from '../theme';
import { border, borderRadius, tooltipShadow } from '../boxes';
const StyledContainer = styled.div`
position: absolute;
top: ${props => props.top};
left: ${props => props.left};
bottom: ${props => props.bottoms};
right: ${props => props.right};
`;
const StyledList = styled.ul`
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};
drop-shadow: ${tooltipShadow};
border-radius: ${borderRadius};
z-index: 1;
&:after, &:before {
content: "";
position: absolute;
bottom: 100%;
left: 50%;
height: 0;
width: 0;
border: solid transparent;
}
&:after {
border-bottom-color: ${theme.white};
border-width: ${remcalc(3)};
margin-left: ${remcalc(-3)};
}
&:before {
border-bottom-color: ${theme.grey};
border-width: ${remcalc(5)};
margin-left: ${remcalc(-5)};
}
`;
/**
* @example ./usage.md
*/
const Tooltip = ({
children,
top = 'auto',
left = 'auto',
bottom = 'auto',
right = 'auto'
}) => (
<StyledContainer top={top} left={left} bottom={bottom} right={right}>
<StyledList>
{children}
</StyledList>
</StyledContainer>
);
Tooltip.propTypes = {
children: PropTypes.node,
top: PropTypes.number,
left: PropTypes.number,
bottom: PropTypes.number,
right: PropTypes.number
};
export default Tooltip;

View File

@ -0,0 +1,113 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import unitcalc from 'unitcalc';
import remcalc from 'remcalc';
import theme from '../theme';
import { border, borderRadius, tooltipShadow } from '../boxes';
const StyledContainer = styled.div`
position: absolute;
top: ${props => props.top};
left: ${props => props.left};
bottom: ${props => props.bottoms};
right: ${props => props.right};
&:focus {
outline: none;
}
`;
const StyledList = styled.ul`
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};
box-shadow: ${tooltipShadow};
border-radius: ${borderRadius};
z-index: 1;
&:after, &:before {
content: "";
position: absolute;
bottom: 100%;
left: 50%;
height: 0;
width: 0;
border: solid transparent;
}
&:after {
border-bottom-color: ${theme.white};
border-width: ${remcalc(3)};
margin-left: ${remcalc(-3)};
}
&:before {
border-bottom-color: ${theme.grey};
border-width: ${remcalc(5)};
margin-left: ${remcalc(-5)};
}
`;
/**
* @example ./usage.md
*/
class Tooltip extends Component {
componentDidMount() {
this.windowClickHandler = this.handleWindowClick.bind(this);
this.windowClickCounter = 0;
window.addEventListener('click', this.windowClickHandler);
}
componentWillReceiveProps(nextProps) {
this.windowClickCounter = 0;
}
componentWillUnmount() {
window.removeEventListener('click', this.windowClickHandler);
}
handleWindowClick(evt) {
if (this.windowClickCounter) {
if (this.props.onBlur) {
this.props.onBlur();
}
}
this.windowClickCounter++;
}
render() {
const {
children,
top = 'auto',
left = 'auto',
bottom = 'auto',
right = 'auto'
} = this.props;
return (
<StyledContainer top={top} left={left} bottom={bottom} right={right}>
<StyledList>
{children}
</StyledList>
</StyledContainer>
);
}
}
Tooltip.propTypes = {
children: PropTypes.node,
top: PropTypes.string,
left: PropTypes.string,
bottom: PropTypes.string,
right: PropTypes.string,
onBlur: PropTypes.func
};
export default Tooltip;

View File

@ -1,14 +1,14 @@
``` ```
const Tooltip = require('./index').default; const Tooltip = require('./tooltip').default;
const TooltipButton = require('./button').default; const TooltipButton = require('./button').default;
const TooltipDivider = require('./divider').default; const TooltipDivider = require('./divider').default;
<div style={{ position: 'relative', height: '175px' }}> <div style={{ position: 'relative', height: '175px' }}>
<Tooltip top='5px' left='60px'> <Tooltip top='5px' left='55px'>
<TooltipButton>Scale</TooltipButton> <TooltipButton>Scale</TooltipButton>
<TooltipButton>Restart</TooltipButton> <TooltipButton>Restart</TooltipButton>
<TooltipDivider />
<TooltipButton>Stop</TooltipButton> <TooltipButton>Stop</TooltipButton>
<TooltipDivider />
<TooltipButton>Delete</TooltipButton> <TooltipButton>Delete</TooltipButton>
</Tooltip> </Tooltip>
</div> </div>

View File

@ -119,7 +119,7 @@ class Topology extends React.Component {
} }
render() { render() {
const { onQuickActions, services } = this.props; const { onQuickActionsClick, onNodeTitleClick, services } = this.props;
const { nodes, links } = this.state; const { nodes, links } = this.state;
@ -197,17 +197,14 @@ class Topology extends React.Component {
this.setDragInfo(false); this.setDragInfo(false);
}; };
const onTitleClick = serviceUUID =>
this.props.onNodeTitleClick(serviceUUID);
const renderedNode = (n, index) => ( const renderedNode = (n, index) => (
<TopologyNode <TopologyNode
key={index} key={index}
data={n} data={n}
index={index} index={index}
onDragStart={onDragStart} onDragStart={onDragStart}
onNodeTitleClick={onTitleClick} onNodeTitleClick={onNodeTitleClick}
onQuickActions={onQuickActions} onQuickActions={onQuickActionsClick}
connected={n.id !== 'consul'} connected={n.id !== 'consul'}
/> />
); );
@ -288,7 +285,7 @@ class Topology extends React.Component {
} }
Topology.propTypes = { Topology.propTypes = {
onQuickActions: PropTypes.func, onQuickActionsClick: PropTypes.func,
onNodeTitleClick: PropTypes.func, onNodeTitleClick: PropTypes.func,
services: PropTypes.array services: PropTypes.array
}; };

View File

@ -27,6 +27,7 @@ const NodeButton = ({ connected, onButtonClick, index }) => {
return ( return (
<g transform={`translate(${x}, ${y})`}> <g transform={`translate(${x}, ${y})`}>
<GraphLine x1={0} y1={0} x2={0} y2={height} connected={connected} /> <GraphLine x1={0} y1={0} x2={0} y2={height} connected={connected} />
{buttonCircles}
<GraphButtonRect <GraphButtonRect
height={height} height={height}
onClick={onButtonClick} onClick={onButtonClick}
@ -35,7 +36,6 @@ const NodeButton = ({ connected, onButtonClick, index }) => {
role="button" role="button"
tabIndex={100 + index} tabIndex={100 + index}
/> />
{buttonCircles}
</g> </g>
); );
}; };

View File

@ -42,7 +42,7 @@ const GraphNode = ({
} }
const d = { const d = {
service: data.uuid, service: data,
position: { position: {
left: tooltipPosition.x, left: tooltipPosition.x,
top: tooltipPosition.y top: tooltipPosition.y
@ -52,7 +52,7 @@ const GraphNode = ({
onQuickActions(evt, d); onQuickActions(evt, d);
}; };
const onTitleClick = () => onNodeTitleClick(data.uuid); const onTitleClick = evt => onNodeTitleClick(evt, { service: data });
const onStart = evt => { const onStart = evt => {
evt.preventDefault(); evt.preventDefault();

View File

@ -77,6 +77,10 @@ export const GraphText = styled.text`
export const GraphButtonRect = styled.rect` export const GraphButtonRect = styled.rect`
opacity: 0; opacity: 0;
cursor: pointer; cursor: pointer;
&:focus {
outline: none;
}
`; `;
export const GraphButtonCircle = styled.circle` export const GraphButtonCircle = styled.circle`

View File

@ -63,7 +63,7 @@ module.exports = {
'src/list/ul.js', 'src/list/ul.js',
'src/list/li.js', 'src/list/li.js',
'src/topology/index.js', 'src/topology/index.js',
'src/tooltip/index.js' 'src/tooltip/tooltip.js'
] ]
}, },
{ {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -68,6 +68,8 @@ const staged = async () => {
return; return;
} }
console.log('existing = ', existing);
return run(existing); return run(existing);
}; };