Compare commits

...

19 Commits

Author SHA1 Message Date
Carl Bergquist
69630b9bb7 Merge pull request #14024 from bergquist/cp_v5.3.x
Cherry picks for v5.3.4
2018-11-13 13:19:12 +01:00
Carl Bergquist
802387ec23 Merge branch 'v5.3.x' into cp_v5.3.x 2018-11-13 13:05:53 +01:00
Carl Bergquist
77cd62d547 Merge pull request #14049 from bergquist/cp_v5.3.3
Cherry-pick for v5.3.3
2018-11-13 12:51:05 +01:00
bergquist
b7facc390d Merge branch 'private_v5.3.x' into cp_v5.3.3
* private_v5.3.x:
  release 5.3.3
2018-11-13 10:53:53 +01:00
bergquist
a7c82192ab release 5.3.4 2018-11-12 11:53:41 +01:00
Augustin Husson
82bf655ec1 don't drop the value when it equals to None
(cherry picked from commit 2abf8a0e8b)
2018-11-12 11:51:30 +01:00
Julien Pivotto
a23780ddf0 Remove Origin and Referer headers while proxying requests
Fix #13949
Fix #13328

Signed-off-by: Julien Pivotto <roidelapluie@inuits.eu>
(cherry picked from commit 62417ca69f)
2018-11-12 11:51:12 +01:00
Dan Cech
7a8e246349 add auth.proxy headers to sample.ini
(cherry picked from commit 8a74fe2b76)
2018-11-12 11:48:41 +01:00
Dan Cech
16517b3040 add auth.proxy headers to default.ini
(cherry picked from commit 502290817a)
2018-11-12 11:48:41 +01:00
Torkel Ödegaard
43e78db0f6 fixed exporter bug missing adding requires for datasources only used via data source variable, fixes #13891
(cherry picked from commit 99610e040f)
2018-11-12 11:48:41 +01:00
bergquist
4fa671acc1 rename and mark functions as private
(cherry picked from commit 7bde98aff9)
2018-11-12 11:47:52 +01:00
bergquist
50efe02b22 export: provide more help regarding export format
this will provide the user with more info about
the export format and default to not use the format
for sharing on grafana.com etc.

ref #13781

(cherry picked from commit 17adb58d80)
2018-11-12 11:47:52 +01:00
bergquist
3ca0ab6d89 Revert "bump version to 5.3.3"
This reverts commit 617e69d411.
2018-11-06 16:58:15 +01:00
Carl Bergquist
fd8f22ca57 Merge pull request #13979 from bergquist/cp_v5.3.3
Cherry-picks for 5.3.3
2018-11-06 15:30:30 +01:00
bergquist
617e69d411 bump version to 5.3.3 2018-11-06 15:09:48 +01:00
Marcus Efraimsson
738db3319e fix selecting datasource using enter key
(cherry picked from commit e5e886ccb7)
2018-11-06 15:07:38 +01:00
bergquist
2d23704082 alerting: delete alerts when parent folder is deleted
closes #13322

(cherry picked from commit 423331dae0)
2018-11-06 15:07:13 +01:00
Torkel Ödegaard
c7736c0b7d IE11 fix for legend tables below graph
(cherry picked from commit d46c258933)
2018-11-06 15:06:48 +01:00
Marcus Efraimsson
eeb4d08031 mysql: fix timeFilter macro should respect local time zone
(cherry picked from commit 97b22aa5a9)
2018-11-06 15:06:05 +01:00
15 changed files with 133 additions and 37 deletions

View File

@@ -344,6 +344,7 @@ header_property = username
auto_sign_up = true
ldap_sync_ttl = 60
whitelist =
headers =
#################################### Auth LDAP ###########################
[auth.ldap]

View File

@@ -294,6 +294,7 @@ log_queries =
;auto_sign_up = true
;ldap_sync_ttl = 60
;whitelist = 192.168.1.1, 192.168.2.1
;headers = Email:X-User-Email, Name:X-User-Name
#################################### Basic Auth ##########################
[auth.basic]

View File

@@ -4,7 +4,7 @@
"company": "Grafana Labs"
},
"name": "grafana",
"version": "5.3.3",
"version": "5.3.4",
"repository": {
"type": "git",
"url": "http://github.com/grafana/grafana.git"

View File

@@ -195,6 +195,10 @@ func (proxy *DataSourceProxy) getDirector() func(req *http.Request) {
req.Header.Del("X-Forwarded-Proto")
req.Header.Set("User-Agent", fmt.Sprintf("Grafana/%s", setting.BuildVersion))
// Clear Origin and Referer to avoir CORS issues
req.Header.Del("Origin")
req.Header.Del("Referer")
// set X-Forwarded-For header
if req.RemoteAddr != "" {
remoteAddr, _, err := net.SplitHostPort(req.RemoteAddr)

View File

@@ -362,6 +362,32 @@ func TestDSRouteRule(t *testing.T) {
})
})
Convey("When proxying a custom datasource", func() {
plugin := &plugins.DataSourcePlugin{}
ds := &m.DataSource{
Type: "custom-datasource",
Url: "http://host/root/",
}
ctx := &m.ReqContext{}
proxy := NewDataSourceProxy(ds, plugin, ctx, "/path/to/folder/")
req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
req.Header.Add("Origin", "grafana.com")
req.Header.Add("Referer", "grafana.com")
req.Header.Add("X-Canary", "stillthere")
So(err, ShouldBeNil)
proxy.getDirector()(req)
Convey("Should keep user request (including trailing slash)", func() {
So(req.URL.String(), ShouldEqual, "http://host/root/path/to/folder/")
})
Convey("Origin and Referer headers should be dropped", func() {
So(req.Header.Get("Origin"), ShouldEqual, "")
So(req.Header.Get("Referer"), ShouldEqual, "")
So(req.Header.Get("X-Canary"), ShouldEqual, "stillthere")
})
})
})
}

View File

@@ -327,6 +327,24 @@ func DeleteDashboard(cmd *m.DeleteDashboardCommand) error {
if dashboard.IsFolder {
deletes = append(deletes, "DELETE FROM dashboard_provisioning WHERE dashboard_id in (select id from dashboard where folder_id = ?)")
deletes = append(deletes, "DELETE FROM dashboard WHERE folder_id = ?")
dashIds := []struct {
Id int64
}{}
err := sess.SQL("select id from dashboard where folder_id = ?", dashboard.Id).Find(&dashIds)
if err != nil {
return err
}
for _, id := range dashIds {
if err := deleteAlertDefinition(id.Id, sess); err != nil {
return nil
}
}
}
if err := deleteAlertDefinition(dashboard.Id, sess); err != nil {
return nil
}
for _, sql := range deletes {
@@ -337,10 +355,6 @@ func DeleteDashboard(cmd *m.DeleteDashboardCommand) error {
}
}
if err := deleteAlertDefinition(dashboard.Id, sess); err != nil {
return nil
}
return nil
})
}

View File

@@ -60,7 +60,7 @@ func (m *mySqlMacroEngine) evaluateMacro(name string, args []string) (string, er
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 FROM_UNIXTIME(%d) AND FROM_UNIXTIME(%d)", args[0], m.timeRange.GetFromAsSecondsEpoch(), m.timeRange.GetToAsSecondsEpoch()), nil
case "__timeFrom":
return fmt.Sprintf("'%s'", m.timeRange.GetFromAsTimeUTC().Format(time.RFC3339)), nil
case "__timeTo":

View File

@@ -60,7 +60,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 FROM_UNIXTIME(%d) AND FROM_UNIXTIME(%d)", from.Unix(), to.Unix()))
})
Convey("interpolate __timeFrom function", func() {
@@ -120,7 +120,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 FROM_UNIXTIME(%d) AND FROM_UNIXTIME(%d)", from.Unix(), to.Unix()))
})
Convey("interpolate __timeFrom function", func() {
@@ -168,7 +168,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 FROM_UNIXTIME(%d) AND FROM_UNIXTIME(%d)", from.Unix(), to.Unix()))
})
Convey("interpolate __timeFrom function", func() {

View File

@@ -88,7 +88,7 @@ export class FormDropdownCtrl {
if (evt.keyCode === 13) {
setTimeout(() => {
this.inputElement.blur();
}, 100);
}, 300);
}
});

View File

@@ -15,11 +15,19 @@
You can share dashboards on <a class="external-link" href="https://grafana.com">Grafana.com</a>
</p>
<gf-form-switch
class="gf-form"
label="Export for sharing externally"
label-class="width-16"
checked="ctrl.shareExternally"
tooltip="Useful for sharing dashboard publicly on grafana.com. Will templatize data source names. Can then only be used with the specific dashboard import API.">
</gf-form-switch>
<div class="gf-form-button-row">
<button type="button" class="btn gf-form-btn width-10 btn-success" ng-click="ctrl.save()">
<button type="button" class="btn gf-form-btn width-10 btn-success" ng-click="ctrl.saveDashboardAsFile()">
<i class="fa fa-save"></i> Save to file
</button>
<button type="button" class="btn gf-form-btn width-10 btn-secondary" ng-click="ctrl.saveJson()">
<button type="button" class="btn gf-form-btn width-10 btn-secondary" ng-click="ctrl.viewJson()">
<i class="fa fa-file-text-o"></i> View JSON
</button>
<a class="btn btn-link" ng-click="ctrl.dismiss()">Cancel</a>

View File

@@ -8,27 +8,47 @@ export class DashExportCtrl {
dash: any;
exporter: DashboardExporter;
dismiss: () => void;
shareExternally: boolean;
/** @ngInject */
constructor(private dashboardSrv, datasourceSrv, private $scope, private $rootScope) {
this.exporter = new DashboardExporter(datasourceSrv);
this.exporter.makeExportable(this.dashboardSrv.getCurrent()).then(dash => {
this.$scope.$apply(() => {
this.dash = dash;
});
});
this.dash = this.dashboardSrv.getCurrent();
}
save() {
const blob = new Blob([angular.toJson(this.dash, true)], {
saveDashboardAsFile() {
if (this.shareExternally) {
this.exporter.makeExportable(this.dash).then((dashboardJson: any) => {
this.$scope.$apply(() => {
this.openSaveAsDialog(dashboardJson);
});
});
} else {
this.openSaveAsDialog(this.dash.getSaveModelClone());
}
}
viewJson() {
if (this.shareExternally) {
this.exporter.makeExportable(this.dash).then((dashboardJson: any) => {
this.$scope.$apply(() => {
this.openJsonModal(dashboardJson);
});
});
} else {
this.openJsonModal(this.dash.getSaveModelClone());
}
}
private openSaveAsDialog(dash: any) {
const blob = new Blob([angular.toJson(dash, true)], {
type: 'application/json;charset=utf-8',
});
saveAs(blob, this.dash.title + '-' + new Date().getTime() + '.json');
saveAs(blob, dash.title + '-' + new Date().getTime() + '.json');
}
saveJson() {
const clone = this.dash;
private openJsonModal(clone: any) {
const editScope = this.$rootScope.$new();
editScope.object = clone;
editScope.enableCopy = true;

View File

@@ -29,19 +29,36 @@ export class DashboardExporter {
}
const templateizeDatasourceUsage = obj => {
let datasource = obj.datasource;
let datasourceVariable = null;
// ignore data source properties that contain a variable
if (obj.datasource && obj.datasource.indexOf('$') === 0) {
if (variableLookup[obj.datasource.substring(1)]) {
return;
if (datasource && datasource.indexOf('$') === 0) {
datasourceVariable = variableLookup[datasource.substring(1)];
if (datasourceVariable && datasourceVariable.current) {
datasource = datasourceVariable.current.value;
}
}
promises.push(
this.datasourceSrv.get(obj.datasource).then(ds => {
this.datasourceSrv.get(datasource).then(ds => {
if (ds.meta.builtIn) {
return;
}
// add data source type to require list
requires['datasource' + ds.meta.id] = {
type: 'datasource',
id: ds.meta.id,
name: ds.meta.name,
version: ds.meta.info.version || '1.0.0',
};
// if used via variable we can skip templatizing usage
if (datasourceVariable) {
return;
}
const refName = 'DS_' + ds.name.replace(' ', '_').toUpperCase();
datasources[refName] = {
name: refName,
@@ -51,14 +68,8 @@ export class DashboardExporter {
pluginId: ds.meta.id,
pluginName: ds.meta.name,
};
obj.datasource = '${' + refName + '}';
requires['datasource' + ds.meta.id] = {
type: 'datasource',
id: ds.meta.id,
name: ds.meta.name,
version: ds.meta.info.version || '1.0.0',
};
obj.datasource = '${' + refName + '}';
})
);
};

View File

@@ -32,8 +32,8 @@ describe('given dashboard with repeated panels', () => {
{
name: 'ds',
type: 'datasource',
query: 'testdb',
current: { value: 'prod', text: 'prod' },
query: 'other2',
current: { value: 'other2', text: 'other2' },
options: [],
},
],
@@ -205,6 +205,11 @@ describe('given dashboard with repeated panels', () => {
expect(variable.options[0].text).toBe('${VAR_PREFIX}');
expect(variable.options[0].value).toBe('${VAR_PREFIX}');
});
it('should add datasources only use via datasource variable to requires', () => {
const require = _.find(exported.__requires, { name: 'OtherDB_2' });
expect(require.id).toBe('other2');
});
});
// Stub responses
@@ -219,6 +224,11 @@ stubs['other'] = {
meta: { id: 'other', info: { version: '1.2.1' }, name: 'OtherDB' },
};
stubs['other2'] = {
name: 'other2',
meta: { id: 'other2', info: { version: '1.2.1' }, name: 'OtherDB_2' },
};
stubs['-- Mixed --'] = {
name: 'mixed',
meta: {

View File

@@ -28,7 +28,7 @@ export class TemplateSrv {
const existsOrEmpty = value => value || value === '';
this.index = this.variables.reduce((acc, currentValue) => {
if (currentValue.current && !currentValue.current.isNone && existsOrEmpty(currentValue.current.value)) {
if (currentValue.current && (currentValue.current.isNone || existsOrEmpty(currentValue.current.value))) {
acc[currentValue.name] = currentValue;
}
return acc;

View File

@@ -28,6 +28,7 @@
position: relative;
cursor: crosshair;
flex-grow: 1;
min-height: 65%;
}
.datapoints-warning {
@@ -46,7 +47,7 @@
.graph-legend {
display: flex;
flex: 0 1 auto;
max-height: 30%;
max-height: 35%;
margin: 0;
text-align: center;
padding-top: 6px;