diff --git a/.circleci/config.yml b/.circleci/config.yml index dba6c5f8bd0..236d5aec398 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -323,7 +323,7 @@ jobs: deploy-enterprise-master: docker: - - image: grafana/grafana-ci-deploy:1.0.0 + - image: grafana/grafana-ci-deploy:1.1.0 steps: - attach_workspace: at: . @@ -346,7 +346,7 @@ jobs: deploy-enterprise-release: docker: - - image: grafana/grafana-ci-deploy:1.0.0 + - image: grafana/grafana-ci-deploy:1.1.0 steps: - attach_workspace: at: . @@ -365,10 +365,20 @@ jobs: - run: name: Deploy to Grafana.com command: './scripts/build/publish.sh --enterprise' + - run: + name: Load GPG private key + comand: './scripts/build/load-signing-key.sh' + - run: + name: Update Debian repository + command: './scripts/build/update_repo/update-deb.sh "enterprise" "$GPG_KEY_PASSWORD" "$CIRCLE_TAG"' + - run: + name: Update RPM repository + command: './scripts/build/update_repo/update-rpm.sh "enterprise" "$GPG_KEY_PASSWORD" "$CIRCLE_TAG"' + deploy-master: docker: - - image: grafana/grafana-ci-deploy:1.0.0 + - image: grafana/grafana-ci-deploy:1.1.0 steps: - attach_workspace: at: . @@ -398,8 +408,9 @@ jobs: deploy-release: docker: - - image: grafana/grafana-ci-deploy:1.0.0 + - image: grafana/grafana-ci-deploy:1.1.0 steps: + - checkout - attach_workspace: at: . - run: @@ -417,6 +428,15 @@ jobs: - run: name: Deploy to Grafana.com command: './scripts/build/publish.sh' + - run: + name: Load GPG private key + comand: './scripts/build/load-signing-key.sh' + - run: + name: Update Debian repository + command: './scripts/build/update_repo/update-deb.sh "oss" "$GPG_KEY_PASSWORD" "$CIRCLE_TAG"' + - run: + name: Update RPM repository + command: './scripts/build/update_repo/update-rpm.sh "oss" "$GPG_KEY_PASSWORD" "$CIRCLE_TAG"' workflows: version: 2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 867253ccd18..671740f7225 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ * **Proxy whitelist**: Add CIDR capability to auth_proxy whitelist [#14546](https://github.com/grafana/grafana/issues/14546), thx [@jacobrichard](https://github.com/jacobrichard) * **OAuth**: Support OAuth providers that are not RFC6749 compliant [#14562](https://github.com/grafana/grafana/issues/14562), thx [@tdabasinskas](https://github.com/tdabasinskas) * **Units**: Add blood glucose level units mg/dL and mmol/L [#14519](https://github.com/grafana/grafana/issues/14519), thx [@kjedamzik](https://github.com/kjedamzik) +* **Stackdriver**: Aggregating series returns more than one series [#14581](https://github.com/grafana/grafana/issues/14581) and [#13914](https://github.com/grafana/grafana/issues/13914), thx [@kinok](https://github.com/kinok) ### Bug fixes * **Search**: Fix for issue with scrolling the "tags filter" dropdown, fixes [#14486](https://github.com/grafana/grafana/issues/14486) diff --git a/docs/sources/http_api/admin.md b/docs/sources/http_api/admin.md index 2d4be21bb78..a27fd2aac14 100644 --- a/docs/sources/http_api/admin.md +++ b/docs/sources/http_api/admin.md @@ -285,7 +285,7 @@ Content-Type: application/json HTTP/1.1 200 Content-Type: application/json -{message: "User permissions updated"} +{"message": "User permissions updated"} ``` ## Delete global User @@ -308,7 +308,7 @@ Content-Type: application/json HTTP/1.1 200 Content-Type: application/json -{message: "User deleted"} +{"message": "User deleted"} ``` ## Pause all alerts @@ -339,5 +339,5 @@ JSON Body schema: HTTP/1.1 200 Content-Type: application/json -{state: "new state", message: "alerts pause/un paused", "alertsAffected": 100} +{"state": "new state", "message": "alerts pause/un paused", "alertsAffected": 100} ``` diff --git a/docs/sources/installation/debian.md b/docs/sources/installation/debian.md index 7ed44572533..d26af5277a1 100644 --- a/docs/sources/installation/debian.md +++ b/docs/sources/installation/debian.md @@ -34,32 +34,29 @@ sudo dpkg -i grafana__amd64.deb Example: ```bash -wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.1.4_amd64.deb +wget https://dl.grafana.com/oss/release/grafana_5.4.2_amd64.deb sudo apt-get install -y adduser libfontconfig -sudo dpkg -i grafana_5.1.4_amd64.deb +sudo dpkg -i grafana_5.4.2_amd64.deb ``` ## APT Repository -Add the following line to your `/etc/apt/sources.list` file. +Create a file `/etc/apt/sources.list.d/grafana.list` and add the following to it. ```bash -deb https://packagecloud.io/grafana/stable/debian/ stretch main +deb https://packages.grafana.com/oss/deb stable main ``` -Use the above line even if you are on Ubuntu or another Debian version. -There is also a testing repository if you want beta or release -candidates. +There is a separate repository if you want beta releases. ```bash -deb https://packagecloud.io/grafana/testing/debian/ stretch main +deb https://packages.grafana.com/oss/deb beta main ``` -Then add the [Package Cloud](https://packagecloud.io/grafana) key. This -allows you to install signed packages. +Use the above line even if you are on Ubuntu or another Debian version. Then add our gpg key. This allows you to install signed packages. ```bash -curl https://packagecloud.io/gpg.key | sudo apt-key add - +curl https://packages.grafana.com/gpg.key | sudo apt-key add - ``` Update your Apt repositories and install Grafana diff --git a/docs/sources/installation/rpm.md b/docs/sources/installation/rpm.md index 5bf3b7ed745..156f703e164 100644 --- a/docs/sources/installation/rpm.md +++ b/docs/sources/installation/rpm.md @@ -32,7 +32,7 @@ $ sudo yum install Example: ```bash -$ sudo yum install https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.1.4-1.x86_64.rpm +$ sudo yum install https://dl.grafana.com/oss/release/grafana-5.4.2-1.x86_64.rpm ``` Or install manually using `rpm`. First execute @@ -44,7 +44,7 @@ $ wget Example: ```bash -$ wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.1.4-1.x86_64.rpm +$ wget https://dl.grafana.com/oss/release/grafana-5.4.2-1.x86_64.rpm ``` ### On CentOS / Fedora / Redhat: @@ -67,19 +67,27 @@ Add the following to a new file at `/etc/yum.repos.d/grafana.repo` ```bash [grafana] name=grafana -baseurl=https://packagecloud.io/grafana/stable/el/7/$basearch +baseurl=https://packages.grafana.com/oss/rpm repo_gpgcheck=1 enabled=1 gpgcheck=1 -gpgkey=https://packagecloud.io/gpg.key https://grafanarel.s3.amazonaws.com/RPM-GPG-KEY-grafana +gpgkey=https://packages.grafana.com/gpg.key sslverify=1 sslcacert=/etc/pki/tls/certs/ca-bundle.crt ``` -There is also a testing repository if you want beta or release candidates. +There is a separate repository if you want beta releases. ```bash -baseurl=https://packagecloud.io/grafana/testing/el/7/$basearch +[grafana] +name=grafana +baseurl=https://packages.grafana.com/oss/rpm-beta +repo_gpgcheck=1 +enabled=1 +gpgcheck=1 +gpgkey=https://packages.grafana.com/gpg.key +sslverify=1 +sslcacert=/etc/pki/tls/certs/ca-bundle.crt ``` Then install Grafana via the `yum` command. @@ -91,7 +99,7 @@ $ sudo yum install grafana ### RPM GPG Key The RPMs are signed, you can verify the signature with this [public GPG -key](https://grafanarel.s3.amazonaws.com/RPM-GPG-KEY-grafana). +key](https://packages.grafana.com/gpg.key). ## Package details diff --git a/package.json b/package.json index eefe2cbbe53..c8d891b91bc 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,6 @@ "@types/jquery": "^1.10.35", "@types/node": "^8.0.31", "@types/react": "^16.7.6", - "@types/react-custom-scrollbars": "^4.0.5", "@types/react-dom": "^16.0.9", "@types/react-select": "^2.0.4", "angular-mocks": "1.6.6", @@ -72,8 +71,8 @@ "ng-annotate-loader": "^0.6.1", "ng-annotate-webpack-plugin": "^0.3.0", "ngtemplate-loader": "^2.0.1", - "npm": "^5.4.2", "node-sass": "^4.11.0", + "npm": "^5.4.2", "optimize-css-assets-webpack-plugin": "^4.0.2", "phantomjs-prebuilt": "^2.1.15", "postcss-browser-reporter": "^0.5.0", @@ -167,7 +166,6 @@ "prop-types": "^15.6.2", "rc-cascader": "^0.14.0", "react": "^16.6.3", - "react-custom-scrollbars": "^4.2.1", "react-dom": "^16.6.3", "react-grid-layout": "0.16.6", "react-highlight-words": "0.11.0", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 2fb210e3b46..6548f75e91f 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -11,11 +11,14 @@ "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", "moment": "^2.22.2", "react": "^16.6.3", + "react-custom-scrollbars": "^4.2.1", "react-dom": "^16.6.3", "react-highlight-words": "0.11.0", "react-popper": "^1.3.0", @@ -23,11 +26,14 @@ "react-virtualized": "^9.21.0" }, "devDependencies": { + "@types/classnames": "^2.2.6", "@types/jest": "^23.3.2", + "@types/jquery": "^1.10.35", "@types/lodash": "^4.14.119", "@types/react": "^16.7.6", - "@types/classnames": "^2.2.6", - "@types/jquery": "^1.10.35", + "@types/react-custom-scrollbars": "^4.0.5", + "@types/react-test-renderer": "^16.0.3", + "react-test-renderer": "^16.7.0", "typescript": "^3.2.2" } } diff --git a/public/app/core/components/CustomScrollbar/CustomScrollbar.test.tsx b/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.test.tsx similarity index 100% rename from public/app/core/components/CustomScrollbar/CustomScrollbar.test.tsx rename to packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.test.tsx diff --git a/public/app/core/components/CustomScrollbar/CustomScrollbar.tsx b/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx similarity index 80% rename from public/app/core/components/CustomScrollbar/CustomScrollbar.tsx rename to packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx index 590c11c0615..cf1657e1c83 100644 --- a/public/app/core/components/CustomScrollbar/CustomScrollbar.tsx +++ b/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx @@ -12,7 +12,7 @@ interface Props { /** * Wraps component into component from `react-custom-scrollbars` */ -class CustomScrollbar extends PureComponent { +export class CustomScrollbar extends PureComponent { static defaultProps: Partial = { customClassName: 'custom-scrollbars', autoHide: true, @@ -28,8 +28,10 @@ class CustomScrollbar extends PureComponent {
} renderTrackVertical={props =>
} renderThumbHorizontal={props =>
} diff --git a/packages/grafana-ui/src/components/CustomScrollbar/_CustomScrollbar.scss b/packages/grafana-ui/src/components/CustomScrollbar/_CustomScrollbar.scss new file mode 100644 index 00000000000..c0a8077fb63 --- /dev/null +++ b/packages/grafana-ui/src/components/CustomScrollbar/_CustomScrollbar.scss @@ -0,0 +1,40 @@ +.custom-scrollbars { + // Fix for Firefox. For some reason sometimes .view container gets a height of its content, but in order to + // make scroll working it should fit outer container size (scroll appears only when inner container size is + // greater than outer one). + display: flex; + flex-grow: 1; + + .view { + display: flex; + flex-grow: 1; + flex-direction: column; + } + + .track-vertical { + border-radius: 3px; + width: 6px !important; + right: 2px; + bottom: 2px; + top: 2px; + } + + .track-horizontal { + border-radius: 3px; + height: 6px !important; + + right: 2px; + bottom: 2px; + left: 2px; + } + + .thumb-vertical { + @include gradient-vertical($scrollbarBackground, $scrollbarBackground2); + border-radius: 6px; + } + + .thumb-horizontal { + @include gradient-horizontal($scrollbarBackground, $scrollbarBackground2); + border-radius: 6px; + } +} \ No newline at end of file diff --git a/public/app/core/components/CustomScrollbar/__snapshots__/CustomScrollbar.test.tsx.snap b/packages/grafana-ui/src/components/CustomScrollbar/__snapshots__/CustomScrollbar.test.tsx.snap similarity index 91% rename from public/app/core/components/CustomScrollbar/__snapshots__/CustomScrollbar.test.tsx.snap rename to packages/grafana-ui/src/components/CustomScrollbar/__snapshots__/CustomScrollbar.test.tsx.snap index 310eb714af9..0a7de5fcffe 100644 --- a/public/app/core/components/CustomScrollbar/__snapshots__/CustomScrollbar.test.tsx.snap +++ b/packages/grafana-ui/src/components/CustomScrollbar/__snapshots__/CustomScrollbar.test.tsx.snap @@ -6,8 +6,8 @@ exports[`CustomScrollbar renders correctly 1`] = ` style={ Object { "height": "auto", - "maxHeight": "inherit", - "minHeight": "inherit", + "maxHeight": "100%", + "minHeight": "0", "overflow": "hidden", "position": "relative", "width": "100%", @@ -23,8 +23,8 @@ exports[`CustomScrollbar renders correctly 1`] = ` "left": undefined, "marginBottom": 0, "marginRight": 0, - "maxHeight": "calc(inherit + 0px)", - "minHeight": "calc(inherit + 0px)", + "maxHeight": "calc(100% + 0px)", + "minHeight": "calc(0 + 0px)", "overflow": "scroll", "position": "relative", "right": undefined, 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 61% rename from public/app/core/components/Tooltip/Popper.tsx rename to packages/grafana-ui/src/components/Tooltip/Popper.tsx index 36cf0fe837e..eb64df1cb6e 100644 --- a/public/app/core/components/Tooltip/Popper.tsx +++ b/packages/grafana-ui/src/components/Tooltip/Popper.tsx @@ -1,49 +1,54 @@ 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 { + Default = 'popper__background--default', + Error = 'popper__background--error', + Brand = 'popper__background--brand', +} + const defaultTransitionStyles = { transition: 'opacity 200ms linear', 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 } = 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 (
{ data-placement={placement} className="popper" > -
+
{renderContent(content)}
diff --git a/packages/grafana-ui/src/components/Tooltip/PopperController.tsx b/packages/grafana-ui/src/components/Tooltip/PopperController.tsx new file mode 100644 index 00000000000..5f4010ac58a --- /dev/null +++ b/packages/grafana-ui/src/components/Tooltip/PopperController.tsx @@ -0,0 +1,99 @@ +import React from 'react'; +import * as PopperJS from 'popper.js'; +import { Themes } from './Popper'; + +type PopperContent = string | (() => 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 83% rename from public/sass/components/_popper.scss rename to packages/grafana-ui/src/components/Tooltip/_Tooltip.scss index d869d52b92f..c8fa099cce6 100644 --- a/public/sass/components/_popper.scss +++ b/packages/grafana-ui/src/components/Tooltip/_Tooltip.scss @@ -1,5 +1,13 @@ $popper-margin-from-ref: 5px; + +@mixin popper-theme($backgroundColor, $arrowColor) { + background: $backgroundColor; + .popper__arrow { + border-color: $arrowColor; + } +} + .popper { position: absolute; z-index: $zindex-tooltip; @@ -8,7 +16,24 @@ $popper-margin-from-ref: 5px; text-align: center; } -.popper .popper__arrow { +.popper__background { + background: $tooltipBackground; + border-radius: $border-radius; + box-shadow: 0 0 2px rgba(0, 0, 0, 0.5); + padding: 10px; + + // Themes + &.popper__background--error { + @include popper-theme($tooltipBackgroundError, $tooltipBackgroundError); + } + + &.popper__background--brand { + @include popper-theme($tooltipBackgroundBrand, $tooltipBackgroundBrand); + @include gradient-vertical($red, $orange); + } +} + +.popper__arrow { width: 0; height: 0; border-style: solid; @@ -16,17 +41,10 @@ $popper-margin-from-ref: 5px; margin: 0px; } -.popper .popper__arrow { +.popper__arrow { border-color: $tooltipBackground; } -.popper__background { - background: $tooltipBackground; - border-radius: $border-radius; - box-shadow: 0 0 2px rgba(0, 0, 0, 0.5); - padding: 10px; -} - // Top .popper[data-placement^='top'] { padding-bottom: $popper-margin-from-ref; 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 d52508c946c..e1d1474bb16 100644 --- a/packages/grafana-ui/src/components/index.scss +++ b/packages/grafana-ui/src/components/index.scss @@ -1 +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 b57b9bcfdb7..abb1cf1b34c 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -1 +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/packages/grafana-ui/src/visualizations/Graph/Graph.tsx b/packages/grafana-ui/src/visualizations/Graph/Graph.tsx index 51afb33802d..ad038cebcda 100644 --- a/packages/grafana-ui/src/visualizations/Graph/Graph.tsx +++ b/packages/grafana-ui/src/visualizations/Graph/Graph.tsx @@ -98,6 +98,7 @@ export class Graph extends PureComponent { $.plot(this.element, timeSeries, flotOptions); } catch (err) { console.log('Graph rendering error', err, flotOptions, timeSeries); + throw new Error('Error rendering panel'); } } diff --git a/pkg/components/dashdiffs/formatter_json.go b/pkg/components/dashdiffs/formatter_json.go index 488a345d492..09dac1ae5ac 100644 --- a/pkg/components/dashdiffs/formatter_json.go +++ b/pkg/components/dashdiffs/formatter_json.go @@ -206,10 +206,9 @@ func (f *JSONFormatter) processObject(object map[string]interface{}, deltas []di // Added for _, delta := range deltas { - switch delta.(type) { + switch delta := delta.(type) { case *diff.Added: - d := delta.(*diff.Added) - f.printRecursive(d.Position.String(), d.Value, ChangeAdded) + f.printRecursive(delta.Position.String(), delta.Value, ChangeAdded) } } @@ -222,9 +221,8 @@ func (f *JSONFormatter) processItem(value interface{}, deltas []diff.Delta, posi if len(matchedDeltas) > 0 { for _, matchedDelta := range matchedDeltas { - switch matchedDelta.(type) { + switch matchedDelta := matchedDelta.(type) { case *diff.Object: - d := matchedDelta.(*diff.Object) switch value.(type) { case map[string]interface{}: //ok @@ -238,7 +236,7 @@ func (f *JSONFormatter) processItem(value interface{}, deltas []diff.Delta, posi f.print("{") f.closeLine() f.push(positionStr, len(o), false) - f.processObject(o, d.Deltas) + f.processObject(o, matchedDelta.Deltas) f.pop() f.newLine(ChangeNil) f.print("}") @@ -246,7 +244,6 @@ func (f *JSONFormatter) processItem(value interface{}, deltas []diff.Delta, posi f.closeLine() case *diff.Array: - d := matchedDelta.(*diff.Array) switch value.(type) { case []interface{}: //ok @@ -260,7 +257,7 @@ func (f *JSONFormatter) processItem(value interface{}, deltas []diff.Delta, posi f.print("[") f.closeLine() f.push(positionStr, len(a), true) - f.processArray(a, d.Deltas) + f.processArray(a, matchedDelta.Deltas) f.pop() f.newLine(ChangeNil) f.print("]") @@ -268,27 +265,23 @@ func (f *JSONFormatter) processItem(value interface{}, deltas []diff.Delta, posi f.closeLine() case *diff.Added: - d := matchedDelta.(*diff.Added) - f.printRecursive(positionStr, d.Value, ChangeAdded) + f.printRecursive(positionStr, matchedDelta.Value, ChangeAdded) f.size[len(f.size)-1]++ case *diff.Modified: - d := matchedDelta.(*diff.Modified) savedSize := f.size[len(f.size)-1] - f.printRecursive(positionStr, d.OldValue, ChangeOld) + f.printRecursive(positionStr, matchedDelta.OldValue, ChangeOld) f.size[len(f.size)-1] = savedSize - f.printRecursive(positionStr, d.NewValue, ChangeNew) + f.printRecursive(positionStr, matchedDelta.NewValue, ChangeNew) case *diff.TextDiff: savedSize := f.size[len(f.size)-1] - d := matchedDelta.(*diff.TextDiff) - f.printRecursive(positionStr, d.OldValue, ChangeOld) + f.printRecursive(positionStr, matchedDelta.OldValue, ChangeOld) f.size[len(f.size)-1] = savedSize - f.printRecursive(positionStr, d.NewValue, ChangeNew) + f.printRecursive(positionStr, matchedDelta.NewValue, ChangeNew) case *diff.Deleted: - d := matchedDelta.(*diff.Deleted) - f.printRecursive(positionStr, d.Value, ChangeDeleted) + f.printRecursive(positionStr, matchedDelta.Value, ChangeDeleted) default: return errors.New("Unknown Delta type detected") @@ -305,13 +298,13 @@ func (f *JSONFormatter) processItem(value interface{}, deltas []diff.Delta, posi func (f *JSONFormatter) searchDeltas(deltas []diff.Delta, position diff.Position) (results []diff.Delta) { results = make([]diff.Delta, 0) for _, delta := range deltas { - switch delta.(type) { + switch typedDelta := delta.(type) { case diff.PostDelta: - if delta.(diff.PostDelta).PostPosition() == position { + if typedDelta.PostPosition() == position { results = append(results, delta) } case diff.PreDelta: - if delta.(diff.PreDelta).PrePosition() == position { + if typedDelta.PrePosition() == position { results = append(results, delta) } default: @@ -417,20 +410,19 @@ func (f *JSONFormatter) print(a string) { } func (f *JSONFormatter) printRecursive(name string, value interface{}, change ChangeType) { - switch value.(type) { + switch value := value.(type) { case map[string]interface{}: f.newLine(change) f.printKey(name) f.print("{") f.closeLine() - m := value.(map[string]interface{}) - size := len(m) + size := len(value) f.push(name, size, false) - keys := sortKeys(m) + keys := sortKeys(value) for _, key := range keys { - f.printRecursive(key, m[key], change) + f.printRecursive(key, value[key], change) } f.pop() @@ -445,10 +437,9 @@ func (f *JSONFormatter) printRecursive(name string, value interface{}, change Ch f.print("[") f.closeLine() - s := value.([]interface{}) - size := len(s) + size := len(value) f.push("", size, true) - for _, item := range s { + for _, item := range value { f.printRecursive("", item, change) } f.pop() diff --git a/pkg/services/sqlstore/login_attempt.go b/pkg/services/sqlstore/login_attempt.go index 78da198e8e7..ceff2394dce 100644 --- a/pkg/services/sqlstore/login_attempt.go +++ b/pkg/services/sqlstore/login_attempt.go @@ -78,14 +78,14 @@ func GetUserLoginAttemptCount(query *m.GetUserLoginAttemptCountQuery) error { } func toInt64(i interface{}) int64 { - switch i.(type) { + switch i := i.(type) { case []byte: - n, _ := strconv.ParseInt(string(i.([]byte)), 10, 64) + n, _ := strconv.ParseInt(string(i), 10, 64) return n case int: - return int64(i.(int)) + return int64(i) case int64: - return i.(int64) + return i } return 0 } diff --git a/pkg/tsdb/cloudwatch/credentials.go b/pkg/tsdb/cloudwatch/credentials.go index 165f8fdbe97..fb92c827f7c 100644 --- a/pkg/tsdb/cloudwatch/credentials.go +++ b/pkg/tsdb/cloudwatch/credentials.go @@ -3,7 +3,6 @@ package cloudwatch import ( "fmt" "os" - "strings" "sync" "time" @@ -43,7 +42,7 @@ func GetCredentials(dsInfo *DatasourceInfo) (*credentials.Credentials, error) { secretAccessKey := "" sessionToken := "" var expiration *time.Time = nil - if dsInfo.AuthType == "arn" && strings.Index(dsInfo.AssumeRoleArn, "arn:aws:iam:") == 0 { + if dsInfo.AuthType == "arn" { params := &sts.AssumeRoleInput{ RoleArn: aws.String(dsInfo.AssumeRoleArn), RoleSessionName: aws.String("GrafanaSession"), diff --git a/public/app/core/components/EmptyListCTA/EmptyListCTA.tsx b/public/app/core/components/EmptyListCTA/EmptyListCTA.tsx index ae0e39cc26d..d63af72ae4d 100644 --- a/public/app/core/components/EmptyListCTA/EmptyListCTA.tsx +++ b/public/app/core/components/EmptyListCTA/EmptyListCTA.tsx @@ -24,12 +24,14 @@ class EmptyListCTA extends Component { {buttonTitle} -
- ProTip: {proTip} - - {proTipLinkTitle} - -
+ {proTip && ( +
+ ProTip: {proTip} + + {proTipLinkTitle} + +
+ )}
); } diff --git a/public/app/core/components/ErrorBoundary/ErrorBoundary.tsx b/public/app/core/components/ErrorBoundary/ErrorBoundary.tsx new file mode 100644 index 00000000000..188750b0fef --- /dev/null +++ b/public/app/core/components/ErrorBoundary/ErrorBoundary.tsx @@ -0,0 +1,44 @@ +import { Component } from 'react'; + +interface ErrorInfo { + componentStack: string; +} + +interface RenderProps { + error: Error; + errorInfo: ErrorInfo; +} + +interface Props { + children: (r: RenderProps) => JSX.Element; +} + +interface State { + error: Error; + errorInfo: ErrorInfo; +} + +class ErrorBoundary extends Component { + readonly state: State = { + error: null, + errorInfo: null, + }; + + componentDidCatch(error: Error, errorInfo: ErrorInfo) { + this.setState({ + error: error, + errorInfo: errorInfo + }); + } + + render() { + const { children } = this.props; + const { error, errorInfo } = this.state; + return children({ + error, + errorInfo, + }); + } +} + +export default ErrorBoundary; 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/ScrollBar/ScrollBar.tsx b/public/app/core/components/ScrollBar/ScrollBar.tsx deleted file mode 100644 index 24d17f67367..00000000000 --- a/public/app/core/components/ScrollBar/ScrollBar.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import React from 'react'; -import baron from 'baron'; - -export interface Props { - children: any; - className: string; -} - -export default class ScrollBar extends React.Component { - private container: any; - private scrollbar: baron; - - constructor(props) { - super(props); - } - - componentDidMount() { - this.scrollbar = baron({ - root: this.container.parentElement, - scroller: this.container, - bar: '.baron__bar', - barOnCls: '_scrollbar', - scrollingCls: '_scrolling', - track: '.baron__track', - }); - } - - componentDidUpdate() { - this.scrollbar.update(); - } - - componentWillUnmount() { - this.scrollbar.dispose(); - } - - // methods can be invoked by outside - setScrollTop(top) { - if (this.container) { - this.container.scrollTop = top; - this.scrollbar.update(); - - return true; - } - return false; - } - - setScrollLeft(left) { - if (this.container) { - this.container.scrollLeft = left; - this.scrollbar.update(); - - return true; - } - return false; - } - - update() { - this.scrollbar.update(); - } - - handleRef = ref => { - this.container = ref; - }; - - render() { - return ( -
-
- {this.props.children} -
- -
-
-
-
- ); - } -} diff --git a/public/app/core/components/Select/Select.tsx b/public/app/core/components/Select/Select.tsx index 893eb1a6655..f66e07c9ed6 100644 --- a/public/app/core/components/Select/Select.tsx +++ b/public/app/core/components/Select/Select.tsx @@ -11,7 +11,7 @@ import OptionGroup from './OptionGroup'; import IndicatorsContainer from './IndicatorsContainer'; import NoOptionsMessage from './NoOptionsMessage'; import ResetStyles from './ResetStyles'; -import CustomScrollbar from '../CustomScrollbar/CustomScrollbar'; +import { CustomScrollbar } from '@grafana/ui'; export interface SelectOptionItem { label?: string; 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 4ba05937531..00000000000 --- a/public/app/core/components/Tooltip/withPopper.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import React from 'react'; - -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; -} - -interface Props { - placement?: string; - className?: string; - refClassName?: string; - content: string | ((props: any) => JSX.Element); -} - -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/alerting/AlertTab.tsx b/public/app/features/alerting/AlertTab.tsx new file mode 100644 index 00000000000..a5afbc198fc --- /dev/null +++ b/public/app/features/alerting/AlertTab.tsx @@ -0,0 +1,141 @@ +// Libraries +import React, { PureComponent } from 'react'; + +// Services & Utils +import { AngularComponent, getAngularLoader } from 'app/core/services/AngularLoader'; +import appEvents from 'app/core/app_events'; + +// Components +import { EditorTabBody, EditorToolbarView } from '../dashboard/dashgrid/EditorTabBody'; +import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; +import StateHistory from './StateHistory'; +import 'app/features/alerting/AlertTabCtrl'; + +// Types +import { DashboardModel } from '../dashboard/dashboard_model'; +import { PanelModel } from '../dashboard/panel_model'; + +interface Props { + angularPanel?: AngularComponent; + dashboard: DashboardModel; + panel: PanelModel; +} + +export class AlertTab extends PureComponent { + element: any; + component: AngularComponent; + panelCtrl: any; + + componentDidMount() { + if (this.shouldLoadAlertTab()) { + this.loadAlertTab(); + } + } + + componentDidUpdate(prevProps: Props) { + if (this.shouldLoadAlertTab()) { + this.loadAlertTab(); + } + } + + shouldLoadAlertTab() { + return this.props.angularPanel && this.element && !this.component; + } + + componentWillUnmount() { + if (this.component) { + this.component.destroy(); + } + } + + loadAlertTab() { + const { angularPanel } = this.props; + + const scope = angularPanel.getScope(); + + // When full page reloading in edit mode the angular panel has on fully compiled & instantiated yet + if (!scope.$$childHead) { + setTimeout(() => { + this.forceUpdate(); + }); + return; + } + + this.panelCtrl = scope.$$childHead.ctrl; + const loader = getAngularLoader(); + const template = ''; + + const scopeProps = { + ctrl: this.panelCtrl, + }; + + this.component = loader.load(this.element, scopeProps, template); + } + + stateHistory = (): EditorToolbarView => { + return { + title: 'State history', + render: () => { + return ( + + ); + }, + }; + }; + + deleteAlert = (): EditorToolbarView => { + const { panel } = this.props; + return { + title: 'Delete', + btnType: 'danger', + onClick: () => { + appEvents.emit('confirm-modal', { + title: 'Delete Alert', + text: 'Are you sure you want to delete this alert rule?', + text2: 'You need to save dashboard for the delete to take effect', + icon: 'fa-trash', + yesText: 'Delete', + onConfirm: () => { + delete panel.alert; + panel.thresholds = []; + this.panelCtrl.alertState = null; + this.panelCtrl.render(); + this.forceUpdate(); + }, + }); + }, + }; + }; + + onAddAlert = () => { + this.panelCtrl._enableAlert(); + this.component.digest(); + this.forceUpdate(); + }; + + render() { + const { alert } = this.props.panel; + + const toolbarItems = alert ? [this.stateHistory(), this.deleteAlert()] : []; + + const model = { + title: 'Panel has no alert rule defined', + icon: 'icon-gf icon-gf-alert', + onClick: this.onAddAlert, + buttonTitle: 'Create Alert', + }; + + return ( + + <> +
(this.element = element)} /> + {!alert && } + + + ); + } +} diff --git a/public/app/features/alerting/AlertTabCtrl.ts b/public/app/features/alerting/AlertTabCtrl.ts index 6d87c159d02..2be25e9df6a 100644 --- a/public/app/features/alerting/AlertTabCtrl.ts +++ b/public/app/features/alerting/AlertTabCtrl.ts @@ -45,6 +45,7 @@ export class AlertTabCtrl { this.noDataModes = alertDef.noDataModes; this.executionErrorModes = alertDef.executionErrorModes; this.appSubUrl = config.appSubUrl; + this.panelCtrl._enableAlert = this.enable; } $onInit() { @@ -114,7 +115,7 @@ export class AlertTabCtrl { } getNotifications() { - return Promise.resolve( + return this.$q.when( this.notifications.map(item => { return this.uiSegmentSrv.newSegment(item.name); }) @@ -147,6 +148,7 @@ export class AlertTabCtrl { // reset plus button this.addNotificationSegment.value = this.uiSegmentSrv.newPlusButton().value; this.addNotificationSegment.html = this.uiSegmentSrv.newPlusButton().html; + this.addNotificationSegment.fake = true; } removeNotification(index) { @@ -353,11 +355,11 @@ export class AlertTabCtrl { }); } - enable() { + enable = () => { this.panel.alert = {}; this.initModel(); this.panel.alert.for = '5m'; //default value for new alerts. for existing alerts we use 0m to avoid breaking changes - } + }; evaluatorParamsChanged() { ThresholdMapper.alertToGraphThresholds(this.panel); diff --git a/public/app/features/alerting/StateHistory.tsx b/public/app/features/alerting/StateHistory.tsx new file mode 100644 index 00000000000..eb5541f6094 --- /dev/null +++ b/public/app/features/alerting/StateHistory.tsx @@ -0,0 +1,110 @@ +import React, { PureComponent } from 'react'; +import alertDef from './state/alertDef'; +import { getBackendSrv } from 'app/core/services/backend_srv'; +import { DashboardModel } from '../dashboard/dashboard_model'; +import appEvents from '../../core/app_events'; + +interface Props { + dashboard: DashboardModel; + panelId: number; + onRefresh: () => void; +} + +interface State { + stateHistoryItems: any[]; +} + +class StateHistory extends PureComponent { + state = { + stateHistoryItems: [], + }; + + componentDidMount(): void { + const { dashboard, panelId } = this.props; + + getBackendSrv() + .get(`/api/annotations?dashboardId=${dashboard.id}&panelId=${panelId}&limit=50&type=alert`) + .then(res => { + const items = res.map(item => { + return { + stateModel: alertDef.getStateDisplayModel(item.newState), + time: dashboard.formatDate(item.time, 'MMM D, YYYY HH:mm:ss'), + info: alertDef.getAlertAnnotationInfo(item), + }; + }); + + this.setState({ + stateHistoryItems: items, + }); + }); + } + + clearHistory = () => { + const { dashboard, onRefresh, panelId } = this.props; + + appEvents.emit('confirm-modal', { + title: 'Delete Alert History', + text: 'Are you sure you want to remove all history & annotations for this alert?', + icon: 'fa-trash', + yesText: 'Yes', + onConfirm: () => { + getBackendSrv() + .post('/api/annotations/mass-delete', { + dashboardId: dashboard.id, + panelId: panelId, + }) + .then(() => { + onRefresh(); + }); + + this.setState({ + stateHistoryItems: [], + }); + }, + }); + }; + + render() { + const { stateHistoryItems } = this.state; + + return ( +
+ {stateHistoryItems.length > 0 && ( +
+ Last 50 state changes + +
+ )} +
    + {stateHistoryItems.length > 0 ? ( + stateHistoryItems.map((item, index) => { + return ( +
  1. +
    + +
    +
    +
    +

    {item.alertName}

    +
    + {item.stateModel.text} +
    +
    + {item.info} +
    +
    {item.time}
    +
  2. + ); + }) + ) : ( + No state changes recorded + )} +
+
+ ); + } +} + +export default StateHistory; diff --git a/public/app/features/alerting/partials/alert_tab.html b/public/app/features/alerting/partials/alert_tab.html index 0e4e48a89a9..da862203da6 100644 --- a/public/app/features/alerting/partials/alert_tab.html +++ b/public/app/features/alerting/partials/alert_tab.html @@ -1,191 +1,168 @@ -
-
- +
+
+ {{ctrl.error}} +
+
+
+
+

Rule

+
+
+ Name + +
+
+ Evaluate every + +
+
+ + + + If an alert rule has a configured For and the query violates the configured + threshold it + will first go from OK to Pending. + Going from OK to Pending Grafana will not send any notifications. Once the alert + rule + has + been firing for more than For duration, it will change to Alerting and send alert + notifications. + +
+
+
-
-
-
- {{ctrl.error}} -
+
+

Conditions

+
+
+ + WHEN +
+
+ + + OF +
+
+ + +
+
+ + + + +
+
+ +
+
-
-
Alert Config
-
- Name - -
-
-
- Evaluate every - -
-
- - - - If an alert rule has a configured For and the query violates the configured threshold it will first go from OK to Pending. - Going from OK to Pending Grafana will not send any notifications. Once the alert rule has been firing for more than For duration, it will change to Alerting and send alert notifications. - -
-
-
+
+ +
+
-
-
Conditions
-
-
- - WHEN -
-
- - - OF -
-
- - -
-
- - - - -
-
- -
-
+
+

No Data & Error Handling

+
+
+ If no data or all values are null +
+
+ SET STATE TO +
+ +
+
+
-
- -
-
+
+
+ If execution error or timeout +
+
+ SET STATE TO +
+ +
+
+
-
-
- If no data or all values are null - SET STATE TO -
- -
-
+
+ +
+
-
- If execution error or timeout - SET STATE TO -
- -
-
+
+ Evaluating rule +
-
- -
-
+
+ +
+
+
-
- Evaluating rule -
- -
- -
-
- -
-
Notifications
-
-
- Send to - -  {{nc.name}}  - - - -
-
-
- Message - -
-
- -
- -
- State history (last 50 state changes) -
- -
-
- No state changes recorded -
- -
    -
  1. -
    - -
    -
    -
    -
    - {{al.stateModel.text}} -
    -
    - {{al.info}} -
    -
    - {{al.time}} -
    -
  2. -
-
-
-
-
- -
-
-
Panel has no alert rule defined
- -
-
+
+
Notifications
+
+
+
+ Send to +
+
+ +  {{nc.name}}  + + +
+
+ +
+
+
+ Message + +
+
+
diff --git a/public/app/features/dashboard/dashgrid/AlertTab.tsx b/public/app/features/dashboard/dashgrid/AlertTab.tsx deleted file mode 100644 index 7df7864c758..00000000000 --- a/public/app/features/dashboard/dashgrid/AlertTab.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import React, { PureComponent } from 'react'; - -import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader'; -import { EditorTabBody } from './EditorTabBody'; -import 'app/features/alerting/AlertTabCtrl'; - -interface Props { - angularPanel?: AngularComponent; -} - -export class AlertTab extends PureComponent { - element: any; - component: AngularComponent; - - constructor(props) { - super(props); - } - - componentDidMount() { - if (this.shouldLoadAlertTab()) { - this.loadAlertTab(); - } - } - - componentDidUpdate(prevProps: Props) { - if (this.shouldLoadAlertTab()) { - this.loadAlertTab(); - } - } - - shouldLoadAlertTab() { - return this.props.angularPanel && this.element; - } - - componentWillUnmount() { - if (this.component) { - this.component.destroy(); - } - } - - loadAlertTab() { - const { angularPanel } = this.props; - - const scope = angularPanel.getScope(); - - // When full page reloading in edit mode the angular panel has on fully compiled & instantiated yet - if (!scope.$$childHead) { - setTimeout(() => { - this.forceUpdate(); - }); - return; - } - - const panelCtrl = scope.$$childHead.ctrl; - const loader = getAngularLoader(); - const template = ''; - - const scopeProps = { - ctrl: panelCtrl, - }; - - this.component = loader.load(this.element, scopeProps, template); - } - - render() { - return ( - -
(this.element = element)} /> - - ); - } -} diff --git a/public/app/features/dashboard/dashgrid/DataPanel.tsx b/public/app/features/dashboard/dashgrid/DataPanel.tsx index 30a939b50aa..d71a274ab10 100644 --- a/public/app/features/dashboard/dashgrid/DataPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DataPanel.tsx @@ -1,5 +1,9 @@ // Library import React, { Component } from 'react'; +import { Tooltip } from '@grafana/ui'; +import { Themes } from '@grafana/ui/src/components/Tooltip/Popper'; + +import ErrorBoundary from 'app/core/components/ErrorBoundary/ErrorBoundary'; // Services import { getDatasourceSrv, DatasourceSrv } from 'app/features/plugins/datasource_srv'; @@ -11,6 +15,8 @@ import kbn from 'app/core/utils/kbn'; import { DataQueryOptions, DataQueryResponse } from 'app/types'; import { TimeRange, TimeSeries, LoadingState } from '@grafana/ui'; +const DEFAULT_PLUGIN_ERROR = 'Error in plugin'; + interface RenderProps { loading: LoadingState; timeSeries: TimeSeries[]; @@ -33,6 +39,7 @@ export interface Props { export interface State { isFirstLoad: boolean; loading: LoadingState; + errorMessage: string; response: DataQueryResponse; } @@ -51,6 +58,7 @@ export class DataPanel extends Component { this.state = { loading: LoadingState.NotStarted, + errorMessage: '', response: { data: [], }, @@ -90,7 +98,7 @@ export class DataPanel extends Component { return; } - this.setState({ loading: LoadingState.Loading }); + this.setState({ loading: LoadingState.Loading, errorMessage: '' }); try { const ds = await this.dataSourceSrv.get(datasource); @@ -128,7 +136,17 @@ export class DataPanel extends Component { }); } catch (err) { console.log('Loading error', err); - this.setState({ loading: LoadingState.Error, isFirstLoad: false }); + this.onError('Request Error'); + } + }; + + onError = (errorMessage: string) => { + if (this.state.loading !== LoadingState.Error || this.state.errorMessage !== errorMessage) { + this.setState({ + loading: LoadingState.Error, + isFirstLoad: false, + errorMessage: errorMessage, + }); } }; @@ -139,7 +157,7 @@ export class DataPanel extends Component { const timeSeries = response.data; if (isFirstLoad && loading === LoadingState.Loading) { - return this.renderLoadingSpinner(); + return this.renderLoadingStates(); } if (!queries.length) { @@ -152,24 +170,44 @@ export class DataPanel extends Component { return ( <> - {this.renderLoadingSpinner()} - {this.props.children({ - timeSeries, - loading, - })} + {this.renderLoadingStates()} + + {({ error, errorInfo }) => { + if (errorInfo) { + this.onError(error.message || DEFAULT_PLUGIN_ERROR); + return null; + } + return ( + <> + {this.props.children({ + timeSeries, + loading, + })} + + ); + }} + ); } - private renderLoadingSpinner(): JSX.Element { - const { loading } = this.state; - + private renderLoadingStates(): JSX.Element { + const { loading, errorMessage } = this.state; if (loading === LoadingState.Loading) { return (
); + } else if (loading === LoadingState.Error) { + return ( + +
+ + +
+
+ ); } return null; 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/EditorTabBody.tsx b/public/app/features/dashboard/dashgrid/EditorTabBody.tsx index 7606d327405..b159cb30a4b 100644 --- a/public/app/features/dashboard/dashgrid/EditorTabBody.tsx +++ b/public/app/features/dashboard/dashgrid/EditorTabBody.tsx @@ -2,7 +2,7 @@ import React, { PureComponent } from 'react'; // Components -import CustomScrollbar from 'app/core/components/CustomScrollbar/CustomScrollbar'; +import { CustomScrollbar } from '@grafana/ui'; import { FadeIn } from 'app/core/components/Animations/FadeIn'; import { PanelOptionSection } from './PanelOptionSection'; @@ -10,21 +10,22 @@ interface Props { children: JSX.Element; heading: string; renderToolbar?: () => JSX.Element; - toolbarItems?: EditorToolBarView[]; + toolbarItems?: EditorToolbarView[]; } -export interface EditorToolBarView { +export interface EditorToolbarView { title?: string; heading?: string; - imgSrc?: string; icon?: string; disabled?: boolean; onClick?: () => void; - render: (closeFunction?: any) => JSX.Element | JSX.Element[]; + render?: () => JSX.Element; + action?: () => void; + btnType?: 'danger'; } interface State { - openView?: EditorToolBarView; + openView?: EditorToolbarView; isOpen: boolean; fadeIn: boolean; } @@ -48,7 +49,7 @@ export class EditorTabBody extends PureComponent { this.setState({ fadeIn: true }); } - onToggleToolBarView = (item: EditorToolBarView) => { + onToggleToolBarView = (item: EditorToolbarView) => { this.setState({ openView: item, isOpen: !this.state.isOpen, @@ -74,12 +75,15 @@ export class EditorTabBody extends PureComponent { return state; } - renderButton(view: EditorToolBarView) { + renderButton(view: EditorToolbarView) { const onClick = () => { if (view.onClick) { view.onClick(); } - this.onToggleToolBarView(view); + + if (view.render) { + this.onToggleToolBarView(view); + } }; return ( @@ -91,7 +95,7 @@ export class EditorTabBody extends PureComponent { ); } - renderOpenView(view: EditorToolBarView) { + renderOpenView(view: EditorToolbarView) { return ( {view.render()} diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx index 94719dfe6e0..84e11511453 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -87,7 +87,6 @@ export class PanelChrome extends PureComponent { const { datasource, targets, transparent } = panel; const PanelComponent = plugin.exports.Panel; const containerClassNames = `panel-container panel-container--absolute ${transparent ? 'panel-transparent' : ''}`; - return ( {({ width, height }) => { diff --git a/public/app/features/dashboard/dashgrid/PanelEditor.tsx b/public/app/features/dashboard/dashgrid/PanelEditor.tsx index a746d6c4b91..a09ff66f114 100644 --- a/public/app/features/dashboard/dashgrid/PanelEditor.tsx +++ b/public/app/features/dashboard/dashgrid/PanelEditor.tsx @@ -4,7 +4,7 @@ import classNames from 'classnames'; import { QueriesTab } from './QueriesTab'; import { VisualizationTab } from './VisualizationTab'; import { GeneralTab } from './GeneralTab'; -import { AlertTab } from './AlertTab'; +import { AlertTab } from '../../alerting/AlertTab'; import config from 'app/core/config'; import { store } from 'app/store/store'; @@ -15,7 +15,8 @@ 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'; +import { Themes } from '@grafana/ui/src/components/Tooltip/Popper'; interface PanelEditorProps { panel: PanelModel; @@ -54,7 +55,7 @@ export class PanelEditor extends PureComponent { case 'queries': return ; case 'alert': - return ; + return ; case 'visualization': 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/dashgrid/QueriesTab.tsx b/public/app/features/dashboard/dashgrid/QueriesTab.tsx index 9ad0bb3cadd..77ab64b1dba 100644 --- a/public/app/features/dashboard/dashgrid/QueriesTab.tsx +++ b/public/app/features/dashboard/dashgrid/QueriesTab.tsx @@ -1,10 +1,10 @@ // Libraries -import React, { SFC, PureComponent } from 'react'; +import React, { PureComponent, SFC } from 'react'; import _ from 'lodash'; // Components -import './../../panel/metrics_tab'; -import { EditorTabBody } from './EditorTabBody'; +import 'app/features/panel/metrics_tab'; +import { EditorTabBody, EditorToolbarView} from './EditorTabBody'; import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker'; import { QueryInspector } from './QueryInspector'; import { QueryOptions } from './QueryOptions'; @@ -13,14 +13,14 @@ import { PanelOptionSection } from './PanelOptionSection'; // Services import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; -import { getBackendSrv, BackendSrv } from 'app/core/services/backend_srv'; -import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader'; +import { BackendSrv, getBackendSrv } from 'app/core/services/backend_srv'; +import { AngularComponent, getAngularLoader } from 'app/core/services/AngularLoader'; import config from 'app/core/config'; // Types import { PanelModel } from '../panel_model'; import { DashboardModel } from '../dashboard_model'; -import { DataSourceSelectItem, DataQuery } from 'app/types'; +import { DataQuery, DataSourceSelectItem } from 'app/types'; import { PluginHelp } from 'app/core/components/PluginHelp/PluginHelp'; interface Props { @@ -50,17 +50,21 @@ export class QueriesTab extends PureComponent { constructor(props) { super(props); - const { panel } = props; this.state = { - currentDS: this.datasources.find(datasource => datasource.value === panel.datasource), isLoadingHelp: false, + currentDS: this.findCurrentDataSource(), helpContent: null, isPickerOpen: false, isAddingMixed: false, }; } + findCurrentDataSource(): DataSourceSelectItem { + const { panel } = this.props; + return this.datasources.find(datasource => datasource.value === panel.datasource) || this.datasources[0]; + } + getAngularQueryComponentScope(): AngularQueryComponentScope { const { panel, dashboard } = this.props; @@ -204,12 +208,12 @@ export class QueriesTab extends PureComponent { const { panel } = this.props; const { currentDS, isAddingMixed } = this.state; - const queryInspector = { + const queryInspector: EditorToolbarView = { title: 'Query Inspector', render: this.renderQueryInspector, }; - const dsHelp = { + const dsHelp: EditorToolbarView = { heading: 'Help', icon: 'fa fa-question', render: this.renderHelp, diff --git a/public/app/features/dashboard/dashgrid/VisualizationTab.tsx b/public/app/features/dashboard/dashgrid/VisualizationTab.tsx index 42d9bf6a6eb..bc7102f35dd 100644 --- a/public/app/features/dashboard/dashgrid/VisualizationTab.tsx +++ b/public/app/features/dashboard/dashgrid/VisualizationTab.tsx @@ -2,10 +2,10 @@ import React, { PureComponent } from 'react'; // Utils & Services -import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader'; +import { AngularComponent, getAngularLoader } from 'app/core/services/AngularLoader'; // Components -import { EditorTabBody } from './EditorTabBody'; +import { EditorTabBody, EditorToolbarView } from './EditorTabBody'; import { VizTypePicker } from './VizTypePicker'; import { PluginHelp } from 'app/core/components/PluginHelp/PluginHelp'; import { FadeIn } from 'app/core/components/Animations/FadeIn'; @@ -206,7 +206,7 @@ export class VisualizationTab extends PureComponent { const { plugin } = this.props; const { isVizPickerOpen, searchQuery } = this.state; - const pluginHelp = { + const pluginHelp: EditorToolbarView = { heading: 'Help', icon: 'fa fa-question', render: this.renderHelp, 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

- - + +
+ +
diff --git a/public/app/features/explore/LogLabels.tsx b/public/app/features/explore/LogLabels.tsx index 8aa1789017e..7675fb13152 100644 --- a/public/app/features/explore/LogLabels.tsx +++ b/public/app/features/explore/LogLabels.tsx @@ -1,4 +1,3 @@ -import _ from 'lodash'; import React, { PureComponent } from 'react'; import classnames from 'classnames'; diff --git a/public/app/features/explore/Typeahead.tsx b/public/app/features/explore/Typeahead.tsx index 721527fbebe..6d6c60c0553 100644 --- a/public/app/features/explore/Typeahead.tsx +++ b/public/app/features/explore/Typeahead.tsx @@ -55,7 +55,7 @@ class TypeaheadItem extends React.PureComponent { interface TypeaheadGroupProps { items: CompletionItem[]; label: string; - onClickItem: (CompletionItem) => void; + onClickItem: (suggestion: CompletionItem) => void; selected: CompletionItem; prefix?: string; } diff --git a/public/app/features/folders/FolderPermissions.tsx b/public/app/features/folders/FolderPermissions.tsx index 176e270038b..62a479b9124 100644 --- a/public/app/features/folders/FolderPermissions.tsx +++ b/public/app/features/folders/FolderPermissions.tsx @@ -2,7 +2,7 @@ import React, { PureComponent } from 'react'; import { hot } from 'react-hot-loader'; import { connect } from 'react-redux'; import PageHeader from 'app/core/components/PageHeader/PageHeader'; -import Tooltip from 'app/core/components/Tooltip/Tooltip'; +import { Tooltip } from '@grafana/ui'; import SlideDown from 'app/core/components/Animations/SlideDown'; import { getNavModel } from 'app/core/selectors/navModel'; import { NavModel, StoreState, FolderState } from 'app/types'; @@ -84,8 +84,10 @@ export class FolderPermissions extends PureComponent {

Folder Permissions

- - + +
+ +