diff --git a/package.json b/package.json index bf3bdf616a7..cc2bc19f95b 100644 --- a/package.json +++ b/package.json @@ -149,6 +149,7 @@ "react": "^16.2.0", "react-dom": "^16.2.0", "react-grid-layout": "^0.16.1", + "react-popper": "^0.7.5", "react-sizeme": "^2.3.6", "remarkable": "^1.7.1", "rxjs": "^5.4.3", diff --git a/public/app/containers/AlertRuleList/AlertRuleList.tsx b/public/app/containers/AlertRuleList/AlertRuleList.tsx index b12a9f234d0..02eaa847a2c 100644 --- a/public/app/containers/AlertRuleList/AlertRuleList.tsx +++ b/public/app/containers/AlertRuleList/AlertRuleList.tsx @@ -23,6 +23,11 @@ export class AlertRuleList extends React.Component { this.props.nav.load('alerting', 'alert-list'); this.fetchRules(); + this.handleTooltipPositionChange = this.handleTooltipPositionChange.bind(this); + + this.state = { + tooltipPosition: 'auto', + }; } onStateFilterChanged = evt => { @@ -44,6 +49,12 @@ export class AlertRuleList extends React.Component { }); }; + handleTooltipPositionChange(evt) { + evt.preventDefault(); + this.setState({ + tooltipPosition: evt.target.value, + }); + } render() { const { nav, alertList } = this.props; diff --git a/public/app/core/components/Tooltip/Popover.jest.tsx b/public/app/core/components/Tooltip/Popover.jest.tsx new file mode 100644 index 00000000000..3850521d5e2 --- /dev/null +++ b/public/app/core/components/Tooltip/Popover.jest.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import renderer from 'react-test-renderer'; +import Popover from './Popover'; + +describe('Popover', () => { + it('renders correctly', () => { + const tree = renderer + .create( + + + + ) + .toJSON(); + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/public/app/core/components/Tooltip/Popover.tsx b/public/app/core/components/Tooltip/Popover.tsx new file mode 100644 index 00000000000..4dc25d34130 --- /dev/null +++ b/public/app/core/components/Tooltip/Popover.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import withTooltip from './withTooltip'; +import { Target } from 'react-popper'; + +interface IPopoverProps { + tooltipSetState: (prevState: object) => void; +} + +class Popover extends React.Component { + constructor(props) { + super(props); + this.toggleTooltip = this.toggleTooltip.bind(this); + } + + toggleTooltip() { + const { tooltipSetState } = this.props; + tooltipSetState(prevState => { + return { + ...prevState, + show: !prevState.show, + }; + }); + } + + render() { + return ( + + {this.props.children} + + ); + } +} + +export default withTooltip(Popover); diff --git a/public/app/core/components/Tooltip/Tooltip.jest.tsx b/public/app/core/components/Tooltip/Tooltip.jest.tsx new file mode 100644 index 00000000000..bc0512ecef7 --- /dev/null +++ b/public/app/core/components/Tooltip/Tooltip.jest.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import renderer from 'react-test-renderer'; +import Tooltip from './Tooltip'; + +describe('Tooltip', () => { + it('renders correctly', () => { + const tree = renderer + .create( + + Link with tooltip + + ) + .toJSON(); + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/public/app/core/components/Tooltip/Tooltip.tsx b/public/app/core/components/Tooltip/Tooltip.tsx new file mode 100644 index 00000000000..ae4093ea3f1 --- /dev/null +++ b/public/app/core/components/Tooltip/Tooltip.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import withTooltip from './withTooltip'; +import { Target } from 'react-popper'; + +interface ITooltipProps { + tooltipSetState: (prevState: object) => void; +} + +class Tooltip extends React.Component { + constructor(props) { + super(props); + this.showTooltip = this.showTooltip.bind(this); + this.hideTooltip = this.hideTooltip.bind(this); + } + + showTooltip() { + const { tooltipSetState } = this.props; + tooltipSetState(prevState => { + return { + ...prevState, + show: true, + }; + }); + } + + hideTooltip() { + const { tooltipSetState } = this.props; + tooltipSetState(prevState => { + return { + ...prevState, + show: false, + }; + }); + } + + render() { + return ( + + {this.props.children} + + ); + } +} + +export default withTooltip(Tooltip); diff --git a/public/app/core/components/Tooltip/__snapshots__/Popover.jest.tsx.snap b/public/app/core/components/Tooltip/__snapshots__/Popover.jest.tsx.snap new file mode 100644 index 00000000000..7496c681747 --- /dev/null +++ b/public/app/core/components/Tooltip/__snapshots__/Popover.jest.tsx.snap @@ -0,0 +1,16 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Popover renders correctly 1`] = ` +
+
+ +
+
+`; diff --git a/public/app/core/components/Tooltip/__snapshots__/Tooltip.jest.tsx.snap b/public/app/core/components/Tooltip/__snapshots__/Tooltip.jest.tsx.snap new file mode 100644 index 00000000000..3d3d7ea074b --- /dev/null +++ b/public/app/core/components/Tooltip/__snapshots__/Tooltip.jest.tsx.snap @@ -0,0 +1,19 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Tooltip renders correctly 1`] = ` +
+ +
+`; diff --git a/public/app/core/components/Tooltip/withTooltip.tsx b/public/app/core/components/Tooltip/withTooltip.tsx new file mode 100644 index 00000000000..4b9115fad19 --- /dev/null +++ b/public/app/core/components/Tooltip/withTooltip.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { Manager, Popper, Arrow } from 'react-popper'; + +interface IwithTooltipProps { + placement?: string; + content: string | ((props: any) => JSX.Element); +} + +export default function withTooltip(WrappedComponent) { + return class extends React.Component { + constructor(props) { + super(props); + + this.setState = this.setState.bind(this); + this.state = { + placement: this.props.placement || 'auto', + show: false, + }; + } + + componentWillReceiveProps(nextProps) { + if (nextProps.placement && nextProps.placement !== this.state.placement) { + this.setState(prevState => { + return { + ...prevState, + placement: nextProps.placement, + }; + }); + } + } + + renderContent(content) { + if (typeof content === 'function') { + // If it's a function we assume it's a React component + const ReactComponent = content; + return ; + } + return content; + } + + render() { + const { content } = this.props; + + return ( + + + {this.state.show ? ( + + {this.renderContent(content)} + + + ) : null} + + ); + } + }; +} diff --git a/public/sass/_grafana.scss b/public/sass/_grafana.scss index 3232e9c7f8e..06b3a374e34 100644 --- a/public/sass/_grafana.scss +++ b/public/sass/_grafana.scss @@ -1,104 +1,105 @@ // vendor -@import "../vendor/css/timepicker.css"; -@import "../vendor/css/spectrum.css"; +@import '../vendor/css/timepicker.css'; +@import '../vendor/css/spectrum.css'; // MIXINS -@import "mixins/mixins"; -@import "mixins/animations"; -@import "mixins/buttons"; -@import "mixins/breakpoints"; -@import "mixins/grid"; -@import "mixins/grid-framework"; -@import "mixins/hover"; -@import "mixins/forms"; -@import "mixins/drop_element"; +@import 'mixins/mixins'; +@import 'mixins/animations'; +@import 'mixins/buttons'; +@import 'mixins/breakpoints'; +@import 'mixins/grid'; +@import 'mixins/grid-framework'; +@import 'mixins/hover'; +@import 'mixins/forms'; +@import 'mixins/drop_element'; // BASE -@import "base/normalize"; -@import "base/reboot"; -@import "base/type"; -@import "base/forms"; -@import "base/grid"; -@import "base/fonts"; -@import "base/code"; -@import "base/icons"; +@import 'base/normalize'; +@import 'base/reboot'; +@import 'base/type'; +@import 'base/forms'; +@import 'base/grid'; +@import 'base/fonts'; +@import 'base/code'; +@import 'base/icons'; // UTILS -@import "utils/utils"; -@import "utils/validation"; -@import "utils/angular"; -@import "utils/spacings"; -@import "utils/widths"; +@import 'utils/utils'; +@import 'utils/validation'; +@import 'utils/angular'; +@import 'utils/spacings'; +@import 'utils/widths'; // LAYOUTS -@import "layout/lists"; -@import "layout/page"; +@import 'layout/lists'; +@import 'layout/page'; // COMPONENTS -@import "components/scrollbar"; -@import "components/cards"; -@import "components/buttons"; -@import "components/navs"; -@import "components/tabs"; -@import "components/alerts"; -@import "components/switch"; -@import "components/tooltip"; -@import "components/tags"; -@import "components/panel_graph"; -@import "components/submenu"; -@import "components/panel_alertlist"; -@import "components/panel_dashlist"; -@import "components/panel_gettingstarted"; -@import "components/panel_pluginlist"; -@import "components/panel_singlestat"; -@import "components/panel_table"; -@import "components/panel_text"; -@import "components/panel_heatmap"; -@import "components/panel_add_panel"; -@import "components/settings_permissions"; -@import "components/tagsinput"; -@import "components/tables_lists"; -@import "components/search"; -@import "components/gf-form"; -@import "components/sidemenu"; -@import "components/navbar"; -@import "components/timepicker"; -@import "components/filter-controls"; -@import "components/filter-list"; -@import "components/filter-table"; -@import "components/old_stuff"; -@import "components/typeahead"; -@import "components/modals"; -@import "components/dropdown"; -@import "components/color_picker"; -@import "components/footer"; -@import "components/infobox"; -@import "components/shortcuts"; -@import "components/drop"; -@import "components/query_editor"; -@import "components/tabbed_view"; -@import "components/query_part"; -@import "components/jsontree"; -@import "components/edit_sidemenu"; -@import "components/row.scss"; -@import "components/json_explorer"; -@import "components/code_editor"; -@import "components/dashboard_grid"; -@import "components/dashboard_list"; -@import "components/page_header"; -@import "components/dashboard_settings"; -@import "components/empty_list_cta"; +@import 'components/scrollbar'; +@import 'components/cards'; +@import 'components/buttons'; +@import 'components/navs'; +@import 'components/tabs'; +@import 'components/alerts'; +@import 'components/switch'; +@import 'components/tooltip'; +@import 'components/tags'; +@import 'components/panel_graph'; +@import 'components/submenu'; +@import 'components/panel_alertlist'; +@import 'components/panel_dashlist'; +@import 'components/panel_gettingstarted'; +@import 'components/panel_pluginlist'; +@import 'components/panel_singlestat'; +@import 'components/panel_table'; +@import 'components/panel_text'; +@import 'components/panel_heatmap'; +@import 'components/panel_add_panel'; +@import 'components/settings_permissions'; +@import 'components/tagsinput'; +@import 'components/tables_lists'; +@import 'components/search'; +@import 'components/gf-form'; +@import 'components/sidemenu'; +@import 'components/navbar'; +@import 'components/timepicker'; +@import 'components/filter-controls'; +@import 'components/filter-list'; +@import 'components/filter-table'; +@import 'components/old_stuff'; +@import 'components/typeahead'; +@import 'components/modals'; +@import 'components/dropdown'; +@import 'components/color_picker'; +@import 'components/footer'; +@import 'components/infobox'; +@import 'components/shortcuts'; +@import 'components/drop'; +@import 'components/query_editor'; +@import 'components/tabbed_view'; +@import 'components/query_part'; +@import 'components/jsontree'; +@import 'components/edit_sidemenu'; +@import 'components/row.scss'; +@import 'components/json_explorer'; +@import 'components/code_editor'; +@import 'components/dashboard_grid'; +@import 'components/dashboard_list'; +@import 'components/page_header'; +@import 'components/dashboard_settings'; +@import 'components/empty_list_cta'; +@import 'components/popper'; // PAGES -@import "pages/login"; -@import "pages/dashboard"; -@import "pages/playlist"; -@import "pages/admin"; -@import "pages/alerting"; -@import "pages/history"; -@import "pages/plugins"; -@import "pages/signup"; -@import "pages/styleguide"; -@import "pages/errorpage"; -@import "old_responsive"; -@import "components/view_states.scss"; +@import 'pages/login'; +@import 'pages/dashboard'; +@import 'pages/playlist'; +@import 'pages/admin'; +@import 'pages/alerting'; +@import 'pages/history'; +@import 'pages/plugins'; +@import 'pages/signup'; +@import 'pages/styleguide'; +@import 'pages/errorpage'; +@import 'old_responsive'; +@import 'components/view_states.scss'; diff --git a/public/sass/components/_popper.scss b/public/sass/components/_popper.scss new file mode 100644 index 00000000000..a4809535feb --- /dev/null +++ b/public/sass/components/_popper.scss @@ -0,0 +1,79 @@ +.popper { + position: absolute; + background: $tooltipBackground; + color: $tooltipColor; + width: 150px; + border-radius: 3px; + box-shadow: 0 0 2px rgba(0, 0, 0, 0.5); + padding: 10px; + text-align: center; +} +.popper .popper__arrow { + width: 0; + height: 0; + border-style: solid; + position: absolute; + margin: 5px; +} + +.popper .popper__arrow { + border-color: $tooltipBackground; +} + +.popper[data-placement^='top'] { + margin-bottom: 5px; +} +.popper[data-placement^='top'] .popper__arrow { + border-width: 5px 5px 0 5px; + border-left-color: transparent; + border-right-color: transparent; + border-bottom-color: transparent; + bottom: -5px; + left: calc(50% - 5px); + margin-top: 0; + margin-bottom: 0; +} +.popper[data-placement^='bottom'] { + margin-top: 5px; +} +.popper[data-placement^='bottom'] .popper__arrow { + border-width: 0 5px 5px 5px; + border-left-color: transparent; + border-right-color: transparent; + border-top-color: transparent; + top: -5px; + left: calc(50% - 5px); + margin-top: 0; + margin-bottom: 0; +} +.popper[data-placement^='right'] { + margin-left: 5px; +} +.popper[data-placement^='right'] .popper__arrow { + border-width: 5px 5px 5px 0; + border-left-color: transparent; + border-top-color: transparent; + border-bottom-color: transparent; + left: -5px; + top: calc(50% - 5px); + margin-left: 0; + margin-right: 0; +} +.popper[data-placement^='left'] { + margin-right: 5px; +} +.popper[data-placement^='left'] .popper__arrow { + border-width: 5px 0 5px 5px; + border-top-color: transparent; + border-right-color: transparent; + border-bottom-color: transparent; + right: -5px; + top: calc(50% - 5px); + margin-left: 0; + margin-right: 0; +} + +.popper__target, +.popper__manager { + display: inline-block; +} diff --git a/yarn.lock b/yarn.lock index 2c54a93aed8..fd9c6e70831 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7504,6 +7504,10 @@ pn@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/pn/-/pn-1.0.0.tgz#1cf5a30b0d806cd18f88fc41a6b5d4ad615b3ba9" +popper.js@^1.12.5: + version "1.12.9" + resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.12.9.tgz#0dfbc2dff96c451bb332edcfcfaaf566d331d5b3" + posix-character-classes@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" @@ -8123,6 +8127,13 @@ react-grid-layout@^0.16.1: react-draggable "^3.0.3" react-resizable "^1.7.5" +react-popper@^0.7.5: + version "0.7.5" + resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-0.7.5.tgz#71c25946f291db381231281f6b95729e8b801596" + dependencies: + popper.js "^1.12.5" + prop-types "^15.5.10" + react-resizable@^1.7.5: version "1.7.5" resolved "https://registry.yarnpkg.com/react-resizable/-/react-resizable-1.7.5.tgz#83eb75bb3684da6989bbbf4f826e1470f0af902e"