Merge branch '14293-metric-display-names' into reactify-stackdriver

# Conflicts:
#	public/app/plugins/datasource/stackdriver/partials/query.aggregation.html
#	public/app/plugins/datasource/stackdriver/partials/query.editor.html
#	public/app/plugins/datasource/stackdriver/partials/query.filter.html
#	public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts
#	public/app/plugins/datasource/stackdriver/query_ctrl.ts
#	public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts
#	public/app/plugins/datasource/stackdriver/specs/query_aggregation_ctrl.test.ts
This commit is contained in:
Erik Sundell 2019-01-07 13:59:36 +01:00
commit 49144f07e4
44 changed files with 561 additions and 367 deletions

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,23 @@ 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.
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
deb https://packagecloud.io/grafana/testing/debian/ stretch main
```
Then add the [Package Cloud](https://packagecloud.io/grafana) 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,21 +67,15 @@ 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.
```bash
baseurl=https://packagecloud.io/grafana/testing/el/7/$basearch
```
Then install Grafana via the `yum` command.
```bash
@ -91,7 +85,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

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

@ -76,7 +76,7 @@ func (dr *dashboardServiceImpl) buildSaveDashboardCommand(dto *SaveDashboardDTO,
return nil, models.ErrDashboardFolderCannotHaveParent
}
if dash.IsFolder && strings.ToLower(dash.Title) == strings.ToLower(models.RootFolderName) {
if dash.IsFolder && strings.EqualFold(dash.Title, models.RootFolderName) {
return nil, models.ErrDashboardFolderNameExists
}

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

@ -86,11 +86,11 @@ func (m *postgresMacroEngine) evaluateMacro(name string, args []string) (string,
return "", fmt.Errorf("missing time column argument for macro %v", name)
}
return fmt.Sprintf("%s BETWEEN '%s' AND '%s'", args[0], m.timeRange.GetFromAsTimeUTC().Format(time.RFC3339), m.timeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil
return fmt.Sprintf("%s BETWEEN '%s' AND '%s'", args[0], m.timeRange.GetFromAsTimeUTC().Format(time.RFC3339Nano), m.timeRange.GetToAsTimeUTC().Format(time.RFC3339Nano)), nil
case "__timeFrom":
return fmt.Sprintf("'%s'", m.timeRange.GetFromAsTimeUTC().Format(time.RFC3339)), nil
return fmt.Sprintf("'%s'", m.timeRange.GetFromAsTimeUTC().Format(time.RFC3339Nano)), nil
case "__timeTo":
return fmt.Sprintf("'%s'", m.timeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil
return fmt.Sprintf("'%s'", m.timeRange.GetToAsTimeUTC().Format(time.RFC3339Nano)), nil
case "__timeGroup":
if len(args) < 2 {
return "", fmt.Errorf("macro %v needs time column and interval and optional fill value", name)

View File

@ -41,7 +41,7 @@ func TestMacroEngine(t *testing.T) {
sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)")
So(err, ShouldBeNil)
So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339), to.Format(time.RFC3339)))
So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339Nano), to.Format(time.RFC3339Nano)))
})
Convey("interpolate __timeFrom function", func() {
@ -138,7 +138,7 @@ func TestMacroEngine(t *testing.T) {
sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)")
So(err, ShouldBeNil)
So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339), to.Format(time.RFC3339)))
So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339Nano), to.Format(time.RFC3339Nano)))
})
Convey("interpolate __unixEpochFilter function", func() {
@ -158,7 +158,7 @@ func TestMacroEngine(t *testing.T) {
sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)")
So(err, ShouldBeNil)
So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339), to.Format(time.RFC3339)))
So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339Nano), to.Format(time.RFC3339Nano)))
})
Convey("interpolate __unixEpochFilter function", func() {
@ -168,5 +168,22 @@ func TestMacroEngine(t *testing.T) {
So(sql, ShouldEqual, fmt.Sprintf("select time >= %d AND time <= %d", from.Unix(), to.Unix()))
})
})
Convey("Given a time range between 1960-02-01 07:00:00.5 and 1980-02-03 08:00:00.5", func() {
from := time.Date(1960, 2, 1, 7, 0, 0, 500e6, time.UTC)
to := time.Date(1980, 2, 3, 8, 0, 0, 500e6, time.UTC)
timeRange := tsdb.NewTimeRange(strconv.FormatInt(from.UnixNano()/int64(time.Millisecond), 10), strconv.FormatInt(to.UnixNano()/int64(time.Millisecond), 10))
So(from.Format(time.RFC3339Nano), ShouldEqual, "1960-02-01T07:00:00.5Z")
So(to.Format(time.RFC3339Nano), ShouldEqual, "1980-02-03T08:00:00.5Z")
Convey("interpolate __timeFilter function", func() {
sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)")
So(err, ShouldBeNil)
So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339Nano), to.Format(time.RFC3339Nano)))
})
})
})
}

View File

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

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

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

@ -14,7 +14,6 @@ const DEFAULT_EXPLORE_STATE: ExploreState = {
datasourceError: null,
datasourceLoading: null,
datasourceMissing: false,
datasourceName: '',
exploreDatasources: [],
graphInterval: 1000,
history: [],
@ -69,7 +68,7 @@ describe('state functions', () => {
it('returns url parameter value for a state object', () => {
const state = {
...DEFAULT_EXPLORE_STATE,
datasourceName: 'foo',
initialDatasource: 'foo',
range: {
from: 'now-5h',
to: 'now',
@ -94,7 +93,7 @@ describe('state functions', () => {
it('returns url parameter value for a state object', () => {
const state = {
...DEFAULT_EXPLORE_STATE,
datasourceName: 'foo',
initialDatasource: 'foo',
range: {
from: 'now-5h',
to: 'now',
@ -120,7 +119,7 @@ describe('state functions', () => {
it('can parse the serialized state into the original state', () => {
const state = {
...DEFAULT_EXPLORE_STATE,
datasourceName: 'foo',
initialDatasource: 'foo',
range: {
from: 'now - 5h',
to: 'now',
@ -144,7 +143,7 @@ describe('state functions', () => {
const resultState = {
...rest,
datasource: DEFAULT_EXPLORE_STATE.datasource,
datasourceName: datasource,
initialDatasource: datasource,
initialQueries: queries,
};

View File

@ -105,7 +105,7 @@ export function parseUrlState(initial: string | undefined): ExploreUrlState {
export function serializeStateToUrlParam(state: ExploreState, compact?: boolean): string {
const urlState: ExploreUrlState = {
datasource: state.datasourceName,
datasource: state.initialDatasource,
queries: state.initialQueries.map(clearQueryKeys),
range: state.range,
};

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

@ -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,20 +1,30 @@
// Libraries
import React, { PureComponent } from 'react';
import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader';
import { EditorTabBody } from './EditorTabBody';
// Services & Utils
import { AngularComponent, getAngularLoader } from 'app/core/services/AngularLoader';
import appEvents from 'app/core/app_events';
// Components
import { EditorTabBody, EditorToolbarView } from './EditorTabBody';
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
import StateHistory from './StateHistory';
import 'app/features/alerting/AlertTabCtrl';
// Types
import { DashboardModel } from '../dashboard_model';
import { PanelModel } from '../panel_model';
interface Props {
angularPanel?: AngularComponent;
dashboard: DashboardModel;
panel: PanelModel;
}
export class AlertTab extends PureComponent<Props> {
element: any;
component: AngularComponent;
constructor(props) {
super(props);
}
panelCtrl: any;
componentDidMount() {
if (this.shouldLoadAlertTab()) {
@ -29,7 +39,7 @@ export class AlertTab extends PureComponent<Props> {
}
shouldLoadAlertTab() {
return this.props.angularPanel && this.element;
return this.props.angularPanel && this.element && !this.component;
}
componentWillUnmount() {
@ -51,21 +61,80 @@ export class AlertTab extends PureComponent<Props> {
return;
}
const panelCtrl = scope.$$childHead.ctrl;
this.panelCtrl = scope.$$childHead.ctrl;
const loader = getAngularLoader();
const template = '<alert-tab />';
const scopeProps = {
ctrl: panelCtrl,
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={[]}>
<div ref={element => (this.element = element)} />
<EditorTabBody heading="Alert" toolbarItems={toolbarItems}>
<>
<div ref={element => (this.element = element)} />
{!alert && <EmptyListCTA model={model} />}
</>
</EditorTabBody>
);
}

View File

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

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

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 {
@ -204,12 +204,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

@ -0,0 +1,110 @@
import React, { PureComponent } from 'react';
import alertDef from '../../alerting/state/alertDef';
import { getBackendSrv } from 'app/core/services/backend_srv';
import { DashboardModel } from '../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

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

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

@ -40,6 +40,8 @@ import ErrorBoundary from './ErrorBoundary';
import { Alert } from './Error';
import TimePicker, { parseTime } from './TimePicker';
const LAST_USED_DATASOURCE_KEY = 'grafana.explore.datasource';
interface ExploreProps {
datasourceSrv: DatasourceSrv;
onChangeSplit: (split: boolean, state?: ExploreState) => void;
@ -90,6 +92,10 @@ interface ExploreProps {
export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
el: any;
exploreEvents: Emitter;
/**
* Set via URL or local storage
*/
initialDatasource: string;
/**
* Current query expressions of the rows including their modifications, used for running queries.
* Not kept in component state to prevent edit-render roundtrips.
@ -115,6 +121,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
initialQueries = splitState.initialQueries;
} else {
const { datasource, queries, range } = props.urlState as ExploreUrlState;
const initialDatasource = datasource || store.get(LAST_USED_DATASOURCE_KEY);
initialQueries = ensureQueries(queries);
const initialRange = { from: parseTime(range.from), to: parseTime(range.to) } || { ...DEFAULT_RANGE };
// Millies step for helper bar charts
@ -124,10 +131,10 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
datasourceError: null,
datasourceLoading: null,
datasourceMissing: false,
datasourceName: datasource,
exploreDatasources: [],
graphInterval: initialGraphInterval,
graphResult: [],
initialDatasource,
initialQueries,
history: [],
logsResult: null,
@ -151,7 +158,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
async componentDidMount() {
const { datasourceSrv } = this.props;
const { datasourceName } = this.state;
const { initialDatasource } = this.state;
if (!datasourceSrv) {
throw new Error('No datasource service passed as props.');
}
@ -165,10 +172,10 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
if (datasources.length > 0) {
this.setState({ datasourceLoading: true, exploreDatasources });
// Priority: datasource in url, default datasource, first explore datasource
// Priority for datasource preselection: URL, localstorage, default datasource
let datasource;
if (datasourceName) {
datasource = await datasourceSrv.get(datasourceName);
if (initialDatasource) {
datasource = await datasourceSrv.get(initialDatasource);
} else {
datasource = await datasourceSrv.get();
}
@ -253,13 +260,15 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
supportsLogs,
supportsTable,
datasourceLoading: false,
datasourceName: datasource.name,
initialDatasource: datasource.name,
initialQueries: nextQueries,
logsHighlighterExpressions: undefined,
showingStartPage: Boolean(StartPage),
},
() => {
if (datasourceError === null) {
// Save last-used datasource
store.set(LAST_USED_DATASOURCE_KEY, datasource.name);
this.onSubmit();
}
}

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

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

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

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

@ -38,8 +38,8 @@ export const defaultProps = {
showThresholdLabels: false,
suffix: '',
decimals: 0,
stat: '',
unit: '',
stat: 'avg',
unit: 'none',
mappings: [],
thresholds: [],
},

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

@ -155,11 +155,11 @@ export interface ExploreState {
datasourceError: any;
datasourceLoading: boolean | null;
datasourceMissing: boolean;
datasourceName?: string;
exploreDatasources: DataSourceSelectItem[];
graphInterval: number; // in ms
graphResult?: any[];
history: HistoryItem[];
initialDatasource?: string;
initialQueries: DataQuery[];
logsHighlighterExpressions?: string[];
logsResult?: LogsModel;

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

@ -63,6 +63,7 @@ $select-input-bg-disabled: $input-bg-disabled;
.gf-form-select-box__menu-list {
overflow-y: auto;
max-height: 300px;
max-width: 600px;
}
.tag-filter .gf-form-select-box__menu {

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

@ -3,9 +3,6 @@ const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
module.exports = {
target: 'web',
stats: {
children: false
},
entry: {
app: './public/app/index.ts',
},
@ -25,6 +22,7 @@ module.exports = {
],
},
stats: {
children: false,
warningsFilter: /export .* was not found in/
},
node: {

View File

@ -3,7 +3,6 @@
const merge = require('webpack-merge');
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
const common = require('./webpack.common.js');
const webpack = require('webpack');
const path = require('path');
const ngAnnotatePlugin = require('ng-annotate-webpack-plugin');
const HtmlWebpackPlugin = require("html-webpack-plugin");

View File

@ -3969,7 +3969,7 @@ debug@^4.1.0:
dependencies:
ms "^2.1.1"
debuglog@*, debuglog@^1.0.1:
debuglog@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492"
@ -6327,7 +6327,7 @@ import-local@^2.0.0:
pkg-dir "^3.0.0"
resolve-cwd "^2.0.0"
imurmurhash@*, imurmurhash@^0.1.4:
imurmurhash@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
@ -7809,10 +7809,6 @@ lockfile@^1.0.4:
dependencies:
signal-exit "^3.0.2"
lodash._baseindexof@*:
version "3.1.0"
resolved "https://registry.yarnpkg.com/lodash._baseindexof/-/lodash._baseindexof-3.1.0.tgz#fe52b53a1c6761e42618d654e4a25789ed61822c"
lodash._baseuniq@~4.6.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/lodash._baseuniq/-/lodash._baseuniq-4.6.0.tgz#0ebb44e456814af7905c6212fa2c9b2d51b841e8"
@ -7820,25 +7816,11 @@ lodash._baseuniq@~4.6.0:
lodash._createset "~4.0.0"
lodash._root "~3.0.0"
lodash._bindcallback@*:
version "3.0.1"
resolved "https://registry.yarnpkg.com/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz#e531c27644cf8b57a99e17ed95b35c748789392e"
lodash._cacheindexof@*:
version "3.0.2"
resolved "https://registry.yarnpkg.com/lodash._cacheindexof/-/lodash._cacheindexof-3.0.2.tgz#3dc69ac82498d2ee5e3ce56091bafd2adc7bde92"
lodash._createcache@*:
version "3.1.2"
resolved "https://registry.yarnpkg.com/lodash._createcache/-/lodash._createcache-3.1.2.tgz#56d6a064017625e79ebca6b8018e17440bdcf093"
dependencies:
lodash._getnative "^3.0.0"
lodash._createset@~4.0.0:
version "4.0.3"
resolved "https://registry.yarnpkg.com/lodash._createset/-/lodash._createset-4.0.3.tgz#0f4659fbb09d75194fa9e2b88a6644d363c9fe26"
lodash._getnative@*, lodash._getnative@^3.0.0:
lodash._getnative@^3.0.0:
version "3.9.1"
resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5"
@ -7914,10 +7896,6 @@ lodash.mergewith@^4.6.0:
version "4.6.1"
resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927"
lodash.restparam@*:
version "3.6.1"
resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805"
lodash.sortby@^4.7.0:
version "4.7.0"
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
@ -10742,7 +10720,7 @@ readable-stream@~1.1.10:
isarray "0.0.1"
string_decoder "~0.10.x"
readdir-scoped-modules@*, readdir-scoped-modules@^1.0.0:
readdir-scoped-modules@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/readdir-scoped-modules/-/readdir-scoped-modules-1.0.2.tgz#9fafa37d286be5d92cbaebdee030dc9b5f406747"
dependencies: