diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 594faa57b0a..6548f75e91f 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -11,6 +11,8 @@ "license": "ISC", "dependencies": { "@torkelo/react-select": "2.1.1", + "@types/react-test-renderer": "^16.0.3", + "@types/react-transition-group": "^2.0.15", "classnames": "^2.2.5", "jquery": "^3.2.1", "lodash": "^4.17.10", diff --git a/public/app/core/components/Portal/Portal.tsx b/packages/grafana-ui/src/components/Portal/Portal.tsx similarity index 87% rename from public/app/core/components/Portal/Portal.tsx rename to packages/grafana-ui/src/components/Portal/Portal.tsx index 25d54a64209..6f51f4053e2 100644 --- a/public/app/core/components/Portal/Portal.tsx +++ b/packages/grafana-ui/src/components/Portal/Portal.tsx @@ -6,11 +6,11 @@ interface Props { root?: HTMLElement; } -export default class BodyPortal extends PureComponent { +export class Portal extends PureComponent { node: HTMLElement = document.createElement('div'); portalRoot: HTMLElement; - constructor(props) { + constructor(props: Props) { super(props); const { className, diff --git a/public/app/core/components/Tooltip/Popper.tsx b/packages/grafana-ui/src/components/Tooltip/Popper.tsx similarity index 74% rename from public/app/core/components/Tooltip/Popper.tsx rename to packages/grafana-ui/src/components/Tooltip/Popper.tsx index 65ef510ba8f..f6f9fa6f73a 100644 --- a/public/app/core/components/Tooltip/Popper.tsx +++ b/packages/grafana-ui/src/components/Tooltip/Popper.tsx @@ -1,6 +1,7 @@ import React, { PureComponent } from 'react'; -import Portal from 'app/core/components/Portal/Portal'; -import { Manager, Popper as ReactPopper, Reference } from 'react-popper'; +import * as PopperJS from 'popper.js'; +import { Manager, Popper as ReactPopper } from 'react-popper'; +import { Portal } from '@grafana/ui'; import Transition from 'react-transition-group/Transition'; export enum Themes { @@ -13,45 +14,40 @@ const defaultTransitionStyles = { opacity: 0, }; -const transitionStyles = { +const transitionStyles: {[key: string]: object} = { exited: { opacity: 0 }, entering: { opacity: 0 }, entered: { opacity: 1 }, exiting: { opacity: 0 }, }; -interface Props { +interface Props extends React.DOMAttributes { renderContent: (content: any) => any; show: boolean; - placement?: any; + placement?: PopperJS.Placement; content: string | ((props: any) => JSX.Element); - refClassName?: string; + referenceElement: PopperJS.ReferenceObject; theme?: Themes; } class Popper extends PureComponent { render() { - const { children, renderContent, show, placement, refClassName, theme } = this.props; + const { renderContent, show, placement, onMouseEnter, onMouseLeave, theme } = this.props; const { content } = this.props; const popperBackgroundClassName = 'popper__background' + (theme ? ' ' + theme : ''); return ( - - {({ ref }) => ( -
- {children} -
- )} -
{transitionState => ( - + {({ ref, style, placement, arrowProps }) => { return (
JSX.Element); + +export interface UsingPopperProps { + show?: boolean; + placement?: PopperJS.Placement; + content: PopperContent; + children: JSX.Element; + renderContent?: (content: PopperContent) => JSX.Element; + theme?: Themes; +} + +type PopperControllerRenderProp = ( + showPopper: () => void, + hidePopper: () => void, + popperProps: { + show: boolean; + placement: PopperJS.Placement; + content: string | ((props: any) => JSX.Element); + renderContent: (content: any) => any; + theme?: Themes; + } +) => JSX.Element; + +interface Props { + placement?: PopperJS.Placement; + content: PopperContent; + className?: string; + children: PopperControllerRenderProp; + theme?: Themes; +} + +interface State { + placement: PopperJS.Placement; + show: boolean; +} + +class PopperController extends React.Component { + constructor(props: Props) { + super(props); + + this.state = { + placement: this.props.placement || 'auto', + show: false, + }; + } + + componentWillReceiveProps(nextProps: Props) { + if (nextProps.placement && nextProps.placement !== this.state.placement) { + this.setState((prevState: State) => { + return { + ...prevState, + placement: nextProps.placement || 'auto', + }; + }); + } + } + + showPopper = () => { + this.setState(prevState => ({ + ...prevState, + show: true, + })); + }; + + hidePopper = () => { + this.setState(prevState => ({ + ...prevState, + show: false, + })); + }; + + renderContent(content: PopperContent) { + if (typeof content === 'function') { + // If it's a function we assume it's a React component + const ReactComponent = content; + return ; + } + return content; + } + + render() { + const { children, content, theme } = this.props; + const { show, placement } = this.state; + + return children(this.showPopper, this.hidePopper, { + show, + placement, + content, + renderContent: this.renderContent, + theme, + }); + } +} + +export default PopperController; diff --git a/public/app/core/components/Tooltip/Tooltip.test.tsx b/packages/grafana-ui/src/components/Tooltip/Tooltip.test.tsx similarity index 56% rename from public/app/core/components/Tooltip/Tooltip.test.tsx rename to packages/grafana-ui/src/components/Tooltip/Tooltip.test.tsx index d2c96bb23d2..95d01c7f2fe 100644 --- a/public/app/core/components/Tooltip/Tooltip.test.tsx +++ b/packages/grafana-ui/src/components/Tooltip/Tooltip.test.tsx @@ -1,13 +1,15 @@ import React from 'react'; import renderer from 'react-test-renderer'; -import Tooltip from './Tooltip'; +import { Tooltip } from './Tooltip'; describe('Tooltip', () => { it('renders correctly', () => { const tree = renderer .create( - - Link with tooltip + + + Link with tooltip + ) .toJSON(); diff --git a/packages/grafana-ui/src/components/Tooltip/Tooltip.tsx b/packages/grafana-ui/src/components/Tooltip/Tooltip.tsx new file mode 100644 index 00000000000..9cffb151d83 --- /dev/null +++ b/packages/grafana-ui/src/components/Tooltip/Tooltip.tsx @@ -0,0 +1,32 @@ +import React, { createRef } from 'react'; +import * as PopperJS from 'popper.js'; +import Popper from './Popper'; +import PopperController, { UsingPopperProps } from './PopperController'; + +export const Tooltip = ({ children, renderContent, ...controllerProps }: UsingPopperProps) => { + const tooltipTriggerRef = createRef(); + + return ( + + {(showPopper, hidePopper, popperProps) => { + return ( + <> + {tooltipTriggerRef.current && ( + + )} + {React.cloneElement(children, { + ref: tooltipTriggerRef, + onMouseEnter: showPopper, + onMouseLeave: hidePopper, + })} + + ); + }} + + ); +}; diff --git a/public/sass/components/_popper.scss b/packages/grafana-ui/src/components/Tooltip/_Tooltip.scss similarity index 100% rename from public/sass/components/_popper.scss rename to packages/grafana-ui/src/components/Tooltip/_Tooltip.scss diff --git a/packages/grafana-ui/src/components/Tooltip/__snapshots__/Tooltip.test.tsx.snap b/packages/grafana-ui/src/components/Tooltip/__snapshots__/Tooltip.test.tsx.snap new file mode 100644 index 00000000000..761221906d4 --- /dev/null +++ b/packages/grafana-ui/src/components/Tooltip/__snapshots__/Tooltip.test.tsx.snap @@ -0,0 +1,12 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Tooltip renders correctly 1`] = ` + + Link with tooltip + +`; diff --git a/packages/grafana-ui/src/components/index.scss b/packages/grafana-ui/src/components/index.scss index 0e18eaf018c..e1d1474bb16 100644 --- a/packages/grafana-ui/src/components/index.scss +++ b/packages/grafana-ui/src/components/index.scss @@ -1,2 +1,3 @@ @import 'CustomScrollbar/CustomScrollbar'; @import 'DeleteButton/DeleteButton'; +@import 'Tooltip/Tooltip'; diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index 7423ce2a93a..abb1cf1b34c 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -1,2 +1,4 @@ export { DeleteButton } from './DeleteButton/DeleteButton'; +export { Tooltip } from './Tooltip/Tooltip'; +export { Portal } from './Portal/Portal'; export { CustomScrollbar } from './CustomScrollbar/CustomScrollbar'; diff --git a/public/app/core/components/Label/Label.tsx b/public/app/core/components/Label/Label.tsx index 362c3c577f7..5d60efa056a 100644 --- a/public/app/core/components/Label/Label.tsx +++ b/public/app/core/components/Label/Label.tsx @@ -1,5 +1,5 @@ import React, { SFC, ReactNode } from 'react'; -import Tooltip from '../Tooltip/Tooltip'; +import { Tooltip } from '@grafana/ui'; interface Props { tooltip?: string; @@ -14,8 +14,10 @@ export const Label: SFC = props => { {props.children} {props.tooltip && ( - - + +
+ +
)}
diff --git a/public/app/core/components/ToggleButtonGroup/ToggleButtonGroup.tsx b/public/app/core/components/ToggleButtonGroup/ToggleButtonGroup.tsx index 2524a265054..86e15923bda 100644 --- a/public/app/core/components/ToggleButtonGroup/ToggleButtonGroup.tsx +++ b/public/app/core/components/ToggleButtonGroup/ToggleButtonGroup.tsx @@ -1,5 +1,5 @@ import React, { SFC, ReactNode, PureComponent } from 'react'; -import Tooltip from 'app/core/components/Tooltip/Tooltip'; +import { Tooltip } from '@grafana/ui'; interface ToggleButtonGroupProps { label?: string; diff --git a/public/app/core/components/Tooltip/Popover.test.tsx b/public/app/core/components/Tooltip/Popover.test.tsx deleted file mode 100644 index eea416a9262..00000000000 --- a/public/app/core/components/Tooltip/Popover.test.tsx +++ /dev/null @@ -1,16 +0,0 @@ -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 deleted file mode 100644 index 62397243c1c..00000000000 --- a/public/app/core/components/Tooltip/Popover.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React, { PureComponent } from 'react'; -import Popper from './Popper'; -import withPopper, { UsingPopperProps } from './withPopper'; - -class Popover extends PureComponent { - render() { - const { children, hidePopper, showPopper, className, ...restProps } = this.props; - - const togglePopper = restProps.show ? hidePopper : showPopper; - - return ( -
- {children} -
- ); - } -} - -export default withPopper(Popover); diff --git a/public/app/core/components/Tooltip/Tooltip.tsx b/public/app/core/components/Tooltip/Tooltip.tsx deleted file mode 100644 index 795da94a03c..00000000000 --- a/public/app/core/components/Tooltip/Tooltip.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import React, { PureComponent } from 'react'; -import Popper from './Popper'; -import withPopper, { UsingPopperProps } from './withPopper'; - -class Tooltip extends PureComponent { - render() { - const { children, hidePopper, showPopper, className, ...restProps } = this.props; - - return ( -
- {children} -
- ); - } -} - -export default withPopper(Tooltip); diff --git a/public/app/core/components/Tooltip/__snapshots__/Popover.test.tsx.snap b/public/app/core/components/Tooltip/__snapshots__/Popover.test.tsx.snap deleted file mode 100644 index b36a4fe9af9..00000000000 --- a/public/app/core/components/Tooltip/__snapshots__/Popover.test.tsx.snap +++ /dev/null @@ -1,16 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Popover renders correctly 1`] = ` -
-
- -
-
-`; diff --git a/public/app/core/components/Tooltip/__snapshots__/Tooltip.test.tsx.snap b/public/app/core/components/Tooltip/__snapshots__/Tooltip.test.tsx.snap deleted file mode 100644 index c7d680049f4..00000000000 --- a/public/app/core/components/Tooltip/__snapshots__/Tooltip.test.tsx.snap +++ /dev/null @@ -1,19 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Tooltip renders correctly 1`] = ` - -`; diff --git a/public/app/core/components/Tooltip/withPopper.tsx b/public/app/core/components/Tooltip/withPopper.tsx deleted file mode 100644 index 3766b78f0f6..00000000000 --- a/public/app/core/components/Tooltip/withPopper.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import React from 'react'; -import { Themes } from './Popper'; -export interface UsingPopperProps { - showPopper: (prevState: object) => void; - hidePopper: (prevState: object) => void; - renderContent: (content: any) => any; - show: boolean; - placement?: string; - content: string | ((props: any) => JSX.Element); - className?: string; - refClassName?: string; - theme?: Themes; -} - -interface Props { - placement?: string; - className?: string; - refClassName?: string; - content: string | ((props: any) => JSX.Element); - theme?: Themes; -} - -interface State { - placement: string; - show: boolean; -} - -export default function withPopper(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, - }; - }); - } - } - - showPopper = () => { - this.setState(prevState => ({ - ...prevState, - show: true, - })); - }; - - hidePopper = () => { - this.setState(prevState => ({ - ...prevState, - show: false, - })); - }; - - 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 { show, placement } = this.state; - const className = this.props.className || ''; - return ( - - ); - } - }; -} diff --git a/public/app/features/dashboard/dashgrid/DataPanel.tsx b/public/app/features/dashboard/dashgrid/DataPanel.tsx index fa21276d7a2..d71a274ab10 100644 --- a/public/app/features/dashboard/dashgrid/DataPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DataPanel.tsx @@ -1,6 +1,8 @@ // Library import React, { Component } from 'react'; -import Tooltip from 'app/core/components/Tooltip/Tooltip'; +import { Tooltip } from '@grafana/ui'; +import { Themes } from '@grafana/ui/src/components/Tooltip/Popper'; + import ErrorBoundary from 'app/core/components/ErrorBoundary/ErrorBoundary'; // Services @@ -12,7 +14,6 @@ import kbn from 'app/core/utils/kbn'; // Types import { DataQueryOptions, DataQueryResponse } from 'app/types'; import { TimeRange, TimeSeries, LoadingState } from '@grafana/ui'; -import { Themes } from 'app/core/components/Tooltip/Popper'; const DEFAULT_PLUGIN_ERROR = 'Error in plugin'; @@ -144,10 +145,10 @@ export class DataPanel extends Component { this.setState({ loading: LoadingState.Error, isFirstLoad: false, - errorMessage: errorMessage + errorMessage: errorMessage, }); } - } + }; render() { const { queries } = this.props; @@ -171,7 +172,7 @@ export class DataPanel extends Component { <> {this.renderLoadingStates()} - {({error, errorInfo}) => { + {({ error, errorInfo }) => { if (errorInfo) { this.onError(error.message || DEFAULT_PLUGIN_ERROR); return null; @@ -200,15 +201,11 @@ export class DataPanel extends Component { ); } else if (loading === LoadingState.Error) { return ( - - - + +
+ + +
); } diff --git a/public/app/features/dashboard/dashgrid/DataSourceOption.tsx b/public/app/features/dashboard/dashgrid/DataSourceOption.tsx index 0adfc4abe16..9a3ce527510 100644 --- a/public/app/features/dashboard/dashgrid/DataSourceOption.tsx +++ b/public/app/features/dashboard/dashgrid/DataSourceOption.tsx @@ -1,5 +1,5 @@ import React, { SFC } from 'react'; -import Tooltip from 'app/core/components/Tooltip/Tooltip'; +import { Tooltip } from '@grafana/ui'; interface Props { label: string; diff --git a/public/app/features/dashboard/dashgrid/PanelEditor.tsx b/public/app/features/dashboard/dashgrid/PanelEditor.tsx index 2b91e19d83c..123204aa239 100644 --- a/public/app/features/dashboard/dashgrid/PanelEditor.tsx +++ b/public/app/features/dashboard/dashgrid/PanelEditor.tsx @@ -15,7 +15,7 @@ import { PanelModel } from '../panel_model'; import { DashboardModel } from '../dashboard_model'; import { PanelPlugin } from 'app/types/plugins'; -import Tooltip from 'app/core/components/Tooltip/Tooltip'; +import { Tooltip } from '@grafana/ui'; interface PanelEditorProps { panel: PanelModel; @@ -138,7 +138,7 @@ function TabItem({ tab, activeTab, onClick }: TabItemParams) { return (
onClick(tab)}> - + diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderCorner.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderCorner.tsx index 331e469a60d..6b6f81fc579 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderCorner.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderCorner.tsx @@ -1,10 +1,10 @@ import React, { Component } from 'react'; +import Remarkable from 'remarkable'; +import { Tooltip } from '@grafana/ui'; import { PanelModel } from 'app/features/dashboard/panel_model'; -import Tooltip from 'app/core/components/Tooltip/Tooltip'; import templateSrv from 'app/features/templating/template_srv'; import { LinkSrv } from 'app/features/dashboard/panellinks/link_srv'; import { getTimeSrv, TimeSrv } from 'app/features/dashboard/time_srv'; -import Remarkable from 'remarkable'; enum InfoModes { Error = 'Error', @@ -78,12 +78,14 @@ export class PanelHeaderCorner extends Component { {infoMode === InfoModes.Info || infoMode === InfoModes.Links ? ( - - +
+ + +
) : null} diff --git a/public/app/features/dashboard/permissions/DashboardPermissions.tsx b/public/app/features/dashboard/permissions/DashboardPermissions.tsx index c07bef42930..96d0e23adcd 100644 --- a/public/app/features/dashboard/permissions/DashboardPermissions.tsx +++ b/public/app/features/dashboard/permissions/DashboardPermissions.tsx @@ -1,5 +1,5 @@ import React, { PureComponent } from 'react'; -import Tooltip from 'app/core/components/Tooltip/Tooltip'; +import { Tooltip } from '@grafana/ui'; import SlideDown from 'app/core/components/Animations/SlideDown'; import { StoreState, FolderInfo } from 'app/types'; import { DashboardAcl, PermissionLevel, NewDashboardAclItem } from 'app/types/acl'; @@ -70,8 +70,10 @@ export class DashboardPermissions extends PureComponent {

Permissions

- - + +
+ +