mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
commit
49144f07e4
@ -285,7 +285,7 @@ Content-Type: application/json
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
{message: "User permissions updated"}
|
||||
{"message": "User permissions updated"}
|
||||
```
|
||||
|
||||
## Delete global User
|
||||
@ -308,7 +308,7 @@ Content-Type: application/json
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
{message: "User deleted"}
|
||||
{"message": "User deleted"}
|
||||
```
|
||||
|
||||
## Pause all alerts
|
||||
@ -339,5 +339,5 @@ JSON Body schema:
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
{state: "new state", message: "alerts pause/un paused", "alertsAffected": 100}
|
||||
{"state": "new state", "message": "alerts pause/un paused", "alertsAffected": 100}
|
||||
```
|
||||
|
@ -34,32 +34,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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)))
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
})
|
||||
}
|
||||
|
@ -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" />}
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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,
|
||||
};
|
||||
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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);
|
||||
|
@ -1,191 +1,168 @@
|
||||
<div class="panel-option-section__body" ng-if="ctrl.alert">
|
||||
<div class="edit-tab-with-sidemenu">
|
||||
<aside class="edit-sidemenu-aside">
|
||||
<ul class="edit-sidemenu">
|
||||
<li ng-class="{active: ctrl.subTabIndex === 0}">
|
||||
<a ng-click="ctrl.changeTabIndex(0)">Alert Config</a>
|
||||
</li>
|
||||
<li ng-class="{active: ctrl.subTabIndex === 1}">
|
||||
<a ng-click="ctrl.changeTabIndex(1)">
|
||||
Notifications <span class="muted">({{ctrl.alertNotifications.length}})</span>
|
||||
</a>
|
||||
</li>
|
||||
<li ng-class="{active: ctrl.subTabIndex === 2}">
|
||||
<a ng-click="ctrl.changeTabIndex(2)">State history</a>
|
||||
</li>
|
||||
<li>
|
||||
<a ng-click="ctrl.delete()">Delete</a>
|
||||
</li>
|
||||
</ul>
|
||||
</aside>
|
||||
<div ng-if="ctrl.panel.alert">
|
||||
<div class="alert alert-error m-b-2" ng-show="ctrl.error">
|
||||
<i class="fa fa-warning"></i> {{ctrl.error}}
|
||||
</div>
|
||||
<div class="panel-option-section">
|
||||
<div class="panel-option-section__body">
|
||||
<div class="gf-form-group">
|
||||
<h4 class="section-heading">Rule</h4>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-6">Name</span>
|
||||
<input type="text" class="gf-form-input width-20" ng-model="ctrl.alert.name">
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-9">Evaluate every</span>
|
||||
<input class="gf-form-input max-width-6" type="text" ng-model="ctrl.alert.frequency">
|
||||
</div>
|
||||
<div class="gf-form max-width-11">
|
||||
<label class="gf-form-label width-5">For</label>
|
||||
<input type="text" class="gf-form-input max-width-6" ng-model="ctrl.alert.for"
|
||||
spellcheck='false' placeholder="5m">
|
||||
<info-popover mode="right-absolute">
|
||||
If an alert rule has a configured For and the query violates the configured
|
||||
threshold it
|
||||
will first go from OK to Pending.
|
||||
Going from OK to Pending Grafana will not send any notifications. Once the alert
|
||||
rule
|
||||
has
|
||||
been firing for more than For duration, it will change to Alerting and send alert
|
||||
notifications.
|
||||
</info-popover>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="edit-tab-content">
|
||||
<div ng-if="ctrl.subTabIndex === 0">
|
||||
<div class="alert alert-error m-b-2" ng-show="ctrl.error">
|
||||
<i class="fa fa-warning"></i> {{ctrl.error}}
|
||||
</div>
|
||||
<div class="gf-form-group">
|
||||
<h4 class="section-heading">Conditions</h4>
|
||||
<div class="gf-form-inline" ng-repeat="conditionModel in ctrl.conditionModels">
|
||||
<div class="gf-form">
|
||||
<metric-segment-model css-class="query-keyword width-5" ng-if="$index"
|
||||
property="conditionModel.operator.type" options="ctrl.evalOperators"
|
||||
custom="false"></metric-segment-model>
|
||||
<span class="gf-form-label query-keyword width-5" ng-if="$index===0">WHEN</span>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<query-part-editor class="gf-form-label query-part width-9"
|
||||
part="conditionModel.reducerPart"
|
||||
handle-event="ctrl.handleReducerPartEvent(conditionModel, $event)">
|
||||
</query-part-editor>
|
||||
<span class="gf-form-label query-keyword">OF</span>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<query-part-editor class="gf-form-label query-part" part="conditionModel.queryPart"
|
||||
handle-event="ctrl.handleQueryPartEvent(conditionModel, $event)">
|
||||
</query-part-editor>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<metric-segment-model property="conditionModel.evaluator.type" options="ctrl.evalFunctions"
|
||||
custom="false" css-class="query-keyword"
|
||||
on-change="ctrl.evaluatorTypeChanged(conditionModel.evaluator)"></metric-segment-model>
|
||||
<input class="gf-form-input max-width-9" type="number" step="any"
|
||||
ng-hide="conditionModel.evaluator.params.length === 0"
|
||||
ng-model="conditionModel.evaluator.params[0]"
|
||||
ng-change="ctrl.evaluatorParamsChanged()" />
|
||||
<label class="gf-form-label query-keyword"
|
||||
ng-show="conditionModel.evaluator.params.length === 2">TO</label>
|
||||
<input class="gf-form-input max-width-9" type="number" step="any"
|
||||
ng-if="conditionModel.evaluator.params.length === 2"
|
||||
ng-model="conditionModel.evaluator.params[1]"
|
||||
ng-change="ctrl.evaluatorParamsChanged()" />
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label">
|
||||
<a class="pointer" tabindex="1" ng-click="ctrl.removeCondition($index)">
|
||||
<i class="fa fa-trash"></i>
|
||||
</a>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group">
|
||||
<h5 class="section-heading">Alert Config</h5>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-6">Name</span>
|
||||
<input type="text" class="gf-form-input width-20" ng-model="ctrl.alert.name">
|
||||
</div>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-9">Evaluate every</span>
|
||||
<input class="gf-form-input max-width-6" type="text" ng-model="ctrl.alert.frequency">
|
||||
</div>
|
||||
<div class="gf-form max-width-11">
|
||||
<label class="gf-form-label width-5">For</label>
|
||||
<input type="text" class="gf-form-input max-width-6" ng-model="ctrl.alert.for" spellcheck='false' placeholder="5m">
|
||||
<info-popover mode="right-absolute">
|
||||
If an alert rule has a configured For and the query violates the configured threshold it will first go from OK to Pending.
|
||||
Going from OK to Pending Grafana will not send any notifications. Once the alert rule has been firing for more than For duration, it will change to Alerting and send alert notifications.
|
||||
</info-popover>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label dropdown">
|
||||
<a class="pointer dropdown-toggle" data-toggle="dropdown">
|
||||
<i class="fa fa-plus"></i>
|
||||
</a>
|
||||
<ul class="dropdown-menu" role="menu">
|
||||
<li ng-repeat="ct in ctrl.conditionTypes" role="menuitem">
|
||||
<a ng-click="ctrl.addCondition(ct.value);">{{ct.text}}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group">
|
||||
<h5 class="section-heading">Conditions</h5>
|
||||
<div class="gf-form-inline" ng-repeat="conditionModel in ctrl.conditionModels">
|
||||
<div class="gf-form">
|
||||
<metric-segment-model css-class="query-keyword width-5" ng-if="$index" property="conditionModel.operator.type" options="ctrl.evalOperators" custom="false"></metric-segment-model>
|
||||
<span class="gf-form-label query-keyword width-5" ng-if="$index===0">WHEN</span>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<query-part-editor class="gf-form-label query-part width-9" part="conditionModel.reducerPart" handle-event="ctrl.handleReducerPartEvent(conditionModel, $event)">
|
||||
</query-part-editor>
|
||||
<span class="gf-form-label query-keyword">OF</span>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<query-part-editor class="gf-form-label query-part" part="conditionModel.queryPart" handle-event="ctrl.handleQueryPartEvent(conditionModel, $event)">
|
||||
</query-part-editor>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<metric-segment-model property="conditionModel.evaluator.type" options="ctrl.evalFunctions" custom="false" css-class="query-keyword" on-change="ctrl.evaluatorTypeChanged(conditionModel.evaluator)"></metric-segment-model>
|
||||
<input class="gf-form-input max-width-9" type="number" step="any" ng-hide="conditionModel.evaluator.params.length === 0" ng-model="conditionModel.evaluator.params[0]" ng-change="ctrl.evaluatorParamsChanged()">
|
||||
<label class="gf-form-label query-keyword" ng-show="conditionModel.evaluator.params.length === 2">TO</label>
|
||||
<input class="gf-form-input max-width-9" type="number" step="any" ng-if="conditionModel.evaluator.params.length === 2" ng-model="conditionModel.evaluator.params[1]" ng-change="ctrl.evaluatorParamsChanged()">
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label">
|
||||
<a class="pointer" tabindex="1" ng-click="ctrl.removeCondition($index)">
|
||||
<i class="fa fa-trash"></i>
|
||||
</a>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form-group">
|
||||
<h4 class="section-heading">No Data & Error Handling</h4>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-15">If no data or all values are null</span>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label query-keyword">SET STATE TO</span>
|
||||
<div class="gf-form-select-wrapper">
|
||||
<select class="gf-form-input" ng-model="ctrl.alert.noDataState"
|
||||
ng-options="f.value as f.text for f in ctrl.noDataModes">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label dropdown">
|
||||
<a class="pointer dropdown-toggle" data-toggle="dropdown">
|
||||
<i class="fa fa-plus"></i>
|
||||
</a>
|
||||
<ul class="dropdown-menu" role="menu">
|
||||
<li ng-repeat="ct in ctrl.conditionTypes" role="menuitem">
|
||||
<a ng-click="ctrl.addCondition(ct.value);">{{ct.text}}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-15">If execution error or timeout</span>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label query-keyword">SET STATE TO</span>
|
||||
<div class="gf-form-select-wrapper">
|
||||
<select class="gf-form-input" ng-model="ctrl.alert.executionErrorState"
|
||||
ng-options="f.value as f.text for f in ctrl.executionErrorModes">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-18">If no data or all values are null</span>
|
||||
<span class="gf-form-label query-keyword">SET STATE TO</span>
|
||||
<div class="gf-form-select-wrapper">
|
||||
<select class="gf-form-input" ng-model="ctrl.alert.noDataState" ng-options="f.value as f.text for f in ctrl.noDataModes">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form-button-row">
|
||||
<button class="btn btn-inverse" ng-click="ctrl.test()">
|
||||
Test Rule
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-18">If execution error or timeout</span>
|
||||
<span class="gf-form-label query-keyword">SET STATE TO</span>
|
||||
<div class="gf-form-select-wrapper">
|
||||
<select class="gf-form-input" ng-model="ctrl.alert.executionErrorState" ng-options="f.value as f.text for f in ctrl.executionErrorModes">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form-group" ng-if="ctrl.testing">
|
||||
Evaluating rule <i class="fa fa-spinner fa-spin"></i>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-button-row">
|
||||
<button class="btn btn-inverse" ng-click="ctrl.test()">
|
||||
Test Rule
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form-group" ng-if="ctrl.testResult">
|
||||
<json-tree root-name="result" object="ctrl.testResult" start-expanded="true"></json-tree>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group" ng-if="ctrl.testing">
|
||||
Evaluating rule <i class="fa fa-spinner fa-spin"></i>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group" ng-if="ctrl.testResult">
|
||||
<json-tree root-name="result" object="ctrl.testResult" start-expanded="true"></json-tree>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group" ng-if="ctrl.subTabIndex === 1">
|
||||
<h5 class="section-heading">Notifications</h5>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form max-width-30">
|
||||
<span class="gf-form-label width-8">Send to</span>
|
||||
<span class="gf-form-label" ng-repeat="nc in ctrl.alertNotifications" ng-style="{'background-color': nc.bgColor }">
|
||||
<i class="{{nc.iconClass}}"></i> {{nc.name}}
|
||||
<i class="fa fa-remove pointer muted" ng-click="ctrl.removeNotification($index)" ng-if="nc.isDefault === false"></i>
|
||||
</span>
|
||||
<metric-segment segment="ctrl.addNotificationSegment" get-options="ctrl.getNotifications()" on-change="ctrl.notificationAdded()"></metric-segment>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form gf-form--v-stretch">
|
||||
<span class="gf-form-label width-8">Message</span>
|
||||
<textarea class="gf-form-input" rows="10" ng-model="ctrl.alert.message" placeholder="Notification message details..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group" style="max-width: 720px;" ng-if="ctrl.subTabIndex === 2">
|
||||
<button class="btn btn-mini btn-danger pull-right" ng-click="ctrl.clearHistory()"><i class="fa fa-trash"></i> Clear history</button>
|
||||
<h5 class="section-heading" style="whitespace: nowrap">
|
||||
State history <span class="muted small">(last 50 state changes)</span>
|
||||
</h5>
|
||||
|
||||
<div ng-show="ctrl.alertHistory.length === 0">
|
||||
<br>
|
||||
<i>No state changes recorded</i>
|
||||
</div>
|
||||
|
||||
<ol class="alert-rule-list" >
|
||||
<li class="alert-rule-item" ng-repeat="al in ctrl.alertHistory">
|
||||
<div class="alert-rule-item__icon {{al.stateModel.stateClass}}">
|
||||
<i class="{{al.stateModel.iconClass}}"></i>
|
||||
</div>
|
||||
<div class="alert-rule-item__body">
|
||||
<div class="alert-rule-item__header">
|
||||
<div class="alert-rule-item__text">
|
||||
<span class="{{al.stateModel.stateClass}}">{{al.stateModel.text}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="alert-list-info">{{al.info}}</span>
|
||||
</div>
|
||||
<div class="alert-rule-item__time">
|
||||
<span>{{al.time}}</span>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group p-t-4 p-b-4" ng-if="!ctrl.alert">
|
||||
<div class="empty-list-cta">
|
||||
<div class="empty-list-cta__title">Panel has no alert rule defined</div>
|
||||
<button class="empty-list-cta__button btn btn-xlarge btn-success" ng-click="ctrl.enable()">
|
||||
<i class="icon-gf icon-gf-alert"></i>
|
||||
Create Alert
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-option-section">
|
||||
<div class="panel-option-section__header">Notifications</div>
|
||||
<div class="panel-option-section__body">
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-8">Send to</span>
|
||||
</div>
|
||||
<div class="gf-form" ng-repeat="nc in ctrl.alertNotifications">
|
||||
<span class="gf-form-label" ng-style="{'background-color': nc.bgColor }">
|
||||
<i class="{{nc.iconClass}}"></i> {{nc.name}}
|
||||
<i class="fa fa-remove pointer muted" ng-click="ctrl.removeNotification($index)" ng-if="nc.isDefault === false"></i>
|
||||
</span>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<metric-segment segment="ctrl.addNotificationSegment"
|
||||
get-options="ctrl.getNotifications()"
|
||||
on-change="ctrl.notificationAdded()"></metric-segment>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form gf-form--v-stretch">
|
||||
<span class="gf-form-label width-8">Message</span>
|
||||
<textarea class="gf-form-input" rows="10" ng-model="ctrl.alert.message"
|
||||
placeholder="Notification message details..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,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>
|
||||
);
|
||||
}
|
||||
|
@ -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()}
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
110
public/app/features/dashboard/dashgrid/StateHistory.tsx
Normal file
110
public/app/features/dashboard/dashgrid/StateHistory.tsx
Normal 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;
|
@ -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,
|
||||
|
@ -16,7 +16,7 @@ const BasicSettings: SFC<Props> = ({ dataSourceName, isDefault, onDefaultChange,
|
||||
<div className="gf-form max-width-30" style={{ marginRight: '3px' }}>
|
||||
<Label
|
||||
tooltip={
|
||||
'The name is used when you select the data source in panels. The Default data source is' +
|
||||
'The name is used when you select the data source in panels. The Default data source is ' +
|
||||
'preselected in new panels.'
|
||||
}
|
||||
>
|
||||
|
@ -16,7 +16,7 @@ exports[`Render should render component 1`] = `
|
||||
}
|
||||
>
|
||||
<Component
|
||||
tooltip="The name is used when you select the data source in panels. The Default data source ispreselected in new panels."
|
||||
tooltip="The name is used when you select the data source in panels. The Default data source is preselected in new panels."
|
||||
>
|
||||
Name
|
||||
</Component>
|
||||
|
@ -5,7 +5,7 @@ import { LayoutModes } from '../../../core/components/LayoutSelector/LayoutSelec
|
||||
const initialState: DataSourcesState = {
|
||||
dataSources: [] as DataSource[],
|
||||
dataSource: {} as DataSource,
|
||||
layoutMode: LayoutModes.Grid,
|
||||
layoutMode: LayoutModes.List,
|
||||
searchQuery: '',
|
||||
dataSourcesCount: 0,
|
||||
dataSourceTypes: [] as Plugin[],
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
import _ from 'lodash';
|
||||
import React, { PureComponent } from 'react';
|
||||
import classnames from 'classnames';
|
||||
|
||||
|
@ -55,7 +55,7 @@ class TypeaheadItem extends React.PureComponent<TypeaheadItemProps> {
|
||||
interface TypeaheadGroupProps {
|
||||
items: CompletionItem[];
|
||||
label: string;
|
||||
onClickItem: (CompletionItem) => void;
|
||||
onClickItem: (suggestion: CompletionItem) => void;
|
||||
selected: CompletionItem;
|
||||
prefix?: string;
|
||||
}
|
||||
|
@ -1,6 +1,3 @@
|
||||
// Libraries
|
||||
import _ from 'lodash';
|
||||
|
||||
// Services & utils
|
||||
import coreModule from 'app/core/core_module';
|
||||
import { Emitter } from 'app/core/utils/emitter';
|
||||
|
@ -308,7 +308,7 @@ export default class InfluxDatasource {
|
||||
return 'now()';
|
||||
}
|
||||
|
||||
const parts = /^now-(\d+)([d|h|m|s])$/.exec(date);
|
||||
const parts = /^now-(\d+)([dhms])$/.exec(date);
|
||||
if (parts) {
|
||||
const amount = parseInt(parts[1], 10);
|
||||
const unit = parts[2];
|
||||
|
@ -1,4 +1,3 @@
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import Cascader from 'rc-cascader';
|
||||
import PluginPrism from 'slate-prism';
|
||||
|
@ -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';
|
||||
|
@ -38,8 +38,8 @@ export const defaultProps = {
|
||||
showThresholdLabels: false,
|
||||
suffix: '',
|
||||
decimals: 0,
|
||||
stat: '',
|
||||
unit: '',
|
||||
stat: 'avg',
|
||||
unit: 'none',
|
||||
mappings: [],
|
||||
thresholds: [],
|
||||
},
|
||||
|
@ -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 };
|
||||
}
|
||||
|
@ -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;
|
||||
|
55
public/app/viz/Gauge.test.tsx
Normal file
55
public/app/viz/Gauge.test.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { Gauge, Props } from './Gauge';
|
||||
import { BasicGaugeColor } from '../types';
|
||||
import { TimeSeriesVMs } from '@grafana/ui';
|
||||
|
||||
jest.mock('jquery', () => ({
|
||||
plot: jest.fn(),
|
||||
}));
|
||||
|
||||
const setup = (propOverrides?: object) => {
|
||||
const props: Props = {
|
||||
baseColor: BasicGaugeColor.Green,
|
||||
maxValue: 100,
|
||||
mappings: [],
|
||||
minValue: 0,
|
||||
prefix: '',
|
||||
showThresholdMarkers: true,
|
||||
showThresholdLabels: false,
|
||||
suffix: '',
|
||||
thresholds: [],
|
||||
unit: 'none',
|
||||
stat: 'avg',
|
||||
height: 300,
|
||||
width: 300,
|
||||
timeSeries: {} as TimeSeriesVMs,
|
||||
decimals: 0,
|
||||
};
|
||||
|
||||
Object.assign(props, propOverrides);
|
||||
|
||||
const wrapper = shallow(<Gauge {...props} />);
|
||||
const instance = wrapper.instance() as Gauge;
|
||||
|
||||
return {
|
||||
instance,
|
||||
wrapper,
|
||||
};
|
||||
};
|
||||
|
||||
describe('Get font color', () => {
|
||||
it('should get base color if no threshold', () => {
|
||||
const { instance } = setup();
|
||||
|
||||
expect(instance.getFontColor(40)).toEqual(BasicGaugeColor.Green);
|
||||
});
|
||||
|
||||
it('should be f2f2f2', () => {
|
||||
const { instance } = setup({
|
||||
thresholds: [{ value: 59, color: '#f2f2f2' }],
|
||||
});
|
||||
|
||||
expect(instance.getFontColor(58)).toEqual('#f2f2f2');
|
||||
});
|
||||
});
|
@ -5,7 +5,7 @@ import { TimeSeriesVMs } from '@grafana/ui';
|
||||
import config from '../core/config';
|
||||
import kbn from '../core/utils/kbn';
|
||||
|
||||
interface Props {
|
||||
export interface Props {
|
||||
baseColor: string;
|
||||
decimals: number;
|
||||
height: number;
|
||||
@ -96,12 +96,14 @@ export class Gauge extends PureComponent<Props> {
|
||||
getFontColor(value) {
|
||||
const { baseColor, maxValue, thresholds } = this.props;
|
||||
|
||||
const atThreshold = thresholds.filter(threshold => value <= threshold.value);
|
||||
if (thresholds.length > 0) {
|
||||
const atThreshold = thresholds.filter(threshold => value <= threshold.value);
|
||||
|
||||
if (atThreshold.length > 0) {
|
||||
return atThreshold[0].color;
|
||||
} else if (value <= maxValue) {
|
||||
return BasicGaugeColor.Red;
|
||||
if (atThreshold.length > 0) {
|
||||
return atThreshold[0].color;
|
||||
} else if (value <= maxValue) {
|
||||
return BasicGaugeColor.Red;
|
||||
}
|
||||
}
|
||||
|
||||
return baseColor;
|
||||
|
@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns="http://www.w3.org/1999/xhtml">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
|
@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns="http://www.w3.org/1999/xhtml">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
|
@ -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 {
|
||||
|
@ -23,7 +23,9 @@
|
||||
left: 20px;
|
||||
right: 20px;
|
||||
top: $navbarHeight;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
left: auto;
|
||||
width: 550px;
|
||||
}
|
||||
|
||||
|
@ -107,6 +107,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
@ -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: {
|
||||
|
@ -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");
|
||||
|
30
yarn.lock
30
yarn.lock
@ -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:
|
||||
|
Loading…
Reference in New Issue
Block a user