mirror of
https://github.com/grafana/grafana.git
synced 2025-02-15 10:03:33 -06:00
Merge branch 'master' into move-value-formats
This commit is contained in:
commit
cf4f98857a
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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}
|
||||
```
|
||||
|
@ -34,32 +34,29 @@ sudo dpkg -i grafana_<version>_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
|
||||
|
@ -32,7 +32,7 @@ $ sudo yum install <rpm package url>
|
||||
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 <rpm package url>
|
||||
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
|
||||
|
||||
|
@ -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",
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ interface Props {
|
||||
/**
|
||||
* Wraps component into <Scrollbars> component from `react-custom-scrollbars`
|
||||
*/
|
||||
class CustomScrollbar extends PureComponent<Props> {
|
||||
export class CustomScrollbar extends PureComponent<Props> {
|
||||
static defaultProps: Partial<Props> = {
|
||||
customClassName: 'custom-scrollbars',
|
||||
autoHide: true,
|
||||
@ -28,8 +28,10 @@ class CustomScrollbar extends PureComponent<Props> {
|
||||
<Scrollbars
|
||||
className={customClassName}
|
||||
autoHeight={true}
|
||||
autoHeightMin={'inherit'}
|
||||
autoHeightMax={'inherit'}
|
||||
// These autoHeightMin & autoHeightMax options affect firefox and chrome differently.
|
||||
// Before these where set to inhert but that caused problems with cut of legends in firefox
|
||||
autoHeightMin={'0'}
|
||||
autoHeightMax={'100%'}
|
||||
renderTrackHorizontal={props => <div {...props} className="track-horizontal" />}
|
||||
renderTrackVertical={props => <div {...props} className="track-vertical" />}
|
||||
renderThumbHorizontal={props => <div {...props} className="thumb-horizontal" />}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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,
|
@ -6,11 +6,11 @@ interface Props {
|
||||
root?: HTMLElement;
|
||||
}
|
||||
|
||||
export default class BodyPortal extends PureComponent<Props> {
|
||||
export class Portal extends PureComponent<Props> {
|
||||
node: HTMLElement = document.createElement('div');
|
||||
portalRoot: HTMLElement;
|
||||
|
||||
constructor(props) {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
const {
|
||||
className,
|
@ -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<HTMLDivElement> {
|
||||
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<Props> {
|
||||
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 (
|
||||
<Manager>
|
||||
<Reference>
|
||||
{({ ref }) => (
|
||||
<div className={`popper_ref ${refClassName || ''}`} ref={ref}>
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</Reference>
|
||||
<Transition in={show} timeout={100} mountOnEnter={true} unmountOnExit={true}>
|
||||
{transitionState => (
|
||||
<Portal>
|
||||
<ReactPopper placement={placement}>
|
||||
<ReactPopper placement={placement} referenceElement={this.props.referenceElement}>
|
||||
{({ ref, style, placement, arrowProps }) => {
|
||||
return (
|
||||
<div
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
ref={ref}
|
||||
style={{
|
||||
...style,
|
||||
@ -53,7 +58,7 @@ class Popper extends PureComponent<Props> {
|
||||
data-placement={placement}
|
||||
className="popper"
|
||||
>
|
||||
<div className="popper__background">
|
||||
<div className={popperBackgroundClassName}>
|
||||
{renderContent(content)}
|
||||
<div ref={arrowProps.ref} data-placement={placement} className="popper__arrow" />
|
||||
</div>
|
@ -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<Props, State> {
|
||||
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 <ReactComponent />;
|
||||
}
|
||||
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;
|
@ -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(
|
||||
<Tooltip className="test-class" placement="auto" content="Tooltip text">
|
||||
<a href="http://www.grafana.com">Link with tooltip</a>
|
||||
<Tooltip placement="auto" content="Tooltip text">
|
||||
<a className="test-class" href="http://www.grafana.com">
|
||||
Link with tooltip
|
||||
</a>
|
||||
</Tooltip>
|
||||
)
|
||||
.toJSON();
|
32
packages/grafana-ui/src/components/Tooltip/Tooltip.tsx
Normal file
32
packages/grafana-ui/src/components/Tooltip/Tooltip.tsx
Normal file
@ -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<PopperJS.ReferenceObject>();
|
||||
|
||||
return (
|
||||
<PopperController {...controllerProps}>
|
||||
{(showPopper, hidePopper, popperProps) => {
|
||||
return (
|
||||
<>
|
||||
{tooltipTriggerRef.current && (
|
||||
<Popper
|
||||
{...popperProps}
|
||||
onMouseEnter={showPopper}
|
||||
onMouseLeave={hidePopper}
|
||||
referenceElement={tooltipTriggerRef.current}
|
||||
/>
|
||||
)}
|
||||
{React.cloneElement(children, {
|
||||
ref: tooltipTriggerRef,
|
||||
onMouseEnter: showPopper,
|
||||
onMouseLeave: hidePopper,
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</PopperController>
|
||||
);
|
||||
};
|
@ -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;
|
@ -0,0 +1,12 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Tooltip renders correctly 1`] = `
|
||||
<a
|
||||
className="test-class"
|
||||
href="http://www.grafana.com"
|
||||
onMouseEnter={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
>
|
||||
Link with tooltip
|
||||
</a>
|
||||
`;
|
@ -1 +1,3 @@
|
||||
@import 'CustomScrollbar/CustomScrollbar';
|
||||
@import 'DeleteButton/DeleteButton';
|
||||
@import 'Tooltip/Tooltip';
|
||||
|
@ -1 +1,4 @@
|
||||
export { DeleteButton } from './DeleteButton/DeleteButton';
|
||||
export { Tooltip } from './Tooltip/Tooltip';
|
||||
export { Portal } from './Portal/Portal';
|
||||
export { CustomScrollbar } from './CustomScrollbar/CustomScrollbar';
|
||||
|
@ -98,6 +98,7 @@ export class Graph extends PureComponent<GraphProps> {
|
||||
$.plot(this.element, timeSeries, flotOptions);
|
||||
} catch (err) {
|
||||
console.log('Graph rendering error', err, flotOptions, timeSeries);
|
||||
throw new Error('Error rendering panel');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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"),
|
||||
|
@ -24,12 +24,14 @@ class EmptyListCTA extends Component<Props, any> {
|
||||
<i className={buttonIcon} />
|
||||
{buttonTitle}
|
||||
</a>
|
||||
<div className="empty-list-cta__pro-tip">
|
||||
<i className="fa fa-rocket" /> ProTip: {proTip}
|
||||
<a className="text-link empty-list-cta__pro-tip-link" href={proTipLink} target={proTipTarget}>
|
||||
{proTipLinkTitle}
|
||||
</a>
|
||||
</div>
|
||||
{proTip && (
|
||||
<div className="empty-list-cta__pro-tip">
|
||||
<i className="fa fa-rocket" /> ProTip: {proTip}
|
||||
<a className="text-link empty-list-cta__pro-tip-link" href={proTipLink} target={proTipTarget}>
|
||||
{proTipLinkTitle}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
44
public/app/core/components/ErrorBoundary/ErrorBoundary.tsx
Normal file
44
public/app/core/components/ErrorBoundary/ErrorBoundary.tsx
Normal file
@ -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<Props, State> {
|
||||
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;
|
@ -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 => {
|
||||
<span className={`gf-form-label width-${props.width ? props.width : '10'}`}>
|
||||
<span>{props.children}</span>
|
||||
{props.tooltip && (
|
||||
<Tooltip className="gf-form-help-icon--right-normal" placement="auto" content={props.tooltip}>
|
||||
<i className="gicon gicon-question gicon--has-hover" />
|
||||
<Tooltip placement="auto" content={props.tooltip}>
|
||||
<div className="gf-form-help-icon--right-normal">
|
||||
<i className="gicon gicon-question gicon--has-hover" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</span>
|
||||
|
@ -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<Props, any> {
|
||||
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 (
|
||||
<div className="baron baron__root baron__clipper">
|
||||
<div className={this.props.className + ' baron__scroller'} ref={this.handleRef}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
|
||||
<div className="baron__track">
|
||||
<div className="baron__bar" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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(
|
||||
<Popover className="test-class" placement="auto" content="Popover text">
|
||||
<button>Button with Popover</button>
|
||||
</Popover>
|
||||
)
|
||||
.toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
});
|
@ -1,19 +0,0 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import Popper from './Popper';
|
||||
import withPopper, { UsingPopperProps } from './withPopper';
|
||||
|
||||
class Popover extends PureComponent<UsingPopperProps> {
|
||||
render() {
|
||||
const { children, hidePopper, showPopper, className, ...restProps } = this.props;
|
||||
|
||||
const togglePopper = restProps.show ? hidePopper : showPopper;
|
||||
|
||||
return (
|
||||
<div className={`popper__manager ${className}`} onClick={togglePopper}>
|
||||
<Popper {...restProps}>{children}</Popper>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withPopper(Popover);
|
@ -1,17 +0,0 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import Popper from './Popper';
|
||||
import withPopper, { UsingPopperProps } from './withPopper';
|
||||
|
||||
class Tooltip extends PureComponent<UsingPopperProps> {
|
||||
render() {
|
||||
const { children, hidePopper, showPopper, className, ...restProps } = this.props;
|
||||
|
||||
return (
|
||||
<div className={`popper__manager ${className}`} onMouseEnter={showPopper} onMouseLeave={hidePopper}>
|
||||
<Popper {...restProps}>{children}</Popper>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withPopper(Tooltip);
|
@ -1,16 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Popover renders correctly 1`] = `
|
||||
<div
|
||||
className="popper__manager test-class"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<div
|
||||
className="popper_ref "
|
||||
>
|
||||
<button>
|
||||
Button with Popover
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
@ -1,19 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Tooltip renders correctly 1`] = `
|
||||
<div
|
||||
className="popper__manager test-class"
|
||||
onMouseEnter={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
>
|
||||
<div
|
||||
className="popper_ref "
|
||||
>
|
||||
<a
|
||||
href="http://www.grafana.com"
|
||||
>
|
||||
Link with tooltip
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
@ -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<Props, State> {
|
||||
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 <ReactComponent />;
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { show, placement } = this.state;
|
||||
const className = this.props.className || '';
|
||||
|
||||
return (
|
||||
<WrappedComponent
|
||||
{...this.props}
|
||||
showPopper={this.showPopper}
|
||||
hidePopper={this.hidePopper}
|
||||
renderContent={this.renderContent}
|
||||
show={show}
|
||||
placement={placement}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
141
public/app/features/alerting/AlertTab.tsx
Normal file
141
public/app/features/alerting/AlertTab.tsx
Normal file
@ -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<Props> {
|
||||
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 = '<alert-tab />';
|
||||
|
||||
const scopeProps = {
|
||||
ctrl: this.panelCtrl,
|
||||
};
|
||||
|
||||
this.component = loader.load(this.element, scopeProps, template);
|
||||
}
|
||||
|
||||
stateHistory = (): EditorToolbarView => {
|
||||
return {
|
||||
title: 'State history',
|
||||
render: () => {
|
||||
return (
|
||||
<StateHistory
|
||||
dashboard={this.props.dashboard}
|
||||
panelId={this.props.panel.id}
|
||||
onRefresh={this.panelCtrl.refresh}
|
||||
/>
|
||||
);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
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 (
|
||||
<EditorTabBody heading="Alert" toolbarItems={toolbarItems}>
|
||||
<>
|
||||
<div ref={element => (this.element = element)} />
|
||||
{!alert && <EmptyListCTA model={model} />}
|
||||
</>
|
||||
</EditorTabBody>
|
||||
);
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
110
public/app/features/alerting/StateHistory.tsx
Normal file
110
public/app/features/alerting/StateHistory.tsx
Normal file
@ -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<Props, State> {
|
||||
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 (
|
||||
<div>
|
||||
{stateHistoryItems.length > 0 && (
|
||||
<div className="p-b-1">
|
||||
<span className="muted">Last 50 state changes</span>
|
||||
<button className="btn btn-mini btn-danger pull-right" onClick={this.clearHistory}>
|
||||
<i className="fa fa-trash" /> {` Clear history`}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<ol className="alert-rule-list">
|
||||
{stateHistoryItems.length > 0 ? (
|
||||
stateHistoryItems.map((item, index) => {
|
||||
return (
|
||||
<li className="alert-rule-item" key={`${item.time}-${index}`}>
|
||||
<div className={`alert-rule-item__icon ${item.stateModel.stateClass}`}>
|
||||
<i className={item.stateModel.iconClass} />
|
||||
</div>
|
||||
<div className="alert-rule-item__body">
|
||||
<div className="alert-rule-item__header">
|
||||
<p className="alert-rule-item__name">{item.alertName}</p>
|
||||
<div className="alert-rule-item__text">
|
||||
<span className={`${item.stateModel.stateClass}`}>{item.stateModel.text}</span>
|
||||
</div>
|
||||
</div>
|
||||
{item.info}
|
||||
</div>
|
||||
<div className="alert-rule-item__time">{item.time}</div>
|
||||
</li>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<i>No state changes recorded</i>
|
||||
)}
|
||||
</ol>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default StateHistory;
|
@ -1,191 +1,168 @@
|
||||
<div class="panel-option-section__body" ng-if="ctrl.alert">
|
||||
<div class="edit-tab-with-sidemenu">
|
||||
<aside class="edit-sidemenu-aside">
|
||||
<ul class="edit-sidemenu">
|
||||
<li ng-class="{active: ctrl.subTabIndex === 0}">
|
||||
<a ng-click="ctrl.changeTabIndex(0)">Alert Config</a>
|
||||
</li>
|
||||
<li ng-class="{active: ctrl.subTabIndex === 1}">
|
||||
<a ng-click="ctrl.changeTabIndex(1)">
|
||||
Notifications <span class="muted">({{ctrl.alertNotifications.length}})</span>
|
||||
</a>
|
||||
</li>
|
||||
<li ng-class="{active: ctrl.subTabIndex === 2}">
|
||||
<a ng-click="ctrl.changeTabIndex(2)">State history</a>
|
||||
</li>
|
||||
<li>
|
||||
<a ng-click="ctrl.delete()">Delete</a>
|
||||
</li>
|
||||
</ul>
|
||||
</aside>
|
||||
<div ng-if="ctrl.panel.alert">
|
||||
<div class="alert alert-error m-b-2" ng-show="ctrl.error">
|
||||
<i class="fa fa-warning"></i> {{ctrl.error}}
|
||||
</div>
|
||||
<div class="panel-option-section">
|
||||
<div class="panel-option-section__body">
|
||||
<div class="gf-form-group">
|
||||
<h4 class="section-heading">Rule</h4>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-6">Name</span>
|
||||
<input type="text" class="gf-form-input width-20" ng-model="ctrl.alert.name">
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-9">Evaluate every</span>
|
||||
<input class="gf-form-input max-width-6" type="text" ng-model="ctrl.alert.frequency">
|
||||
</div>
|
||||
<div class="gf-form max-width-11">
|
||||
<label class="gf-form-label width-5">For</label>
|
||||
<input type="text" class="gf-form-input max-width-6" ng-model="ctrl.alert.for"
|
||||
spellcheck='false' placeholder="5m">
|
||||
<info-popover mode="right-absolute">
|
||||
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.
|
||||
</info-popover>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="edit-tab-content">
|
||||
<div ng-if="ctrl.subTabIndex === 0">
|
||||
<div class="alert alert-error m-b-2" ng-show="ctrl.error">
|
||||
<i class="fa fa-warning"></i> {{ctrl.error}}
|
||||
</div>
|
||||
<div class="gf-form-group">
|
||||
<h4 class="section-heading">Conditions</h4>
|
||||
<div class="gf-form-inline" ng-repeat="conditionModel in ctrl.conditionModels">
|
||||
<div class="gf-form">
|
||||
<metric-segment-model css-class="query-keyword width-5" ng-if="$index"
|
||||
property="conditionModel.operator.type" options="ctrl.evalOperators"
|
||||
custom="false"></metric-segment-model>
|
||||
<span class="gf-form-label query-keyword width-5" ng-if="$index===0">WHEN</span>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<query-part-editor class="gf-form-label query-part width-9"
|
||||
part="conditionModel.reducerPart"
|
||||
handle-event="ctrl.handleReducerPartEvent(conditionModel, $event)">
|
||||
</query-part-editor>
|
||||
<span class="gf-form-label query-keyword">OF</span>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<query-part-editor class="gf-form-label query-part" part="conditionModel.queryPart"
|
||||
handle-event="ctrl.handleQueryPartEvent(conditionModel, $event)">
|
||||
</query-part-editor>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<metric-segment-model property="conditionModel.evaluator.type" options="ctrl.evalFunctions"
|
||||
custom="false" css-class="query-keyword"
|
||||
on-change="ctrl.evaluatorTypeChanged(conditionModel.evaluator)"></metric-segment-model>
|
||||
<input class="gf-form-input max-width-9" type="number" step="any"
|
||||
ng-hide="conditionModel.evaluator.params.length === 0"
|
||||
ng-model="conditionModel.evaluator.params[0]"
|
||||
ng-change="ctrl.evaluatorParamsChanged()" />
|
||||
<label class="gf-form-label query-keyword"
|
||||
ng-show="conditionModel.evaluator.params.length === 2">TO</label>
|
||||
<input class="gf-form-input max-width-9" type="number" step="any"
|
||||
ng-if="conditionModel.evaluator.params.length === 2"
|
||||
ng-model="conditionModel.evaluator.params[1]"
|
||||
ng-change="ctrl.evaluatorParamsChanged()" />
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label">
|
||||
<a class="pointer" tabindex="1" ng-click="ctrl.removeCondition($index)">
|
||||
<i class="fa fa-trash"></i>
|
||||
</a>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group">
|
||||
<h5 class="section-heading">Alert Config</h5>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-6">Name</span>
|
||||
<input type="text" class="gf-form-input width-20" ng-model="ctrl.alert.name">
|
||||
</div>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-9">Evaluate every</span>
|
||||
<input class="gf-form-input max-width-6" type="text" ng-model="ctrl.alert.frequency">
|
||||
</div>
|
||||
<div class="gf-form max-width-11">
|
||||
<label class="gf-form-label width-5">For</label>
|
||||
<input type="text" class="gf-form-input max-width-6" ng-model="ctrl.alert.for" spellcheck='false' placeholder="5m">
|
||||
<info-popover mode="right-absolute">
|
||||
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.
|
||||
</info-popover>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label dropdown">
|
||||
<a class="pointer dropdown-toggle" data-toggle="dropdown">
|
||||
<i class="fa fa-plus"></i>
|
||||
</a>
|
||||
<ul class="dropdown-menu" role="menu">
|
||||
<li ng-repeat="ct in ctrl.conditionTypes" role="menuitem">
|
||||
<a ng-click="ctrl.addCondition(ct.value);">{{ct.text}}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group">
|
||||
<h5 class="section-heading">Conditions</h5>
|
||||
<div class="gf-form-inline" ng-repeat="conditionModel in ctrl.conditionModels">
|
||||
<div class="gf-form">
|
||||
<metric-segment-model css-class="query-keyword width-5" ng-if="$index" property="conditionModel.operator.type" options="ctrl.evalOperators" custom="false"></metric-segment-model>
|
||||
<span class="gf-form-label query-keyword width-5" ng-if="$index===0">WHEN</span>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<query-part-editor class="gf-form-label query-part width-9" part="conditionModel.reducerPart" handle-event="ctrl.handleReducerPartEvent(conditionModel, $event)">
|
||||
</query-part-editor>
|
||||
<span class="gf-form-label query-keyword">OF</span>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<query-part-editor class="gf-form-label query-part" part="conditionModel.queryPart" handle-event="ctrl.handleQueryPartEvent(conditionModel, $event)">
|
||||
</query-part-editor>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<metric-segment-model property="conditionModel.evaluator.type" options="ctrl.evalFunctions" custom="false" css-class="query-keyword" on-change="ctrl.evaluatorTypeChanged(conditionModel.evaluator)"></metric-segment-model>
|
||||
<input class="gf-form-input max-width-9" type="number" step="any" ng-hide="conditionModel.evaluator.params.length === 0" ng-model="conditionModel.evaluator.params[0]" ng-change="ctrl.evaluatorParamsChanged()">
|
||||
<label class="gf-form-label query-keyword" ng-show="conditionModel.evaluator.params.length === 2">TO</label>
|
||||
<input class="gf-form-input max-width-9" type="number" step="any" ng-if="conditionModel.evaluator.params.length === 2" ng-model="conditionModel.evaluator.params[1]" ng-change="ctrl.evaluatorParamsChanged()">
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label">
|
||||
<a class="pointer" tabindex="1" ng-click="ctrl.removeCondition($index)">
|
||||
<i class="fa fa-trash"></i>
|
||||
</a>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form-group">
|
||||
<h4 class="section-heading">No Data & Error Handling</h4>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-15">If no data or all values are null</span>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label query-keyword">SET STATE TO</span>
|
||||
<div class="gf-form-select-wrapper">
|
||||
<select class="gf-form-input" ng-model="ctrl.alert.noDataState"
|
||||
ng-options="f.value as f.text for f in ctrl.noDataModes">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label dropdown">
|
||||
<a class="pointer dropdown-toggle" data-toggle="dropdown">
|
||||
<i class="fa fa-plus"></i>
|
||||
</a>
|
||||
<ul class="dropdown-menu" role="menu">
|
||||
<li ng-repeat="ct in ctrl.conditionTypes" role="menuitem">
|
||||
<a ng-click="ctrl.addCondition(ct.value);">{{ct.text}}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-15">If execution error or timeout</span>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label query-keyword">SET STATE TO</span>
|
||||
<div class="gf-form-select-wrapper">
|
||||
<select class="gf-form-input" ng-model="ctrl.alert.executionErrorState"
|
||||
ng-options="f.value as f.text for f in ctrl.executionErrorModes">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-18">If no data or all values are null</span>
|
||||
<span class="gf-form-label query-keyword">SET STATE TO</span>
|
||||
<div class="gf-form-select-wrapper">
|
||||
<select class="gf-form-input" ng-model="ctrl.alert.noDataState" ng-options="f.value as f.text for f in ctrl.noDataModes">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form-button-row">
|
||||
<button class="btn btn-inverse" ng-click="ctrl.test()">
|
||||
Test Rule
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-18">If execution error or timeout</span>
|
||||
<span class="gf-form-label query-keyword">SET STATE TO</span>
|
||||
<div class="gf-form-select-wrapper">
|
||||
<select class="gf-form-input" ng-model="ctrl.alert.executionErrorState" ng-options="f.value as f.text for f in ctrl.executionErrorModes">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form-group" ng-if="ctrl.testing">
|
||||
Evaluating rule <i class="fa fa-spinner fa-spin"></i>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-button-row">
|
||||
<button class="btn btn-inverse" ng-click="ctrl.test()">
|
||||
Test Rule
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form-group" ng-if="ctrl.testResult">
|
||||
<json-tree root-name="result" object="ctrl.testResult" start-expanded="true"></json-tree>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group" ng-if="ctrl.testing">
|
||||
Evaluating rule <i class="fa fa-spinner fa-spin"></i>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group" ng-if="ctrl.testResult">
|
||||
<json-tree root-name="result" object="ctrl.testResult" start-expanded="true"></json-tree>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group" ng-if="ctrl.subTabIndex === 1">
|
||||
<h5 class="section-heading">Notifications</h5>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form max-width-30">
|
||||
<span class="gf-form-label width-8">Send to</span>
|
||||
<span class="gf-form-label" ng-repeat="nc in ctrl.alertNotifications" ng-style="{'background-color': nc.bgColor }">
|
||||
<i class="{{nc.iconClass}}"></i> {{nc.name}}
|
||||
<i class="fa fa-remove pointer muted" ng-click="ctrl.removeNotification($index)" ng-if="nc.isDefault === false"></i>
|
||||
</span>
|
||||
<metric-segment segment="ctrl.addNotificationSegment" get-options="ctrl.getNotifications()" on-change="ctrl.notificationAdded()"></metric-segment>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form gf-form--v-stretch">
|
||||
<span class="gf-form-label width-8">Message</span>
|
||||
<textarea class="gf-form-input" rows="10" ng-model="ctrl.alert.message" placeholder="Notification message details..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group" style="max-width: 720px;" ng-if="ctrl.subTabIndex === 2">
|
||||
<button class="btn btn-mini btn-danger pull-right" ng-click="ctrl.clearHistory()"><i class="fa fa-trash"></i> Clear history</button>
|
||||
<h5 class="section-heading" style="whitespace: nowrap">
|
||||
State history <span class="muted small">(last 50 state changes)</span>
|
||||
</h5>
|
||||
|
||||
<div ng-show="ctrl.alertHistory.length === 0">
|
||||
<br>
|
||||
<i>No state changes recorded</i>
|
||||
</div>
|
||||
|
||||
<ol class="alert-rule-list" >
|
||||
<li class="alert-rule-item" ng-repeat="al in ctrl.alertHistory">
|
||||
<div class="alert-rule-item__icon {{al.stateModel.stateClass}}">
|
||||
<i class="{{al.stateModel.iconClass}}"></i>
|
||||
</div>
|
||||
<div class="alert-rule-item__body">
|
||||
<div class="alert-rule-item__header">
|
||||
<div class="alert-rule-item__text">
|
||||
<span class="{{al.stateModel.stateClass}}">{{al.stateModel.text}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="alert-list-info">{{al.info}}</span>
|
||||
</div>
|
||||
<div class="alert-rule-item__time">
|
||||
<span>{{al.time}}</span>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group p-t-4 p-b-4" ng-if="!ctrl.alert">
|
||||
<div class="empty-list-cta">
|
||||
<div class="empty-list-cta__title">Panel has no alert rule defined</div>
|
||||
<button class="empty-list-cta__button btn btn-xlarge btn-success" ng-click="ctrl.enable()">
|
||||
<i class="icon-gf icon-gf-alert"></i>
|
||||
Create Alert
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-option-section">
|
||||
<div class="panel-option-section__header">Notifications</div>
|
||||
<div class="panel-option-section__body">
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-8">Send to</span>
|
||||
</div>
|
||||
<div class="gf-form" ng-repeat="nc in ctrl.alertNotifications">
|
||||
<span class="gf-form-label" ng-style="{'background-color': nc.bgColor }">
|
||||
<i class="{{nc.iconClass}}"></i> {{nc.name}}
|
||||
<i class="fa fa-remove pointer muted" ng-click="ctrl.removeNotification($index)" ng-if="nc.isDefault === false"></i>
|
||||
</span>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<metric-segment segment="ctrl.addNotificationSegment"
|
||||
get-options="ctrl.getNotifications()"
|
||||
on-change="ctrl.notificationAdded()"></metric-segment>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form gf-form--v-stretch">
|
||||
<span class="gf-form-label width-8">Message</span>
|
||||
<textarea class="gf-form-input" rows="10" ng-model="ctrl.alert.message"
|
||||
placeholder="Notification message details..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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<Props> {
|
||||
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 = '<alert-tab />';
|
||||
|
||||
const scopeProps = {
|
||||
ctrl: panelCtrl,
|
||||
};
|
||||
|
||||
this.component = loader.load(this.element, scopeProps, template);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<EditorTabBody heading="Alert" toolbarItems={[]}>
|
||||
<div ref={element => (this.element = element)} />
|
||||
</EditorTabBody>
|
||||
);
|
||||
}
|
||||
}
|
@ -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<Props, State> {
|
||||
|
||||
this.state = {
|
||||
loading: LoadingState.NotStarted,
|
||||
errorMessage: '',
|
||||
response: {
|
||||
data: [],
|
||||
},
|
||||
@ -90,7 +98,7 @@ export class DataPanel extends Component<Props, State> {
|
||||
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<Props, State> {
|
||||
});
|
||||
} 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<Props, State> {
|
||||
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<Props, State> {
|
||||
|
||||
return (
|
||||
<>
|
||||
{this.renderLoadingSpinner()}
|
||||
{this.props.children({
|
||||
timeSeries,
|
||||
loading,
|
||||
})}
|
||||
{this.renderLoadingStates()}
|
||||
<ErrorBoundary>
|
||||
{({ error, errorInfo }) => {
|
||||
if (errorInfo) {
|
||||
this.onError(error.message || DEFAULT_PLUGIN_ERROR);
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{this.props.children({
|
||||
timeSeries,
|
||||
loading,
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</ErrorBoundary>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
private renderLoadingSpinner(): JSX.Element {
|
||||
const { loading } = this.state;
|
||||
|
||||
private renderLoadingStates(): JSX.Element {
|
||||
const { loading, errorMessage } = this.state;
|
||||
if (loading === LoadingState.Loading) {
|
||||
return (
|
||||
<div className="panel-loading">
|
||||
<i className="fa fa-spinner fa-spin" />
|
||||
</div>
|
||||
);
|
||||
} else if (loading === LoadingState.Error) {
|
||||
return (
|
||||
<Tooltip content={errorMessage} placement="bottom-start" theme={Themes.Error}>
|
||||
<div className="panel-info-corner panel-info-corner--error">
|
||||
<i className="fa" />
|
||||
<span className="panel-info-corner-inner" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
@ -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;
|
||||
|
@ -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<Props, State> {
|
||||
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<Props, State> {
|
||||
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<Props, State> {
|
||||
);
|
||||
}
|
||||
|
||||
renderOpenView(view: EditorToolBarView) {
|
||||
renderOpenView(view: EditorToolbarView) {
|
||||
return (
|
||||
<PanelOptionSection title={view.title || view.heading} onClose={this.onCloseOpenView}>
|
||||
{view.render()}
|
||||
|
@ -87,7 +87,6 @@ export class PanelChrome extends PureComponent<Props, State> {
|
||||
const { datasource, targets, transparent } = panel;
|
||||
const PanelComponent = plugin.exports.Panel;
|
||||
const containerClassNames = `panel-container panel-container--absolute ${transparent ? 'panel-transparent' : ''}`;
|
||||
|
||||
return (
|
||||
<AutoSizer>
|
||||
{({ width, height }) => {
|
||||
|
@ -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<PanelEditorProps> {
|
||||
case 'queries':
|
||||
return <QueriesTab panel={panel} dashboard={dashboard} />;
|
||||
case 'alert':
|
||||
return <AlertTab angularPanel={angularPanel} />;
|
||||
return <AlertTab angularPanel={angularPanel} dashboard={dashboard} panel={panel} />;
|
||||
case 'visualization':
|
||||
return (
|
||||
<VisualizationTab
|
||||
@ -138,7 +139,7 @@ function TabItem({ tab, activeTab, onClick }: TabItemParams) {
|
||||
return (
|
||||
<div className="panel-editor-tabs__item" onClick={() => onClick(tab)}>
|
||||
<a className={tabClasses}>
|
||||
<Tooltip content={`${tab.text}`} className="popper__manager--block" placement="auto">
|
||||
<Tooltip content={`${tab.text}`} placement="auto" theme={Themes.Brand}>
|
||||
<i className={`gicon gicon-${tab.id}${activeTab === tab.id ? '-active' : ''}`} />
|
||||
</Tooltip>
|
||||
</a>
|
||||
|
@ -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<Props> {
|
||||
{infoMode === InfoModes.Info || infoMode === InfoModes.Links ? (
|
||||
<Tooltip
|
||||
content={this.getInfoContent}
|
||||
className="popper__manager--block"
|
||||
refClassName={`panel-info-corner panel-info-corner--${infoMode.toLowerCase()}`}
|
||||
placement="bottom-start"
|
||||
>
|
||||
<i className="fa" />
|
||||
<span className="panel-info-corner-inner" />
|
||||
<div
|
||||
className={`panel-info-corner panel-info-corner--${infoMode.toLowerCase()}`}
|
||||
>
|
||||
<i className="fa" />
|
||||
<span className="panel-info-corner-inner" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</>
|
||||
|
@ -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<Props, State> {
|
||||
|
||||
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<Props, State> {
|
||||
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,
|
||||
|
@ -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<Props, State> {
|
||||
const { plugin } = this.props;
|
||||
const { isVizPickerOpen, searchQuery } = this.state;
|
||||
|
||||
const pluginHelp = {
|
||||
const pluginHelp: EditorToolbarView = {
|
||||
heading: 'Help',
|
||||
icon: 'fa fa-question',
|
||||
render: this.renderHelp,
|
||||
|
@ -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<Props, State> {
|
||||
<div className="dashboard-settings__header">
|
||||
<div className="page-action-bar">
|
||||
<h3 className="d-inline-block">Permissions</h3>
|
||||
<Tooltip className="page-sub-heading-icon" placement="auto" content={PermissionsInfo}>
|
||||
<i className="gicon gicon-question gicon--has-hover" />
|
||||
<Tooltip placement="auto" content={PermissionsInfo}>
|
||||
<div className="page-sub-heading-icon">
|
||||
<i className="gicon gicon-question gicon--has-hover" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
<div className="page-action-bar__spacer" />
|
||||
<button className="btn btn-success pull-right" onClick={this.onOpenAddPermissions} disabled={isAdding}>
|
||||
|
@ -16,7 +16,7 @@ const BasicSettings: SFC<Props> = ({ dataSourceName, isDefault, onDefaultChange,
|
||||
<div className="gf-form max-width-30" style={{ marginRight: '3px' }}>
|
||||
<Label
|
||||
tooltip={
|
||||
'The name is used when you select the data source in panels. The Default data source is' +
|
||||
'The name is used when you select the data source in panels. The Default data source is ' +
|
||||
'preselected in new panels.'
|
||||
}
|
||||
>
|
||||
|
@ -16,7 +16,7 @@ exports[`Render should render component 1`] = `
|
||||
}
|
||||
>
|
||||
<Component
|
||||
tooltip="The name is used when you select the data source in panels. The Default data source ispreselected in new panels."
|
||||
tooltip="The name is used when you select the data source in panels. The Default data source is preselected in new panels."
|
||||
>
|
||||
Name
|
||||
</Component>
|
||||
|
@ -5,7 +5,7 @@ import { LayoutModes } from '../../../core/components/LayoutSelector/LayoutSelec
|
||||
const initialState: DataSourcesState = {
|
||||
dataSources: [] as DataSource[],
|
||||
dataSource: {} as DataSource,
|
||||
layoutMode: LayoutModes.Grid,
|
||||
layoutMode: LayoutModes.List,
|
||||
searchQuery: '',
|
||||
dataSourcesCount: 0,
|
||||
dataSourceTypes: [] as Plugin[],
|
||||
|
@ -944,7 +944,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
<div className="navbar-buttons relative">
|
||||
<button className="btn navbar-button navbar-button--primary" onClick={this.onSubmit}>
|
||||
Run Query{' '}
|
||||
{loading ? <i className="fa fa-spinner fa-spin run-icon" /> : <i className="fa fa-level-down run-icon" />}
|
||||
{loading ? <i className="fa fa-spinner fa-fw fa-spin run-icon" /> : <i className="fa fa-level-down fa-fw run-icon" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,4 +1,3 @@
|
||||
import _ from 'lodash';
|
||||
import React, { PureComponent } from 'react';
|
||||
import classnames from 'classnames';
|
||||
|
||||
|
@ -55,7 +55,7 @@ class TypeaheadItem extends React.PureComponent<TypeaheadItemProps> {
|
||||
interface TypeaheadGroupProps {
|
||||
items: CompletionItem[];
|
||||
label: string;
|
||||
onClickItem: (CompletionItem) => void;
|
||||
onClickItem: (suggestion: CompletionItem) => void;
|
||||
selected: CompletionItem;
|
||||
prefix?: string;
|
||||
}
|
||||
|
@ -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<Props, State> {
|
||||
<div className="page-container page-body">
|
||||
<div className="page-action-bar">
|
||||
<h3 className="page-sub-heading">Folder Permissions</h3>
|
||||
<Tooltip className="page-sub-heading-icon" placement="auto" content={PermissionsInfo}>
|
||||
<i className="gicon gicon-question gicon--has-hover" />
|
||||
<Tooltip placement="auto" content={PermissionsInfo}>
|
||||
<div className="page-sub-heading-icon">
|
||||
<i className="gicon gicon-question gicon--has-hover" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
<div className="page-action-bar__spacer" />
|
||||
<button className="btn btn-success pull-right" onClick={this.onOpenAddPermissions} disabled={isAdding}>
|
||||
|
@ -1,6 +1,3 @@
|
||||
// Libraries
|
||||
import _ from 'lodash';
|
||||
|
||||
// Services & utils
|
||||
import coreModule from 'app/core/core_module';
|
||||
import { Emitter } from 'app/core/utils/emitter';
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import SlideDown from 'app/core/components/Animations/SlideDown';
|
||||
import Tooltip from 'app/core/components/Tooltip/Tooltip';
|
||||
import { Tooltip } from '@grafana/ui';
|
||||
import { TeamGroup } from '../../types';
|
||||
import { addTeamGroup, loadTeamGroups, removeTeamGroup } from './state/actions';
|
||||
import { getTeamGroups } from './state/selectors';
|
||||
@ -77,8 +77,10 @@ export class TeamGroupSync extends PureComponent<Props, State> {
|
||||
<div>
|
||||
<div className="page-action-bar">
|
||||
<h3 className="page-sub-heading">External group sync</h3>
|
||||
<Tooltip className="page-sub-heading-icon" placement="auto" content={headerTooltip}>
|
||||
<i className="gicon gicon-question gicon--has-hover" />
|
||||
<Tooltip placement="auto" content={headerTooltip}>
|
||||
<div className="page-sub-heading-icon">
|
||||
<i className="gicon gicon-question gicon--has-hover" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
<div className="page-action-bar__spacer" />
|
||||
{groups.length > 0 && (
|
||||
|
@ -60,6 +60,7 @@ export class TeamSettings extends React.Component<Props, State> {
|
||||
onChange={this.onChangeName}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="gf-form max-width-30">
|
||||
<Label tooltip="This is optional and is primarily used to set the team profile avatar (via gravatar service)">
|
||||
Email
|
||||
|
@ -10,15 +10,18 @@ exports[`Render should render component 1`] = `
|
||||
>
|
||||
External group sync
|
||||
</h3>
|
||||
<class_1
|
||||
className="page-sub-heading-icon"
|
||||
<Component
|
||||
content="Sync LDAP or OAuth groups with your Grafana teams."
|
||||
placement="auto"
|
||||
>
|
||||
<i
|
||||
className="gicon gicon-question gicon--has-hover"
|
||||
/>
|
||||
</class_1>
|
||||
<div
|
||||
className="page-sub-heading-icon"
|
||||
>
|
||||
<i
|
||||
className="gicon gicon-question gicon--has-hover"
|
||||
/>
|
||||
</div>
|
||||
</Component>
|
||||
<div
|
||||
className="page-action-bar__spacer"
|
||||
/>
|
||||
@ -116,15 +119,18 @@ exports[`Render should render groups table 1`] = `
|
||||
>
|
||||
External group sync
|
||||
</h3>
|
||||
<class_1
|
||||
className="page-sub-heading-icon"
|
||||
<Component
|
||||
content="Sync LDAP or OAuth groups with your Grafana teams."
|
||||
placement="auto"
|
||||
>
|
||||
<i
|
||||
className="gicon gicon-question gicon--has-hover"
|
||||
/>
|
||||
</class_1>
|
||||
<div
|
||||
className="page-sub-heading-icon"
|
||||
>
|
||||
<i
|
||||
className="gicon gicon-question gicon--has-hover"
|
||||
/>
|
||||
</div>
|
||||
</Component>
|
||||
<div
|
||||
className="page-action-bar__spacer"
|
||||
/>
|
||||
|
@ -308,7 +308,7 @@ export default class InfluxDatasource {
|
||||
return 'now()';
|
||||
}
|
||||
|
||||
const parts = /^now-(\d+)([d|h|m|s])$/.exec(date);
|
||||
const parts = /^now-(\d+)([dhms])$/.exec(date);
|
||||
if (parts) {
|
||||
const amount = parseInt(parts[1], 10);
|
||||
const unit = parts[2];
|
||||
|
@ -1,4 +1,3 @@
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import Cascader from 'rc-cascader';
|
||||
import PluginPrism from 'slate-prism';
|
||||
|
@ -151,32 +151,32 @@ export const aggOptions = [
|
||||
{
|
||||
text: 'mean',
|
||||
value: 'REDUCE_MEAN',
|
||||
valueTypes: [ValueTypes.INT64, ValueTypes.DOUBLE, ValueTypes.MONEY],
|
||||
valueTypes: [ValueTypes.INT64, ValueTypes.DOUBLE, ValueTypes.MONEY, ValueTypes.DISTRIBUTION],
|
||||
metricKinds: [MetricKind.GAUGE, MetricKind.DELTA],
|
||||
},
|
||||
{
|
||||
text: 'min',
|
||||
value: 'REDUCE_MIN',
|
||||
valueTypes: [ValueTypes.INT64, ValueTypes.DOUBLE, ValueTypes.MONEY],
|
||||
metricKinds: [MetricKind.GAUGE, MetricKind.DELTA],
|
||||
metricKinds: [MetricKind.GAUGE, MetricKind.DELTA, MetricKind.CUMULATIVE, MetricKind.METRIC_KIND_UNSPECIFIED],
|
||||
},
|
||||
{
|
||||
text: 'max',
|
||||
value: 'REDUCE_MAX',
|
||||
valueTypes: [ValueTypes.INT64, ValueTypes.DOUBLE, ValueTypes.MONEY],
|
||||
metricKinds: [MetricKind.GAUGE, MetricKind.DELTA],
|
||||
metricKinds: [MetricKind.GAUGE, MetricKind.DELTA, MetricKind.CUMULATIVE, MetricKind.METRIC_KIND_UNSPECIFIED],
|
||||
},
|
||||
{
|
||||
text: 'sum',
|
||||
value: 'REDUCE_SUM',
|
||||
valueTypes: [ValueTypes.INT64, ValueTypes.DOUBLE, ValueTypes.MONEY, ValueTypes.DISTRIBUTION],
|
||||
metricKinds: [MetricKind.GAUGE, MetricKind.DELTA],
|
||||
metricKinds: [MetricKind.GAUGE, MetricKind.DELTA, MetricKind.CUMULATIVE, MetricKind.METRIC_KIND_UNSPECIFIED],
|
||||
},
|
||||
{
|
||||
text: 'std. dev.',
|
||||
value: 'REDUCE_STDDEV',
|
||||
valueTypes: [ValueTypes.INT64, ValueTypes.DOUBLE, ValueTypes.MONEY, ValueTypes.DISTRIBUTION],
|
||||
metricKinds: [MetricKind.GAUGE, MetricKind.DELTA],
|
||||
metricKinds: [MetricKind.GAUGE, MetricKind.DELTA, MetricKind.CUMULATIVE, MetricKind.METRIC_KIND_UNSPECIFIED],
|
||||
},
|
||||
{
|
||||
text: 'count',
|
||||
|
@ -16,7 +16,6 @@ export default class StackdriverDatasource {
|
||||
constructor(instanceSettings, private backendSrv, private templateSrv, private timeSrv) {
|
||||
this.baseUrl = `/stackdriver/`;
|
||||
this.url = instanceSettings.url;
|
||||
this.doRequest = this.doRequest;
|
||||
this.id = instanceSettings.id;
|
||||
this.projectName = instanceSettings.jsonData.defaultProject || '';
|
||||
this.authenticationType = instanceSettings.jsonData.authenticationType || 'jwt';
|
||||
|
@ -88,7 +88,6 @@ export class StackdriverQueryCtrl extends QueryCtrl {
|
||||
try {
|
||||
jsonBody = JSON.parse(queryRes.error);
|
||||
} catch {
|
||||
this.lastQueryError = queryRes.error;
|
||||
}
|
||||
|
||||
this.lastQueryError = jsonBody.error.message;
|
||||
|
@ -1,9 +1,10 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { Switch } from 'app/core/components/Switch/Switch';
|
||||
import { OptionModuleProps } from './module';
|
||||
import { Label } from '../../../core/components/Label/Label';
|
||||
import { PanelOptionsProps } from '@grafana/ui';
|
||||
import { Options } from './types';
|
||||
|
||||
export default class GaugeOptions extends PureComponent<OptionModuleProps> {
|
||||
export default class GaugeOptions extends PureComponent<PanelOptionsProps<Options>> {
|
||||
onToggleThresholdLabels = () =>
|
||||
this.props.onChange({ ...this.props.options, showThresholdLabels: !this.props.options.showThresholdLabels });
|
||||
|
||||
@ -15,7 +16,8 @@ export default class GaugeOptions extends PureComponent<OptionModuleProps> {
|
||||
onMaxValueChange = ({ target }) => this.props.onChange({ ...this.props.options, maxValue: target.value });
|
||||
|
||||
render() {
|
||||
const { maxValue, minValue, showThresholdLabels, showThresholdMarkers } = this.props.options;
|
||||
const { options } = this.props;
|
||||
const { maxValue, minValue, showThresholdLabels, showThresholdMarkers } = options;
|
||||
|
||||
return (
|
||||
<div className="section gf-form-group">
|
||||
|
20
public/app/plugins/panel/gauge/GaugePanel.tsx
Normal file
20
public/app/plugins/panel/gauge/GaugePanel.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { PanelProps, NullValueMode } from '@grafana/ui';
|
||||
import { getTimeSeriesVMs } from 'app/viz/state/timeSeries';
|
||||
import Gauge from 'app/viz/Gauge';
|
||||
import { Options } from './types';
|
||||
|
||||
interface Props extends PanelProps<Options> {}
|
||||
|
||||
export class GaugePanel extends PureComponent<Props> {
|
||||
render() {
|
||||
const { timeSeries, width, height } = this.props;
|
||||
|
||||
const vmSeries = getTimeSeriesVMs({
|
||||
timeSeries: timeSeries,
|
||||
nullValueMode: NullValueMode.Ignore,
|
||||
});
|
||||
|
||||
return <Gauge timeSeries={vmSeries} {...this.props.options} width={width} height={height} />;
|
||||
}
|
||||
}
|
46
public/app/plugins/panel/gauge/GaugePanelOptions.tsx
Normal file
46
public/app/plugins/panel/gauge/GaugePanelOptions.tsx
Normal file
@ -0,0 +1,46 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import ValueOptions from 'app/plugins/panel/gauge/ValueOptions';
|
||||
import Thresholds from 'app/plugins/panel/gauge/Thresholds';
|
||||
import { BasicGaugeColor } from 'app/types';
|
||||
import { PanelOptionsProps } from '@grafana/ui';
|
||||
import ValueMappings from 'app/plugins/panel/gauge/ValueMappings';
|
||||
import { Options } from './types';
|
||||
import GaugeOptions from './GaugeOptions';
|
||||
|
||||
export const defaultProps = {
|
||||
options: {
|
||||
baseColor: BasicGaugeColor.Green,
|
||||
minValue: 0,
|
||||
maxValue: 100,
|
||||
prefix: '',
|
||||
showThresholdMarkers: true,
|
||||
showThresholdLabels: false,
|
||||
suffix: '',
|
||||
decimals: 0,
|
||||
stat: 'avg',
|
||||
unit: 'none',
|
||||
mappings: [],
|
||||
thresholds: [],
|
||||
},
|
||||
};
|
||||
|
||||
export default class GaugePanelOptions extends PureComponent<PanelOptionsProps<Options>> {
|
||||
static defaultProps = defaultProps;
|
||||
|
||||
render() {
|
||||
const { onChange, options } = this.props;
|
||||
return (
|
||||
<>
|
||||
<div className="form-section">
|
||||
<ValueOptions onChange={onChange} options={options} />
|
||||
<GaugeOptions onChange={onChange} options={options} />
|
||||
<Thresholds onChange={onChange} options={options} />
|
||||
</div>
|
||||
|
||||
<div className="form-section">
|
||||
<ValueMappings onChange={onChange} options={options} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
@ -1,12 +1,13 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import Thresholds from './Thresholds';
|
||||
import { defaultProps, OptionsProps } from './module';
|
||||
import { defaultProps } from './GaugePanelOptions';
|
||||
import { BasicGaugeColor } from 'app/types';
|
||||
import { PanelOptionsProps } from '@grafana/ui';
|
||||
import { Options } from './types';
|
||||
|
||||
const setup = (propOverrides?: object) => {
|
||||
const props: PanelOptionsProps<OptionsProps> = {
|
||||
const props: PanelOptionsProps<Options> = {
|
||||
onChange: jest.fn(),
|
||||
options: {
|
||||
...defaultProps.options,
|
||||
|
@ -1,15 +1,16 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { ColorPicker } from 'app/core/components/colorpicker/ColorPicker';
|
||||
import { OptionModuleProps } from './module';
|
||||
import { BasicGaugeColor, Threshold } from 'app/types';
|
||||
import { PanelOptionsProps } from '@grafana/ui';
|
||||
import { Options } from './types';
|
||||
|
||||
interface State {
|
||||
thresholds: Threshold[];
|
||||
baseColor: string;
|
||||
}
|
||||
|
||||
export default class Thresholds extends PureComponent<OptionModuleProps, State> {
|
||||
export default class Thresholds extends PureComponent<PanelOptionsProps<Options>, State> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
|
@ -1,11 +1,13 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import ValueMappings from './ValueMappings';
|
||||
import { defaultProps, OptionModuleProps } from './module';
|
||||
import { MappingType } from 'app/types';
|
||||
import { PanelOptionsProps } from '@grafana/ui';
|
||||
import { Options } from './types';
|
||||
import { defaultProps } from 'app/plugins/panel/gauge/GaugePanelOptions';
|
||||
|
||||
const setup = (propOverrides?: object) => {
|
||||
const props: OptionModuleProps = {
|
||||
const props: PanelOptionsProps<Options> = {
|
||||
onChange: jest.fn(),
|
||||
options: {
|
||||
...defaultProps.options,
|
||||
|
@ -1,14 +1,15 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import MappingRow from './MappingRow';
|
||||
import { OptionModuleProps } from './module';
|
||||
import { MappingType, RangeMap, ValueMap } from 'app/types';
|
||||
import { PanelOptionsProps } from '@grafana/ui';
|
||||
import { Options } from './types';
|
||||
|
||||
interface State {
|
||||
mappings: Array<ValueMap | RangeMap>;
|
||||
nextIdToAdd: number;
|
||||
}
|
||||
|
||||
export default class ValueMappings extends PureComponent<OptionModuleProps, State> {
|
||||
export default class ValueMappings extends PureComponent<PanelOptionsProps<Options>, State> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
|
@ -2,7 +2,8 @@ import React, { PureComponent } from 'react';
|
||||
import { Label } from 'app/core/components/Label/Label';
|
||||
import Select from 'app/core/components/Select/Select';
|
||||
import UnitPicker from 'app/core/components/Select/UnitPicker';
|
||||
import { OptionModuleProps } from './module';
|
||||
import { PanelOptionsProps } from '@grafana/ui';
|
||||
import { Options } from './types';
|
||||
|
||||
const statOptions = [
|
||||
{ value: 'min', label: 'Min' },
|
||||
@ -20,7 +21,7 @@ const statOptions = [
|
||||
|
||||
const labelWidth = 6;
|
||||
|
||||
export default class ValueOptions extends PureComponent<OptionModuleProps> {
|
||||
export default class ValueOptions extends PureComponent<PanelOptionsProps<Options>> {
|
||||
onUnitChange = unit => this.props.onChange({ ...this.props.options, unit: unit.value });
|
||||
|
||||
onStatChange = stat => this.props.onChange({ ...this.props.options, stat: stat.value });
|
||||
|
@ -1,83 +1,4 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import Gauge from 'app/viz/Gauge';
|
||||
import { getTimeSeriesVMs } from 'app/viz/state/timeSeries';
|
||||
import ValueOptions from './ValueOptions';
|
||||
import GaugeOptions from './GaugeOptions';
|
||||
import Thresholds from './Thresholds';
|
||||
import ValueMappings from './ValueMappings';
|
||||
import { PanelOptionsProps, PanelProps, NullValueMode } from '@grafana/ui';
|
||||
import { BasicGaugeColor, RangeMap, Threshold, ValueMap } from 'app/types';
|
||||
import GaugePanelOptions, { defaultProps } from './GaugePanelOptions';
|
||||
import { GaugePanel } from './GaugePanel';
|
||||
|
||||
export interface OptionsProps {
|
||||
baseColor: string;
|
||||
decimals: number;
|
||||
mappings: Array<RangeMap | ValueMap>;
|
||||
maxValue: number;
|
||||
minValue: number;
|
||||
prefix: string;
|
||||
showThresholdLabels: boolean;
|
||||
showThresholdMarkers: boolean;
|
||||
stat: string;
|
||||
suffix: string;
|
||||
thresholds: Threshold[];
|
||||
unit: string;
|
||||
}
|
||||
|
||||
export interface OptionModuleProps {
|
||||
onChange: (item: any) => void;
|
||||
options: OptionsProps;
|
||||
}
|
||||
|
||||
export const defaultProps = {
|
||||
options: {
|
||||
baseColor: BasicGaugeColor.Green,
|
||||
minValue: 0,
|
||||
maxValue: 100,
|
||||
prefix: '',
|
||||
showThresholdMarkers: true,
|
||||
showThresholdLabels: false,
|
||||
suffix: '',
|
||||
decimals: 0,
|
||||
stat: '',
|
||||
unit: '',
|
||||
mappings: [],
|
||||
thresholds: [],
|
||||
},
|
||||
};
|
||||
|
||||
interface Props extends PanelProps<OptionsProps> {}
|
||||
|
||||
class GaugePanel extends PureComponent<Props> {
|
||||
render() {
|
||||
const { timeSeries, width, height } = this.props;
|
||||
|
||||
const vmSeries = getTimeSeriesVMs({
|
||||
timeSeries: timeSeries,
|
||||
nullValueMode: NullValueMode.Ignore,
|
||||
});
|
||||
|
||||
return <Gauge timeSeries={vmSeries} {...this.props.options} width={width} height={height} />;
|
||||
}
|
||||
}
|
||||
|
||||
class Options extends PureComponent<PanelOptionsProps<OptionsProps>> {
|
||||
static defaultProps = defaultProps;
|
||||
|
||||
render() {
|
||||
const { onChange, options } = this.props;
|
||||
return (
|
||||
<div>
|
||||
<div className="form-section">
|
||||
<ValueOptions onChange={onChange} options={options} />
|
||||
<GaugeOptions onChange={onChange} options={options} />
|
||||
<Thresholds onChange={onChange} options={options} />
|
||||
</div>
|
||||
<div className="form-section">
|
||||
<ValueMappings onChange={onChange} options={options} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { GaugePanel as Panel, Options as PanelOptions, defaultProps as PanelDefaults };
|
||||
export { GaugePanel as Panel, GaugePanelOptions as PanelOptions, defaultProps as PanelDefaults };
|
||||
|
16
public/app/plugins/panel/gauge/types.ts
Normal file
16
public/app/plugins/panel/gauge/types.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { RangeMap, ValueMap, Threshold } from 'app/types';
|
||||
|
||||
export interface Options {
|
||||
baseColor: string;
|
||||
decimals: number;
|
||||
mappings: Array<RangeMap | ValueMap>;
|
||||
maxValue: number;
|
||||
minValue: number;
|
||||
prefix: string;
|
||||
showThresholdLabels: boolean;
|
||||
showThresholdMarkers: boolean;
|
||||
stat: string;
|
||||
suffix: string;
|
||||
thresholds: Threshold[];
|
||||
unit: string;
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import _ from 'lodash';
|
||||
import React, { PureComponent } from 'react';
|
||||
import { TimeSeries } from 'app/core/core';
|
||||
import CustomScrollbar from 'app/core/components/CustomScrollbar/CustomScrollbar';
|
||||
import { CustomScrollbar } from '@grafana/ui';
|
||||
import { LegendItem, LEGEND_STATS } from './LegendSeriesItem';
|
||||
|
||||
interface LegendProps {
|
||||
|
@ -16,10 +16,6 @@ import { Options } from './types';
|
||||
interface Props extends PanelProps<Options> {}
|
||||
|
||||
export class GraphPanel extends PureComponent<Props> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { timeSeries, timeRange, width, height } = this.props;
|
||||
const { showLines, showBars, showPoints } = this.props.options;
|
||||
|
@ -9,7 +9,7 @@ import { Switch } from 'app/core/components/Switch/Switch';
|
||||
import { PanelOptionsProps } from '@grafana/ui';
|
||||
import { Options } from './types';
|
||||
|
||||
export class GraphOptions extends PureComponent<PanelOptionsProps<Options>> {
|
||||
export class GraphPanelOptions extends PureComponent<PanelOptionsProps<Options>> {
|
||||
onToggleLines = () => {
|
||||
this.props.onChange({ ...this.props.options, showLines: !this.props.options.showLines });
|
||||
};
|
@ -1,4 +1,4 @@
|
||||
import { GraphPanel } from './GraphPanel';
|
||||
import { GraphOptions } from './GraphOptions';
|
||||
import { GraphPanelOptions } from './GraphPanelOptions';
|
||||
|
||||
export { GraphPanel as Panel, GraphOptions as PanelOptions };
|
||||
export { GraphPanel as Panel, GraphPanelOptions as PanelOptions };
|
||||
|
@ -251,7 +251,6 @@ export class HeatmapRenderer {
|
||||
if (tickInterval === 0) {
|
||||
yMax = max * this.dataRangeWidingFactor;
|
||||
yMin = min - min * (this.dataRangeWidingFactor - 1);
|
||||
tickInterval = (yMax - yMin) / 2;
|
||||
} else {
|
||||
yMax = Math.ceil((max + yWiding) / tickInterval) * tickInterval;
|
||||
yMin = Math.floor((min - yWiding) / tickInterval) * tickInterval;
|
||||
@ -389,9 +388,7 @@ export class HeatmapRenderer {
|
||||
|
||||
// Adjust data range to log base
|
||||
adjustLogRange(min, max, logBase) {
|
||||
let yMin, yMax;
|
||||
|
||||
yMin = this.data.heatmapStats.minLog;
|
||||
let yMin = this.data.heatmapStats.minLog;
|
||||
if (this.data.heatmapStats.minLog > 1 || !this.data.heatmapStats.minLog) {
|
||||
yMin = 1;
|
||||
} else {
|
||||
@ -399,7 +396,7 @@ export class HeatmapRenderer {
|
||||
}
|
||||
|
||||
// Adjust max Y value to log base
|
||||
yMax = this.adjustLogMax(this.data.heatmapStats.max, logBase);
|
||||
const yMax = this.adjustLogMax(this.data.heatmapStats.max, logBase);
|
||||
|
||||
return { yMin, yMax };
|
||||
}
|
||||
|
55
public/app/viz/Gauge.test.tsx
Normal file
55
public/app/viz/Gauge.test.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { Gauge, Props } from './Gauge';
|
||||
import { BasicGaugeColor } from '../types';
|
||||
import { TimeSeriesVMs } from '@grafana/ui';
|
||||
|
||||
jest.mock('jquery', () => ({
|
||||
plot: jest.fn(),
|
||||
}));
|
||||
|
||||
const setup = (propOverrides?: object) => {
|
||||
const props: Props = {
|
||||
baseColor: BasicGaugeColor.Green,
|
||||
maxValue: 100,
|
||||
mappings: [],
|
||||
minValue: 0,
|
||||
prefix: '',
|
||||
showThresholdMarkers: true,
|
||||
showThresholdLabels: false,
|
||||
suffix: '',
|
||||
thresholds: [],
|
||||
unit: 'none',
|
||||
stat: 'avg',
|
||||
height: 300,
|
||||
width: 300,
|
||||
timeSeries: {} as TimeSeriesVMs,
|
||||
decimals: 0,
|
||||
};
|
||||
|
||||
Object.assign(props, propOverrides);
|
||||
|
||||
const wrapper = shallow(<Gauge {...props} />);
|
||||
const instance = wrapper.instance() as Gauge;
|
||||
|
||||
return {
|
||||
instance,
|
||||
wrapper,
|
||||
};
|
||||
};
|
||||
|
||||
describe('Get font color', () => {
|
||||
it('should get base color if no threshold', () => {
|
||||
const { instance } = setup();
|
||||
|
||||
expect(instance.getFontColor(40)).toEqual(BasicGaugeColor.Green);
|
||||
});
|
||||
|
||||
it('should be f2f2f2', () => {
|
||||
const { instance } = setup({
|
||||
thresholds: [{ value: 59, color: '#f2f2f2' }],
|
||||
});
|
||||
|
||||
expect(instance.getFontColor(58)).toEqual('#f2f2f2');
|
||||
});
|
||||
});
|
@ -5,7 +5,7 @@ import { TimeSeriesVMs } from '@grafana/ui';
|
||||
import config from '../core/config';
|
||||
import kbn from '../core/utils/kbn';
|
||||
|
||||
interface Props {
|
||||
export interface Props {
|
||||
baseColor: string;
|
||||
decimals: number;
|
||||
height: number;
|
||||
@ -96,12 +96,14 @@ export class Gauge extends PureComponent<Props> {
|
||||
getFontColor(value) {
|
||||
const { baseColor, maxValue, thresholds } = this.props;
|
||||
|
||||
const atThreshold = thresholds.filter(threshold => value <= threshold.value);
|
||||
if (thresholds.length > 0) {
|
||||
const atThreshold = thresholds.filter(threshold => value <= threshold.value);
|
||||
|
||||
if (atThreshold.length > 0) {
|
||||
return atThreshold[0].color;
|
||||
} else if (value <= maxValue) {
|
||||
return BasicGaugeColor.Red;
|
||||
if (atThreshold.length > 0) {
|
||||
return atThreshold[0].color;
|
||||
} else if (value <= maxValue) {
|
||||
return BasicGaugeColor.Red;
|
||||
}
|
||||
}
|
||||
|
||||
return baseColor;
|
||||
|
@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns="http://www.w3.org/1999/xhtml">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
|
@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns="http://www.w3.org/1999/xhtml">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
|
@ -97,7 +97,6 @@
|
||||
@import 'components/page_header';
|
||||
@import 'components/dashboard_settings';
|
||||
@import 'components/empty_list_cta';
|
||||
@import 'components/popper';
|
||||
@import 'components/form_select_box';
|
||||
@import 'components/panel_editor';
|
||||
@import 'components/toolbar';
|
||||
|
@ -103,6 +103,7 @@ $panel-bg: #212124;
|
||||
$panel-border-color: $dark-1;
|
||||
$panel-border: solid 1px $panel-border-color;
|
||||
$panel-header-hover-bg: $dark-4;
|
||||
$panel-corner: $panel-bg;
|
||||
|
||||
// page header
|
||||
$page-header-bg: linear-gradient(90deg, #292a2d, black);
|
||||
@ -302,12 +303,15 @@ $popover-error-bg: $btn-danger-bg;
|
||||
// Tooltips and popovers
|
||||
// -------------------------
|
||||
$tooltipColor: $popover-help-color;
|
||||
$tooltipBackground: $popover-help-bg;
|
||||
$tooltipArrowWidth: 5px;
|
||||
$tooltipArrowColor: $tooltipBackground;
|
||||
$tooltipLinkColor: $link-color;
|
||||
$graph-tooltip-bg: $dark-1;
|
||||
|
||||
$tooltipBackground: $popover-help-bg;
|
||||
$tooltipArrowColor: $tooltipBackground;
|
||||
$tooltipBackgroundError: $brand-danger;
|
||||
$tooltipBackgroundBrand: $brand-primary;
|
||||
|
||||
// images
|
||||
$checkboxImageUrl: '../img/checkbox.png';
|
||||
|
||||
|
@ -76,8 +76,7 @@ $textShadow: none;
|
||||
|
||||
// gradients
|
||||
$brand-gradient: linear-gradient(to right, rgba(255, 213, 0, 1) 0%, rgba(255, 68, 0, 1) 99%, rgba(255, 68, 0, 1) 100%);
|
||||
$page-gradient: linear-gradient(-60deg, $gray-7, #f5f6f9 70%, $gray-7 98%);
|
||||
//$page-gradient: linear-gradient(180deg, $white 10px, $gray-7 100px);
|
||||
$page-gradient: linear-gradient(180deg, $white 10px, $gray-7 100px);
|
||||
$edit-gradient: linear-gradient(-60deg, $gray-7, #f5f6f9 70%, $gray-7 98%);
|
||||
|
||||
// Links
|
||||
@ -102,6 +101,7 @@ $panel-bg: $white;
|
||||
$panel-border-color: $gray-5;
|
||||
$panel-border: solid 1px $panel-border-color;
|
||||
$panel-header-hover-bg: $gray-6;
|
||||
$panel-corner: $gray-4;
|
||||
|
||||
// Page header
|
||||
$page-header-bg: linear-gradient(90deg, $white, $gray-7);
|
||||
@ -307,12 +307,15 @@ $popover-error-bg: $btn-danger-bg;
|
||||
// Tooltips and popovers
|
||||
// -------------------------
|
||||
$tooltipColor: $popover-help-color;
|
||||
$tooltipBackground: $popover-help-bg;
|
||||
$tooltipArrowWidth: 5px;
|
||||
$tooltipArrowColor: $tooltipBackground;
|
||||
$tooltipLinkColor: lighten($popover-help-color, 5%);
|
||||
$graph-tooltip-bg: $gray-5;
|
||||
|
||||
$tooltipBackground: $popover-help-bg;
|
||||
$tooltipArrowColor: $tooltipBackground; // Used by Angular tooltip
|
||||
$tooltipBackgroundError: $brand-danger;
|
||||
$tooltipBackgroundBrand: $brand-primary;
|
||||
|
||||
// images
|
||||
$checkboxImageUrl: '../img/checkbox_white.png';
|
||||
|
||||
|
@ -295,50 +295,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Custom styles for 'react-custom-scrollbars'
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
.scroll-margin-helper {
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
@ -23,7 +23,9 @@
|
||||
left: 20px;
|
||||
right: 20px;
|
||||
top: $navbarHeight;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
left: auto;
|
||||
width: 550px;
|
||||
}
|
||||
|
||||
|
@ -107,6 +107,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
@ -214,7 +214,7 @@ div.flot-text {
|
||||
|
||||
&--info {
|
||||
display: block;
|
||||
@include panel-corner-color(lighten($panel-bg, 4%));
|
||||
@include panel-corner-color(lighten($panel-corner, 4%));
|
||||
.fa:before {
|
||||
content: '\f129';
|
||||
}
|
||||
@ -222,7 +222,7 @@ div.flot-text {
|
||||
|
||||
&--links {
|
||||
display: block;
|
||||
@include panel-corner-color(lighten($panel-bg, 4%));
|
||||
@include panel-corner-color(lighten($panel-corner, 4%));
|
||||
.fa {
|
||||
left: 4px;
|
||||
}
|
||||
@ -233,7 +233,7 @@ div.flot-text {
|
||||
|
||||
&--error {
|
||||
display: block;
|
||||
color: $text-color;
|
||||
color: $white;
|
||||
@include panel-corner-color($popover-error-bg);
|
||||
.fa:before {
|
||||
content: '\f12a';
|
||||
|
@ -160,7 +160,7 @@
|
||||
}
|
||||
|
||||
.run-icon {
|
||||
margin-left: 0.5em;
|
||||
margin-left: 0.25em;
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,25 @@
|
||||
FROM circleci/golang:1.11
|
||||
|
||||
RUN git clone https://github.com/aptly-dev/aptly $GOPATH/src/github.com/aptly-dev/aptly && \
|
||||
cd $GOPATH/src/github.com/aptly-dev/aptly && \
|
||||
# pin aptly to a specific commit after 1.3.0 that contains gpg2 support
|
||||
git reset --hard a64807efdaf5e380bfa878c71bc88eae10d62be1 && \
|
||||
make install
|
||||
|
||||
FROM circleci/python:2.7-stretch
|
||||
|
||||
RUN sudo pip install awscli && \
|
||||
curl https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-sdk-222.0.0-linux-x86_64.tar.gz | \
|
||||
sudo tar xvzf - -C /opt
|
||||
ENV PATH=$PATH:/opt/google-cloud-sdk/bin
|
||||
|
||||
USER root
|
||||
|
||||
RUN pip install awscli && \
|
||||
curl https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-sdk-222.0.0-linux-x86_64.tar.gz | \
|
||||
tar xvzf - -C /opt && \
|
||||
apt update && \
|
||||
apt install -y createrepo expect && \
|
||||
apt-get autoremove -y && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY --from=0 /go/bin/aptly /usr/local/bin/aptly
|
||||
|
||||
USER circleci
|
||||
|
@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
|
||||
_version="1.0.0"
|
||||
_version="1.1.0"
|
||||
_tag="grafana/grafana-ci-deploy:${_version}"
|
||||
|
||||
docker build -t $_tag .
|
||||
|
7
scripts/build/load-signing-key.sh
Normal file
7
scripts/build/load-signing-key.sh
Normal file
@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
git clone git@github.com:torkelo/private.git ~/private-repo
|
||||
gpg --batch --allow-secret-key-import --import ~/private-repo/signing/private.key
|
||||
pkill gpg-agent
|
27
scripts/build/update_repo/aptly.conf
Normal file
27
scripts/build/update_repo/aptly.conf
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"rootDir": "/deb-repo/db",
|
||||
"downloadConcurrency": 4,
|
||||
"downloadSpeedLimit": 0,
|
||||
"architectures": [],
|
||||
"dependencyFollowSuggests": false,
|
||||
"dependencyFollowRecommends": false,
|
||||
"dependencyFollowAllVariants": false,
|
||||
"dependencyFollowSource": false,
|
||||
"dependencyVerboseResolve": false,
|
||||
"gpgDisableSign": false,
|
||||
"gpgDisableVerify": false,
|
||||
"gpgProvider": "gpg2",
|
||||
"downloadSourcePackages": false,
|
||||
"skipLegacyPool": true,
|
||||
"ppaDistributorID": "ubuntu",
|
||||
"ppaCodename": "",
|
||||
"skipContentsPublishing": false,
|
||||
"FileSystemPublishEndpoints": {
|
||||
"repo": {
|
||||
"rootDir": "/deb-repo/repo",
|
||||
"linkMethod": "copy"
|
||||
}
|
||||
},
|
||||
"S3PublishEndpoints": {},
|
||||
"SwiftPublishEndpoints": {}
|
||||
}
|
7
scripts/build/update_repo/sign-rpm-repo.sh
Executable file
7
scripts/build/update_repo/sign-rpm-repo.sh
Executable file
@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env expect
|
||||
|
||||
set password [lindex $argv 0]
|
||||
spawn gpg --detach-sign --armor /rpm-repo/repodata/repomd.xml
|
||||
expect "Enter passphrase: "
|
||||
send -- "$password\r"
|
||||
expect eof
|
7
scripts/build/update_repo/unlock-gpg-key.sh
Executable file
7
scripts/build/update_repo/unlock-gpg-key.sh
Executable file
@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env expect
|
||||
|
||||
set password [lindex $argv 0]
|
||||
spawn gpg --detach-sign --armor /tmp/sign-this
|
||||
expect "Enter passphrase: "
|
||||
send -- "$password\r"
|
||||
expect eof
|
58
scripts/build/update_repo/update-deb.sh
Executable file
58
scripts/build/update_repo/update-deb.sh
Executable file
@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
RELEASE_TYPE="${1:-}"
|
||||
GPG_PASS="${2:-}"
|
||||
RELEASE_TAG="${3:-}"
|
||||
REPO="grafana"
|
||||
|
||||
if [ -z "$RELEASE_TYPE" -o -z "$GPG_PASS" ]; then
|
||||
echo "Both RELEASE_TYPE (arg 1) and GPG_PASS (arg 2) has to be set"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$RELEASE_TYPE" != "oss" && "$RELEASE_TYPE" != "enterprise" ]]; then
|
||||
echo "RELEASE_TYPE (arg 1) must be either oss or enterprise."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if echo "$RELEASE_TAG" | grep -q "beta"; then
|
||||
REPO="beta"
|
||||
fi
|
||||
|
||||
set -e
|
||||
|
||||
# Setup environment
|
||||
cp scripts/build/update_repo/aptly.conf /etc/aptly.conf
|
||||
mkdir -p /deb-repo/db \
|
||||
/deb-repo/repo \
|
||||
/deb-repo/tmp
|
||||
|
||||
# Download the database
|
||||
gsutil -m rsync -r "gs://grafana-aptly-db/$RELEASE_TYPE" /deb-repo/db
|
||||
|
||||
# Add the new release to the repo
|
||||
aptly publish drop grafana filesystem:repo:grafana || true
|
||||
aptly publish drop beta filesystem:repo:grafana || true
|
||||
cp ./dist/*.deb /deb-repo/tmp
|
||||
rm /deb-repo/tmp/grafana_latest*.deb || true
|
||||
aptly repo add "$REPO" ./dist
|
||||
|
||||
# Setup signing and sign the repo
|
||||
|
||||
echo "allow-loopback-pinentry" > ~/.gnupg/gpg-agent.conf
|
||||
echo "pinentry-mode loopback" > ~/.gnupg/gpg.conf
|
||||
|
||||
touch /tmp/sign-this
|
||||
./scripts/build/update_repo/unlock-gpg-key.sh "$GPG_PASS"
|
||||
rm /tmp/sign-this /tmp/sign-this.asc
|
||||
|
||||
aptly publish repo grafana filesystem:repo:grafana
|
||||
aptly publish repo beta filesystem:repo:grafana
|
||||
|
||||
# Update the repo and db on gcp
|
||||
gsutil -m rsync -r -d /deb-repo/db "gs://grafana-aptly-db/$RELEASE_TYPE"
|
||||
gsutil -m rsync -r -d /deb-repo/repo/grafana "gs://grafana-repo/$RELEASE_TYPE/deb"
|
||||
|
||||
# usage:
|
||||
#
|
||||
# deb https://packages.grafana.com/oss/deb stable main
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user