Merge branch 'master' into move-value-formats

This commit is contained in:
Peter Holmberg 2019-01-09 21:13:53 +00:00
commit cf4f98857a
104 changed files with 1374 additions and 893 deletions

View File

@ -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

View File

@ -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)

View File

@ -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}
```

View File

@ -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

View File

@ -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

View File

@ -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",

View File

@ -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"
}
}

View File

@ -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" />}

View File

@ -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;
}
}

View File

@ -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,

View File

@ -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,

View File

@ -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>

View File

@ -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;

View File

@ -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();

View 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>
);
};

View File

@ -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;

View File

@ -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>
`;

View File

@ -1 +1,3 @@
@import 'CustomScrollbar/CustomScrollbar';
@import 'DeleteButton/DeleteButton';
@import 'Tooltip/Tooltip';

View File

@ -1 +1,4 @@
export { DeleteButton } from './DeleteButton/DeleteButton';
export { Tooltip } from './Tooltip/Tooltip';
export { Portal } from './Portal/Portal';
export { CustomScrollbar } from './CustomScrollbar/CustomScrollbar';

View File

@ -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');
}
}

View File

@ -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()

View File

@ -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
}

View File

@ -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"),

View File

@ -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>
);
}

View 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;

View File

@ -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>

View File

@ -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>
);
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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();
});
});

View File

@ -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);

View File

@ -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);

View File

@ -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>
`;

View File

@ -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>
`;

View File

@ -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}
/>
);
}
};
}

View 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>
);
}
}

View File

@ -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);

View 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;

View File

@ -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>&nbsp;{{nc.name}}&nbsp;
<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>&nbsp;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>&nbsp;{{nc.name}}&nbsp;
<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>

View File

@ -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>
);
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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()}

View File

@ -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 }) => {

View File

@ -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>

View File

@ -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}
</>

View File

@ -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,

View File

@ -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,

View File

@ -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}>

View File

@ -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.'
}
>

View File

@ -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>

View File

@ -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[],

View File

@ -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>

View File

@ -1,4 +1,3 @@
import _ from 'lodash';
import React, { PureComponent } from 'react';
import classnames from 'classnames';

View File

@ -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;
}

View File

@ -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}>

View File

@ -1,6 +1,3 @@
// Libraries
import _ from 'lodash';
// Services & utils
import coreModule from 'app/core/core_module';
import { Emitter } from 'app/core/utils/emitter';

View File

@ -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 && (

View File

@ -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

View File

@ -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"
/>

View File

@ -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];

View File

@ -1,4 +1,3 @@
import _ from 'lodash';
import React from 'react';
import Cascader from 'rc-cascader';
import PluginPrism from 'slate-prism';

View File

@ -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',

View File

@ -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';

View File

@ -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;

View File

@ -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">

View 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} />;
}
}

View 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>
</>
);
}
}

View File

@ -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,

View File

@ -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);

View File

@ -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,

View File

@ -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);

View File

@ -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 });

View File

@ -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 };

View 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;
}

View File

@ -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 {

View File

@ -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;

View File

@ -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 });
};

View File

@ -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 };

View File

@ -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 };
}

View 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');
});
});

View File

@ -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;

View File

@ -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" />

View File

@ -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" />

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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;
}

View File

@ -23,7 +23,9 @@
left: 20px;
right: 20px;
top: $navbarHeight;
@include media-breakpoint-up(md) {
left: auto;
width: 550px;
}

View File

@ -107,6 +107,7 @@
display: flex;
flex-direction: column;
flex-grow: 1;
justify-content: center;
overflow: hidden;
}

View File

@ -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';

View File

@ -160,7 +160,7 @@
}
.run-icon {
margin-left: 0.5em;
margin-left: 0.25em;
transform: rotate(90deg);
}

View File

@ -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

View File

@ -1,6 +1,6 @@
#!/bin/bash
_version="1.0.0"
_version="1.1.0"
_tag="grafana/grafana-ci-deploy:${_version}"
docker build -t $_tag .

View 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

View 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": {}
}

View 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

View 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

View 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