diff --git a/frontend/src/components/header/index.js b/frontend/src/components/header/index.js index 2909b8ad..4af09fa0 100644 --- a/frontend/src/components/header/index.js +++ b/frontend/src/components/header/index.js @@ -8,7 +8,7 @@ import { remcalc } from '@ui/shared/functions'; import Logo from '../../resources/logo.svg'; import PropTypes from '@root/prop-types'; import Row from '@ui/components/row'; -import Tooltip from '@ui/components/tooltip'; +import Tooltip, { TooltipButton } from '@ui/components/tooltip'; import { pseudoEl, typography } from '@ui/shared/composers'; import { colors } from '@ui/shared/constants'; @@ -46,12 +46,6 @@ const StyledAvatarWrapper = styled.div` } `; -const StyledTooltipWrapper = styled.div` - right: ${remcalc(-18)}; - bottom: ${remcalc(-140)}; - position: absolute; -`; - const StyledName = styled.span` color: ${colors.base.secondaryDark}; font-size: ${remcalc(16)}; @@ -76,7 +70,7 @@ const StyledAvatar = styled(Avatar)` const arrowPosition = { bottom: '100%', - right: '10%' + right: 18 }; const Header = ({ @@ -96,19 +90,21 @@ const Header = ({ }; const tooltipComponent = !tooltip ? null : ( - - -
  • - My Account -
  • -
  • - Settings -
  • -
  • - About -
  • -
    -
    + +
  • + My Account +
  • +
  • + Settings +
  • +
  • + About +
  • +
    ); return ( diff --git a/frontend/src/components/service/item.js b/frontend/src/components/service/item.js index 859f1a6f..3a8beae2 100644 --- a/frontend/src/components/service/item.js +++ b/frontend/src/components/service/item.js @@ -30,6 +30,7 @@ const TitleInnerContainer = styled.div` `; const ServiceItem = ({ + onQuickActions=() => {}, org = '', project = '', service = {} @@ -72,6 +73,10 @@ const ServiceItem = ({ Flags ); + const onOptionsClick = (evt) => { + onQuickActions(evt, service.uuid); + }; + const header = isChild ? null : ( @@ -79,7 +84,9 @@ const ServiceItem = ({ {subtitle} {description} - + + … + ); @@ -113,6 +120,7 @@ const ServiceItem = ({ }; ServiceItem.propTypes = { + onQuickActions: React.PropTypes.func, org: React.PropTypes.string, project: React.PropTypes.string, service: PropTypes.service diff --git a/frontend/src/components/services/tooltip.js b/frontend/src/components/services/tooltip.js new file mode 100644 index 00000000..25d7facd --- /dev/null +++ b/frontend/src/components/services/tooltip.js @@ -0,0 +1,41 @@ +import React from 'react'; +import Tooltip, { TooltipButton, TooltipDivider } from '@ui/components/tooltip'; + +const ServicesTooltip = ({ + show, + position +}) => { + return show ? ( + +
  • + Scale +
  • +
  • + Rollback +
  • +
  • + Reprovision +
  • +
  • + Transfer +
  • +
  • + Setup metrics +
  • + +
  • + Stop +
  • +
  • + Delete +
  • +
    + ) : null; +}; + +ServicesTooltip.propTypes = { + position: React.PropTypes.object, + show: React.PropTypes.bool +}; + +export default ServicesTooltip; diff --git a/frontend/src/containers/services/list.js b/frontend/src/containers/services/list.js index 84beab99..89afc75b 100644 --- a/frontend/src/containers/services/list.js +++ b/frontend/src/containers/services/list.js @@ -1,44 +1,93 @@ import React from 'react'; +import styled from 'styled-components'; import { connect } from 'react-redux'; import PropTypes from '@root/prop-types'; import ServiceItem from '@components/service/item'; import UnmanagedInstances from '@components/services/unmanaged-instances'; +import { toggleTooltip } from '@state/actions'; +import ServicesTooltip from '@components/services/tooltip'; import { orgByIdSelector, projectByIdSelector, - servicesByProjectIdSelector + servicesByProjectIdSelector, + serviceUiTooltipSelector } from '@state/selectors'; -const Services = (props) => { - const { - org = {}, - project = {}, - services = [] - } = props; +const StyledContainer = styled.div` + position: relative; +`; - const instances = 5; - const serviceList = services.map((service) => ( - - )); +class Services extends React.Component { - return ( -
    - { serviceList } - { instances && } -
    - ); -}; + ref(name) { + this._refs = this._refs || {}; + + return (el) => { + this._refs[name] = el; + }; + } + + render() { + const { + org = {}, + project = {}, + services = [], + toggleTooltip = (() => {}), + uiTooltip = {} + } = this.props; + + const onQuickActions = (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 + }; + + toggleTooltip({ + service: service, + position: position + }); + }; + + const instances = 5; + + const serviceList = services.map((service) => ( + + )); + + return ( +
    + { instances && } + +
    + {serviceList} + +
    +
    +
    + ); + } +} Services.propTypes = { org: PropTypes.org, project: PropTypes.project, - services: React.PropTypes.arrayOf(PropTypes.service) + services: React.PropTypes.arrayOf(PropTypes.service), + toggleTooltip: React.PropTypes.func, + uiTooltip: React.PropTypes.object }; const mapStateToProps = (state, { @@ -49,9 +98,15 @@ const mapStateToProps = (state, { }) => ({ org: orgByIdSelector(match.params.org)(state), project: projectByIdSelector(match.params.projectId)(state), - services: servicesByProjectIdSelector(match.params.projectId)(state) + services: servicesByProjectIdSelector(match.params.projectId)(state), + uiTooltip: serviceUiTooltipSelector(state) +}); + +const mapDispatchToProps = (dispatch) => ({ + toggleTooltip: (data) => dispatch(toggleTooltip(data)) }); export default connect( - mapStateToProps + mapStateToProps, + mapDispatchToProps )(Services); diff --git a/frontend/src/containers/services/topology.js b/frontend/src/containers/services/topology.js index e00a5691..a4bd6fc9 100644 --- a/frontend/src/containers/services/topology.js +++ b/frontend/src/containers/services/topology.js @@ -1,26 +1,49 @@ import React from 'react'; import { connect } from 'react-redux'; +import styled from 'styled-components'; import PropTypes from '@root/prop-types'; import { TopologyGraph } from '@ui/components/topology'; +import ServicesTooltip from '@components/services/tooltip'; + +import { toggleTooltip } from '@state/actions'; import { orgByIdSelector, projectByIdSelector, - servicesForTopologySelector + servicesForTopologySelector, + serviceUiTooltipSelector } from '@state/selectors'; +const StyledContainer = styled.div` + position: relative; +`; + const Services = (props) => { const { - services = [] + services = [], + toggleTooltip, + uiTooltip } = props; + const onQuickActions = (evt, tooltipData) => { + toggleTooltip(tooltipData); + }; + return ( - + + + + ); }; Services.propTypes = { - services: React.PropTypes.arrayOf(PropTypes.service) + services: React.PropTypes.arrayOf(PropTypes.service), + toggleTooltip: React.PropTypes.func, + uiTooltip: React.PropTypes.object }; const mapStateToProps = (state, { @@ -30,9 +53,15 @@ const mapStateToProps = (state, { }) => ({ org: orgByIdSelector(match.params.org)(state), project: projectByIdSelector(match.params.projectId)(state), - services: servicesForTopologySelector(match.params.projectId)(state) + services: servicesForTopologySelector(match.params.projectId)(state), + uiTooltip: serviceUiTooltipSelector(state) +}); + +const mapDispatchToProps = (dispatch) => ({ + toggleTooltip: (data) => dispatch(toggleTooltip(data)) }); export default connect( - mapStateToProps + mapStateToProps, + mapDispatchToProps )(Services); diff --git a/frontend/src/mock-state.json b/frontend/src/mock-state.json index 77766bfc..7d3c686a 100644 --- a/frontend/src/mock-state.json +++ b/frontend/src/mock-state.json @@ -627,7 +627,10 @@ "activity-feed", "service-manifest", "firewall" - ] + ], + "tooltip": { + "show": false + } }, "data": [{ "uuid": "081a792c-47e0-4439-924b-2efa9788ae9e", diff --git a/frontend/src/state/actions.js b/frontend/src/state/actions.js index 874b29d5..7091eab6 100644 --- a/frontend/src/state/actions.js +++ b/frontend/src/state/actions.js @@ -52,3 +52,5 @@ export const switchMonitorViewPage = createAction(`${APP}/SWITCH_MONITOR_VIEW_PAGE`); export const handleNewProject = createAction(`${APP}/CREATE_NEW_PROJECT`); +export const toggleTooltip = + createAction(`${APP}/TOGGLE_QUICK_ACTIONS_TOOLTIP`); diff --git a/frontend/src/state/reducers/services.js b/frontend/src/state/reducers/services.js index d266d7af..0ed692d1 100644 --- a/frontend/src/state/reducers/services.js +++ b/frontend/src/state/reducers/services.js @@ -1,5 +1,9 @@ import { handleActions } from 'redux-actions'; -import { addMetric, toggleServiceCollapsed } from '@state/actions'; +import { + addMetric, + toggleServiceCollapsed, + toggleTooltip +} from '@state/actions'; import { toggleCollapsed } from '@state/reducers/common'; const getMetrics = (stateMetrics, addMetric, metric) => { @@ -42,5 +46,31 @@ export default handleActions({ action.payload.service, action.payload.metric ) - }) + }), + [toggleTooltip.toString()]: (state, action) => { + + const { + position, + service + } = action.payload; + + const show = state.ui.tooltip.service !== service; + const tooltip = show ? { + show: true, + position: { + ...position + }, + service: service + } : { + show: false + }; + + return { + ...state, + ui: { + ...state.ui, + tooltip: tooltip + } + }; + } }, {}); diff --git a/frontend/src/state/selectors.js b/frontend/src/state/selectors.js index ca7eeb64..b8bd6370 100644 --- a/frontend/src/state/selectors.js +++ b/frontend/src/state/selectors.js @@ -7,6 +7,7 @@ const account = (state) => get(state, 'account.data', {}); const accountUi = (state) => get(state, 'account.ui', {}); const orgUiSections = (state) => get(state, 'orgs.ui.sections', []); const projectUiSections = (state) => get(state, 'projects.ui.sections', []); +const serviceUiTooltip = (state) => get(state, 'services.ui.tooltip', []); const serviceUiSections = (state) => get(state, 'services.ui.sections', []); const orgs = (state) => get(state, 'orgs.data', []); const orgUI = (state) => get(state, 'orgs.ui', []); @@ -249,5 +250,6 @@ export { members as membersSelector, peopleByProjectId as peopleByProjectIdSelector, projectsUI as projectUISelector, - projectIndexById as projectIndexByIdSelect + projectIndexById as projectIndexByIdSelect, + serviceUiTooltip as serviceUiTooltipSelector }; diff --git a/ui/src/components/tooltip/button.js b/ui/src/components/tooltip/button.js new file mode 100644 index 00000000..59ca5452 --- /dev/null +++ b/ui/src/components/tooltip/button.js @@ -0,0 +1,36 @@ +import styled from 'styled-components'; +import { colors } from '../../shared/constants'; +import { unitcalc } from '../../shared/functions'; +import Button from '../button'; + +const TooltipButton = styled(Button)` + width: 100%; + padding: ${unitcalc(1)} ${unitcalc(3)}; + background-color: ${colors.base.white}; + color: ${colors.base.secondary}; + text-align: left; + border: none; + box-shadow: none; + + &:focus { + background-color: ${colors.base.white}; + color: ${colors.base.primary}; + border: none; + } + + &:hover { + background-color: ${colors.base.white}; + color: ${colors.base.primary}; + border: none; + } + + &:active, + &:active:hover, + &:active:focus { + background-color: ${colors.base.white}; + color: ${colors.base.primary}; + border: none; + } +`; + +export default TooltipButton; diff --git a/ui/src/components/tooltip/index.js b/ui/src/components/tooltip/index.js index a0575bad..e3c0f4b4 100644 --- a/ui/src/components/tooltip/index.js +++ b/ui/src/components/tooltip/index.js @@ -1,42 +1,60 @@ -import { remcalc } from '../../shared/functions'; +import { remcalc, unitcalc } from '../../shared/functions'; import { + absolutePosition, baseBox, pseudoEl, Baseline, - moveZ + moveZ, + getMeasurement } from '../../shared/composers'; -import { colors } from '../../shared/constants'; +import { boxes, colors, tooltipShadow } from '../../shared/constants'; import styled from 'styled-components'; import React from 'react'; const ItemPadder = 9; const WrapperPadder = 24; +const StyledContainer = styled.div` + ${(props) => absolutePosition(props)} +`; + const StyledList = styled.ul` background: ${colors.base.white}; + box-sizing: border-box; color: ${colors.base.text}; display: inline-block; font-family: sans-serif; list-style-type: none; margin: 0; - padding: 0; - min-width: ${remcalc(200)}; + padding: ${unitcalc(2)} 0; + /*min-width: ${remcalc(200)};*/ + + position: absolute; + top: 4px; + ${(props) => { + return props.arrowPosition.left ? + `left: -${getMeasurement(props.arrowPosition.left)}` : + props.arrowPosition.right ? + `right: -${getMeasurement(props.arrowPosition.right)}` : null; + }}; ${props => props.styles} - ${baseBox()} - + ${baseBox({ + shadow: tooltipShadow + })} + ${moveZ({ amount: 1 })} - & > * { + /*& > * { padding: ${remcalc(ItemPadder)} ${remcalc(WrapperPadder)}; &:hover { background: ${colors.base.grey}; } - } + }*/ &:after, &:before { border: solid transparent; @@ -49,14 +67,14 @@ const StyledList = styled.ul` &:after { border-color: rgba(255, 255, 255, 0); border-bottom-color: ${colors.base.white}; - border-width: ${remcalc(10)}; - margin-left: ${remcalc(-10)}; + border-width: ${remcalc(3)}; + margin-left: ${remcalc(-3)}; } &:before { border-color: rgba(216, 216, 216, 0); - border-bottom-color: ${colors.base.greyDark}; - border-width: ${remcalc(12)}; - margin-left: ${remcalc(-12)}; + border-bottom-color: ${colors.base.grey}; + border-width: ${remcalc(5)}; + margin-left: ${remcalc(-5)}; } `; @@ -64,13 +82,15 @@ const Tooltip = ({ children, arrowPosition = { bottom: '100%', - left: '10%' + left: '50%' }, ...props }) => ( - - {children} - + + + {children} + + ); Tooltip.propTypes = { @@ -81,3 +101,10 @@ Tooltip.propTypes = { export default Baseline( Tooltip ); + +export { default as TooltipButton } from './button'; + +export const TooltipDivider = styled.div` + border-top: ${boxes.border.unchecked}; + margin: ${unitcalc(1)} 0 ${unitcalc(1.5)} 0; +`; diff --git a/ui/src/components/topology/graph-node/index.js b/ui/src/components/topology/graph-node/index.js index 3b67d04e..eabaa865 100644 --- a/ui/src/components/topology/graph-node/index.js +++ b/ui/src/components/topology/graph-node/index.js @@ -13,7 +13,8 @@ const GraphNode = ({ connected, data, index, - onDragStart + onDragStart, + onQuickActions }) => { const { @@ -38,7 +39,26 @@ const GraphNode = ({ } const onButtonClick = (evt) => { - // console.log('Rect clicked!!!'); + + const tooltipPosition = { + x: data.x + Constants.buttonRect.x + Constants.buttonRect.width/2, + y: data.y + Constants.buttonRect.y + Constants.buttonRect.height + }; + + if ( connected ) { + tooltipPosition.x = tooltipPosition.x + left; + tooltipPosition.y = tooltipPosition.y + top; + } + + const d = { + service: data.uuid, + position: { + left: tooltipPosition.x, + top: tooltipPosition.y + } + }; + + onQuickActions(evt, d); }; const onStart = (evt) => { @@ -102,7 +122,8 @@ GraphNode.propTypes = { connected: React.PropTypes.bool, data: React.PropTypes.object.isRequired, index: React.PropTypes.number.isRequired, - onDragStart: React.PropTypes.func + onDragStart: React.PropTypes.func, + onQuickActions: React.PropTypes.func }; export default Baseline( diff --git a/ui/src/components/topology/topology-graph.js b/ui/src/components/topology/topology-graph.js index d5c11172..1aefd3d2 100644 --- a/ui/src/components/topology/topology-graph.js +++ b/ui/src/components/topology/topology-graph.js @@ -88,7 +88,7 @@ class TopologyGraph extends React.Component { const n = Math.ceil( Math.log( nextSimulation.alphaMin()) / Math.log( - 1 - nextSimulation.alphaDecay())) - 200; + 1 - nextSimulation.alphaDecay())); for (var i = 0; i < n; ++i) { nextSimulation.tick(); } @@ -104,7 +104,10 @@ class TopologyGraph extends React.Component { render() { - const services = this.props.services; + const { + onQuickActions, + services + } = this.props; const { nodes, @@ -218,6 +221,7 @@ class TopologyGraph extends React.Component { data={n} index={index} onDragStart={onDragStart} + onQuickActions={onQuickActions} connected={n.id !== 'consul'} /> )); @@ -250,6 +254,7 @@ class TopologyGraph extends React.Component { } TopologyGraph.propTypes = { + onQuickActions: React.PropTypes.func, services: React.PropTypes.array }; diff --git a/ui/src/shared/composers/index.js b/ui/src/shared/composers/index.js index 6f7f5bd7..64708119 100644 --- a/ui/src/shared/composers/index.js +++ b/ui/src/shared/composers/index.js @@ -1,5 +1,6 @@ import styled, { css } from 'styled-components'; import camelCase from 'camel-case'; +import isString from 'lodash.isstring'; import { boxes, colors } from '../constants'; import { unitcalc, remcalc } from '../functions'; @@ -82,15 +83,25 @@ export const baseBox = ({ box-shadow: ${shadow}; `; +export const getMeasurement = (measurement) => + isString(measurement) ? measurement : + !isNaN(measurement) ? `${measurement}px`: 'auto'; + +export const absolutePosition = ( + positions = {} +) => css` + position: absolute; + top: ${getMeasurement(positions.top)}; + right: ${getMeasurement(positions.right)}; + bottom: ${getMeasurement(positions.bottom)}; + left: ${getMeasurement(positions.left)}; +`; + export const pseudoEl = ( positions = {} ) => css` content: ""; - position: absolute; - top: ${positions.top || 'auto'}; - right: ${positions.right || 'auto'}; - bottom: ${positions.bottom || 'auto'}; - left: ${positions.left || 'auto'}; + ${absolutePosition(positions)}; `; export const clearfix = css` diff --git a/ui/src/shared/constants/boxes.js b/ui/src/shared/constants/boxes.js index d4f80bc7..3d97c578 100644 --- a/ui/src/shared/constants/boxes.js +++ b/ui/src/shared/constants/boxes.js @@ -5,6 +5,8 @@ export const borderRadius = remcalc(4); export const bottomShaddow = `0 ${remcalc(2)} 0 0 rgba(0, 0, 0, 0.05)`; export const bottomShaddowDarker = `0 ${remcalc(2)} 0 0 rgba(0, 0, 0, 0.1)`; export const insetShaddow = `inset 0 ${remcalc(3)} 0 0 rgba(0, 0, 0, 0.05)`; +export const tooltipShadow = + `0 ${remcalc(2)} ${remcalc(6)} ${remcalc(1)} rgba(0, 0, 0, 0.1)`; export const border = { checked: `${remcalc(1)} solid ${base.primary}`,