mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Began work on improving structure and organization of components under features/dashboard, #14062
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
import angular from 'angular';
|
||||
import { saveAs } from 'file-saver';
|
||||
|
||||
import coreModule from 'app/core/core_module';
|
||||
import { DashboardExporter } from './DashboardExporter';
|
||||
|
||||
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.dash = this.dashboardSrv.getCurrent();
|
||||
}
|
||||
|
||||
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, dash.title + '-' + new Date().getTime() + '.json');
|
||||
}
|
||||
|
||||
private openJsonModal(clone: object) {
|
||||
const model = {
|
||||
object: clone,
|
||||
enableCopy: true,
|
||||
};
|
||||
|
||||
this.$rootScope.appEvent('show-modal', {
|
||||
src: 'public/app/partials/edit_json.html',
|
||||
model: model,
|
||||
});
|
||||
|
||||
this.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
export function dashExportDirective() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
templateUrl: 'public/app/features/dashboard/components/DashExportModal/template.html',
|
||||
controller: DashExportCtrl,
|
||||
bindToController: true,
|
||||
controllerAs: 'ctrl',
|
||||
scope: { dismiss: '&' },
|
||||
};
|
||||
}
|
||||
|
||||
coreModule.directive('dashExportModal', dashExportDirective);
|
||||
@@ -0,0 +1,254 @@
|
||||
jest.mock('app/core/store', () => {
|
||||
return {
|
||||
getBool: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
import _ from 'lodash';
|
||||
import config from 'app/core/config';
|
||||
import { DashboardExporter } from './DashboardExporter';
|
||||
import { DashboardModel } from '../../dashboard_model';
|
||||
|
||||
describe('given dashboard with repeated panels', () => {
|
||||
let dash, exported;
|
||||
|
||||
beforeEach(done => {
|
||||
dash = {
|
||||
templating: {
|
||||
list: [
|
||||
{
|
||||
name: 'apps',
|
||||
type: 'query',
|
||||
datasource: 'gfdb',
|
||||
current: { value: 'Asd', text: 'Asd' },
|
||||
options: [{ value: 'Asd', text: 'Asd' }],
|
||||
},
|
||||
{
|
||||
name: 'prefix',
|
||||
type: 'constant',
|
||||
current: { value: 'collectd', text: 'collectd' },
|
||||
options: [],
|
||||
},
|
||||
{
|
||||
name: 'ds',
|
||||
type: 'datasource',
|
||||
query: 'other2',
|
||||
current: { value: 'other2', text: 'other2' },
|
||||
options: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
annotations: {
|
||||
list: [
|
||||
{
|
||||
name: 'logs',
|
||||
datasource: 'gfdb',
|
||||
},
|
||||
],
|
||||
},
|
||||
panels: [
|
||||
{ id: 6, datasource: 'gfdb', type: 'graph' },
|
||||
{ id: 7 },
|
||||
{
|
||||
id: 8,
|
||||
datasource: '-- Mixed --',
|
||||
targets: [{ datasource: 'other' }],
|
||||
},
|
||||
{ id: 9, datasource: '$ds' },
|
||||
{
|
||||
id: 2,
|
||||
repeat: 'apps',
|
||||
datasource: 'gfdb',
|
||||
type: 'graph',
|
||||
},
|
||||
{ id: 3, repeat: null, repeatPanelId: 2 },
|
||||
{
|
||||
id: 4,
|
||||
collapsed: true,
|
||||
panels: [
|
||||
{ id: 10, datasource: 'gfdb', type: 'table' },
|
||||
{ id: 11 },
|
||||
{
|
||||
id: 12,
|
||||
datasource: '-- Mixed --',
|
||||
targets: [{ datasource: 'other' }],
|
||||
},
|
||||
{ id: 13, datasource: '$ds' },
|
||||
{
|
||||
id: 14,
|
||||
repeat: 'apps',
|
||||
datasource: 'gfdb',
|
||||
type: 'heatmap',
|
||||
},
|
||||
{ id: 15, repeat: null, repeatPanelId: 14 },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
config.buildInfo.version = '3.0.2';
|
||||
|
||||
//Stubs test function calls
|
||||
const datasourceSrvStub = { get: jest.fn(arg => getStub(arg)) };
|
||||
|
||||
config.panels['graph'] = {
|
||||
id: 'graph',
|
||||
name: 'Graph',
|
||||
info: { version: '1.1.0' },
|
||||
};
|
||||
|
||||
config.panels['table'] = {
|
||||
id: 'table',
|
||||
name: 'Table',
|
||||
info: { version: '1.1.1' },
|
||||
};
|
||||
|
||||
config.panels['heatmap'] = {
|
||||
id: 'heatmap',
|
||||
name: 'Heatmap',
|
||||
info: { version: '1.1.2' },
|
||||
};
|
||||
|
||||
dash = new DashboardModel(dash, {});
|
||||
const exporter = new DashboardExporter(datasourceSrvStub);
|
||||
exporter.makeExportable(dash).then(clean => {
|
||||
exported = clean;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should replace datasource refs', () => {
|
||||
const panel = exported.panels[0];
|
||||
expect(panel.datasource).toBe('${DS_GFDB}');
|
||||
});
|
||||
|
||||
it('should replace datasource refs in collapsed row', () => {
|
||||
const panel = exported.panels[5].panels[0];
|
||||
expect(panel.datasource).toBe('${DS_GFDB}');
|
||||
});
|
||||
|
||||
it('should replace datasource in variable query', () => {
|
||||
expect(exported.templating.list[0].datasource).toBe('${DS_GFDB}');
|
||||
expect(exported.templating.list[0].options.length).toBe(0);
|
||||
expect(exported.templating.list[0].current.value).toBe(undefined);
|
||||
expect(exported.templating.list[0].current.text).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should replace datasource in annotation query', () => {
|
||||
expect(exported.annotations.list[1].datasource).toBe('${DS_GFDB}');
|
||||
});
|
||||
|
||||
it('should add datasource as input', () => {
|
||||
expect(exported.__inputs[0].name).toBe('DS_GFDB');
|
||||
expect(exported.__inputs[0].pluginId).toBe('testdb');
|
||||
expect(exported.__inputs[0].type).toBe('datasource');
|
||||
});
|
||||
|
||||
it('should add datasource to required', () => {
|
||||
const require = _.find(exported.__requires, { name: 'TestDB' });
|
||||
expect(require.name).toBe('TestDB');
|
||||
expect(require.id).toBe('testdb');
|
||||
expect(require.type).toBe('datasource');
|
||||
expect(require.version).toBe('1.2.1');
|
||||
});
|
||||
|
||||
it('should not add built in datasources to required', () => {
|
||||
const require = _.find(exported.__requires, { name: 'Mixed' });
|
||||
expect(require).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should add datasources used in mixed mode', () => {
|
||||
const require = _.find(exported.__requires, { name: 'OtherDB' });
|
||||
expect(require).not.toBe(undefined);
|
||||
});
|
||||
|
||||
it('should add graph panel to required', () => {
|
||||
const require = _.find(exported.__requires, { name: 'Graph' });
|
||||
expect(require.name).toBe('Graph');
|
||||
expect(require.id).toBe('graph');
|
||||
expect(require.version).toBe('1.1.0');
|
||||
});
|
||||
|
||||
it('should add table panel to required', () => {
|
||||
const require = _.find(exported.__requires, { name: 'Table' });
|
||||
expect(require.name).toBe('Table');
|
||||
expect(require.id).toBe('table');
|
||||
expect(require.version).toBe('1.1.1');
|
||||
});
|
||||
|
||||
it('should add heatmap panel to required', () => {
|
||||
const require = _.find(exported.__requires, { name: 'Heatmap' });
|
||||
expect(require.name).toBe('Heatmap');
|
||||
expect(require.id).toBe('heatmap');
|
||||
expect(require.version).toBe('1.1.2');
|
||||
});
|
||||
|
||||
it('should add grafana version', () => {
|
||||
const require = _.find(exported.__requires, { name: 'Grafana' });
|
||||
expect(require.type).toBe('grafana');
|
||||
expect(require.id).toBe('grafana');
|
||||
expect(require.version).toBe('3.0.2');
|
||||
});
|
||||
|
||||
it('should add constant template variables as inputs', () => {
|
||||
const input = _.find(exported.__inputs, { name: 'VAR_PREFIX' });
|
||||
expect(input.type).toBe('constant');
|
||||
expect(input.label).toBe('prefix');
|
||||
expect(input.value).toBe('collectd');
|
||||
});
|
||||
|
||||
it('should templatize constant variables', () => {
|
||||
const variable = _.find(exported.templating.list, { name: 'prefix' });
|
||||
expect(variable.query).toBe('${VAR_PREFIX}');
|
||||
expect(variable.current.text).toBe('${VAR_PREFIX}');
|
||||
expect(variable.current.value).toBe('${VAR_PREFIX}');
|
||||
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
|
||||
const stubs = [];
|
||||
stubs['gfdb'] = {
|
||||
name: 'gfdb',
|
||||
meta: { id: 'testdb', info: { version: '1.2.1' }, name: 'TestDB' },
|
||||
};
|
||||
|
||||
stubs['other'] = {
|
||||
name: '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: {
|
||||
id: 'mixed',
|
||||
info: { version: '1.2.1' },
|
||||
name: 'Mixed',
|
||||
builtIn: true,
|
||||
},
|
||||
};
|
||||
|
||||
stubs['-- Grafana --'] = {
|
||||
name: '-- Grafana --',
|
||||
meta: {
|
||||
id: 'grafana',
|
||||
info: { version: '1.2.1' },
|
||||
name: 'grafana',
|
||||
builtIn: true,
|
||||
},
|
||||
};
|
||||
|
||||
function getStub(arg) {
|
||||
return Promise.resolve(stubs[arg || 'gfdb']);
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
import config from 'app/core/config';
|
||||
import _ from 'lodash';
|
||||
import { DashboardModel } from '../../dashboard_model';
|
||||
|
||||
export class DashboardExporter {
|
||||
constructor(private datasourceSrv) {}
|
||||
|
||||
makeExportable(dashboard: DashboardModel) {
|
||||
// clean up repeated rows and panels,
|
||||
// this is done on the live real dashboard instance, not on a clone
|
||||
// so we need to undo this
|
||||
// this is pretty hacky and needs to be changed
|
||||
dashboard.cleanUpRepeats();
|
||||
|
||||
const saveModel = dashboard.getSaveModelClone();
|
||||
saveModel.id = null;
|
||||
|
||||
// undo repeat cleanup
|
||||
dashboard.processRepeats();
|
||||
|
||||
const inputs = [];
|
||||
const requires = {};
|
||||
const datasources = {};
|
||||
const promises = [];
|
||||
const variableLookup: any = {};
|
||||
|
||||
for (const variable of saveModel.templating.list) {
|
||||
variableLookup[variable.name] = variable;
|
||||
}
|
||||
|
||||
const templateizeDatasourceUsage = obj => {
|
||||
let datasource = obj.datasource;
|
||||
let datasourceVariable = null;
|
||||
|
||||
// ignore data source properties that contain a variable
|
||||
if (datasource && datasource.indexOf('$') === 0) {
|
||||
datasourceVariable = variableLookup[datasource.substring(1)];
|
||||
if (datasourceVariable && datasourceVariable.current) {
|
||||
datasource = datasourceVariable.current.value;
|
||||
}
|
||||
}
|
||||
|
||||
promises.push(
|
||||
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,
|
||||
label: ds.name,
|
||||
description: '',
|
||||
type: 'datasource',
|
||||
pluginId: ds.meta.id,
|
||||
pluginName: ds.meta.name,
|
||||
};
|
||||
|
||||
obj.datasource = '${' + refName + '}';
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const processPanel = panel => {
|
||||
if (panel.datasource !== undefined) {
|
||||
templateizeDatasourceUsage(panel);
|
||||
}
|
||||
|
||||
if (panel.targets) {
|
||||
for (const target of panel.targets) {
|
||||
if (target.datasource !== undefined) {
|
||||
templateizeDatasourceUsage(target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const panelDef = config.panels[panel.type];
|
||||
if (panelDef) {
|
||||
requires['panel' + panelDef.id] = {
|
||||
type: 'panel',
|
||||
id: panelDef.id,
|
||||
name: panelDef.name,
|
||||
version: panelDef.info.version,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// check up panel data sources
|
||||
for (const panel of saveModel.panels) {
|
||||
processPanel(panel);
|
||||
|
||||
// handle collapsed rows
|
||||
if (panel.collapsed !== undefined && panel.collapsed === true && panel.panels) {
|
||||
for (const rowPanel of panel.panels) {
|
||||
processPanel(rowPanel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// templatize template vars
|
||||
for (const variable of saveModel.templating.list) {
|
||||
if (variable.type === 'query') {
|
||||
templateizeDatasourceUsage(variable);
|
||||
variable.options = [];
|
||||
variable.current = {};
|
||||
variable.refresh = variable.refresh > 0 ? variable.refresh : 1;
|
||||
}
|
||||
}
|
||||
|
||||
// templatize annotations vars
|
||||
for (const annotationDef of saveModel.annotations.list) {
|
||||
templateizeDatasourceUsage(annotationDef);
|
||||
}
|
||||
|
||||
// add grafana version
|
||||
requires['grafana'] = {
|
||||
type: 'grafana',
|
||||
id: 'grafana',
|
||||
name: 'Grafana',
|
||||
version: config.buildInfo.version,
|
||||
};
|
||||
|
||||
return Promise.all(promises)
|
||||
.then(() => {
|
||||
_.each(datasources, (value, key) => {
|
||||
inputs.push(value);
|
||||
});
|
||||
|
||||
// templatize constants
|
||||
for (const variable of saveModel.templating.list) {
|
||||
if (variable.type === 'constant') {
|
||||
const refName = 'VAR_' + variable.name.replace(' ', '_').toUpperCase();
|
||||
inputs.push({
|
||||
name: refName,
|
||||
type: 'constant',
|
||||
label: variable.label || variable.name,
|
||||
value: variable.current.value,
|
||||
description: '',
|
||||
});
|
||||
// update current and option
|
||||
variable.query = '${' + refName + '}';
|
||||
variable.options[0] = variable.current = {
|
||||
value: variable.query,
|
||||
text: variable.query,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// make inputs and requires a top thing
|
||||
const newObj = {};
|
||||
newObj['__inputs'] = inputs;
|
||||
newObj['__requires'] = _.sortBy(requires, ['id']);
|
||||
|
||||
_.defaults(newObj, saveModel);
|
||||
return newObj;
|
||||
})
|
||||
.catch(err => {
|
||||
console.log('Export failed:', err);
|
||||
return {
|
||||
error: err,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { DashboardExporter } from './DashboardExporter';
|
||||
export { DashExportCtrl } from './DashExportCtrl';
|
||||
@@ -0,0 +1,25 @@
|
||||
<div class="share-modal-header">
|
||||
<div class="share-modal-big-icon">
|
||||
<i class="fa fa-cloud-upload"></i>
|
||||
</div>
|
||||
<div>
|
||||
<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.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.viewJson()">
|
||||
<i class="fa fa-file-text-o"></i> View JSON
|
||||
</button>
|
||||
<a class="btn btn-link" ng-click="ctrl.dismiss()">Cancel</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user