mirror of
https://github.com/grafana/grafana.git
synced 2026-01-10 22:14:04 +08:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
69630b9bb7 | ||
|
|
802387ec23 | ||
|
|
77cd62d547 | ||
|
|
b7facc390d | ||
|
|
a7c82192ab | ||
|
|
82bf655ec1 | ||
|
|
a23780ddf0 | ||
|
|
7a8e246349 | ||
|
|
16517b3040 | ||
|
|
43e78db0f6 | ||
|
|
4fa671acc1 | ||
|
|
50efe02b22 | ||
|
|
3ca0ab6d89 | ||
|
|
fd8f22ca57 | ||
|
|
617e69d411 | ||
|
|
738db3319e | ||
|
|
2d23704082 | ||
|
|
c7736c0b7d | ||
|
|
eeb4d08031 |
@@ -344,6 +344,7 @@ header_property = username
|
||||
auto_sign_up = true
|
||||
ldap_sync_ttl = 60
|
||||
whitelist =
|
||||
headers =
|
||||
|
||||
#################################### Auth LDAP ###########################
|
||||
[auth.ldap]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -88,7 +88,7 @@ export class FormDropdownCtrl {
|
||||
if (evt.keyCode === 13) {
|
||||
setTimeout(() => {
|
||||
this.inputElement.blur();
|
||||
}, 100);
|
||||
}, 300);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 + '}';
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user