mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'master' into 10630_folder_api
This commit is contained in:
@@ -20,11 +20,5 @@ export function registerAngularDirectives() {
|
||||
['tagOptions', { watchDepth: 'reference' }],
|
||||
]);
|
||||
react2AngularDirective('selectUserPicker', UserPicker, ['backendSrv', 'handlePicked']);
|
||||
react2AngularDirective('dashboardPermissions', DashboardPermissions, [
|
||||
'backendSrv',
|
||||
'dashboardId',
|
||||
'folderTitle',
|
||||
'folderSlug',
|
||||
'folderId',
|
||||
]);
|
||||
react2AngularDirective('dashboardPermissions', DashboardPermissions, ['backendSrv', 'dashboardId', 'folder']);
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ describe('AddPermissions', () => {
|
||||
}
|
||||
);
|
||||
|
||||
wrapper = shallow(<AddPermissions permissions={store.permissions} backendSrv={backendSrv} dashboardId={1} />);
|
||||
wrapper = shallow(<AddPermissions permissions={store.permissions} backendSrv={backendSrv} />);
|
||||
instance = wrapper.instance();
|
||||
return store.permissions.load(1, true, false);
|
||||
});
|
||||
|
||||
@@ -9,7 +9,6 @@ import { permissionOptions } from 'app/stores/PermissionsStore/PermissionsStore'
|
||||
export interface IProps {
|
||||
permissions: any;
|
||||
backendSrv: any;
|
||||
dashboardId: any;
|
||||
}
|
||||
@observer
|
||||
class AddPermissions extends Component<IProps, any> {
|
||||
@@ -31,12 +30,6 @@ class AddPermissions extends Component<IProps, any> {
|
||||
const { value } = evt.target;
|
||||
const { permissions } = this.props;
|
||||
|
||||
// if (value === 'Viewer' || value === 'Editor') {
|
||||
// // permissions.addStoreItem({ permission: 1, role: value, dashboardId: dashboardId }, dashboardId);
|
||||
// // this.resetNewType();
|
||||
// return;
|
||||
// }
|
||||
|
||||
permissions.setNewType(value);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,12 +6,11 @@ import Tooltip from 'app/core/components/Tooltip/Tooltip';
|
||||
import PermissionsInfo from 'app/core/components/Permissions/PermissionsInfo';
|
||||
import AddPermissions from 'app/core/components/Permissions/AddPermissions';
|
||||
import SlideDown from 'app/core/components/Animations/SlideDown';
|
||||
import { FolderInfo } from './FolderInfo';
|
||||
|
||||
export interface IProps {
|
||||
dashboardId: number;
|
||||
folderId: number;
|
||||
folderTitle: string;
|
||||
folderSlug: string;
|
||||
folder?: FolderInfo;
|
||||
backendSrv: any;
|
||||
}
|
||||
@observer
|
||||
@@ -28,8 +27,12 @@ class DashboardPermissions extends Component<IProps, any> {
|
||||
this.permissions.toggleAddPermissions();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.permissions.hideAddPermissions();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { dashboardId, folderTitle, folderSlug, folderId, backendSrv } = this.props;
|
||||
const { dashboardId, folder, backendSrv } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -50,13 +53,13 @@ class DashboardPermissions extends Component<IProps, any> {
|
||||
</div>
|
||||
</div>
|
||||
<SlideDown in={this.permissions.isAddPermissionsVisible}>
|
||||
<AddPermissions permissions={this.permissions} backendSrv={backendSrv} dashboardId={dashboardId} />
|
||||
<AddPermissions permissions={this.permissions} backendSrv={backendSrv} />
|
||||
</SlideDown>
|
||||
<Permissions
|
||||
permissions={this.permissions}
|
||||
isFolder={false}
|
||||
dashboardId={dashboardId}
|
||||
folderInfo={{ title: folderTitle, slug: folderSlug, id: folderId }}
|
||||
folderInfo={folder}
|
||||
backendSrv={backendSrv}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export interface FolderInfo {
|
||||
title: string;
|
||||
id: number;
|
||||
slug: string;
|
||||
title: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ export default observer(({ item, removeItem, permissionChanged, itemIndex, folde
|
||||
folderInfo && (
|
||||
<em className="muted no-wrap">
|
||||
Inherited from folder{' '}
|
||||
<a className="text-link" href={`dashboards/folder/${folderInfo.id}/${folderInfo.slug}/permissions`}>
|
||||
<a className="text-link" href={`${folderInfo.url}/permissions`}>
|
||||
{folderInfo.title}
|
||||
</a>{' '}
|
||||
</em>
|
||||
|
||||
@@ -31,7 +31,7 @@ class UserPicker extends Component<IProps, any> {
|
||||
|
||||
this.debouncedSearch = debounce(this.search, 300, {
|
||||
leading: true,
|
||||
trailing: false,
|
||||
trailing: true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -39,10 +39,10 @@ class UserPicker extends Component<IProps, any> {
|
||||
const { toggleLoading, backendSrv } = this.props;
|
||||
|
||||
toggleLoading(true);
|
||||
return backendSrv.get(`/api/users/search?perpage=10&page=1&query=${query}`).then(result => {
|
||||
const users = result.users.map(user => {
|
||||
return backendSrv.get(`/api/org/users?query=${query}&limit=10`).then(result => {
|
||||
const users = result.map(user => {
|
||||
return {
|
||||
id: user.id,
|
||||
id: user.userId,
|
||||
label: `${user.login} - ${user.email}`,
|
||||
avatarUrl: user.avatarUrl,
|
||||
login: user.login,
|
||||
|
||||
@@ -7,7 +7,6 @@ export interface Props {
|
||||
}
|
||||
|
||||
export default class ScrollBar extends React.Component<Props, any> {
|
||||
|
||||
private container: any;
|
||||
private ps: PerfectScrollbar;
|
||||
|
||||
@@ -16,7 +15,9 @@ export default class ScrollBar extends React.Component<Props, any> {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.ps = new PerfectScrollbar(this.container);
|
||||
this.ps = new PerfectScrollbar(this.container, {
|
||||
wheelPropagation: true,
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
|
||||
@@ -34,6 +34,7 @@ export class FormDropdownCtrl {
|
||||
lookupText: boolean;
|
||||
placeholder: any;
|
||||
startOpen: any;
|
||||
debounce: number;
|
||||
|
||||
/** @ngInject **/
|
||||
constructor(private $scope, $element, private $sce, private templateSrv, private $q) {
|
||||
@@ -72,6 +73,10 @@ export class FormDropdownCtrl {
|
||||
this.source(this.query, this.process.bind(this));
|
||||
};
|
||||
|
||||
if (this.debounce) {
|
||||
typeahead.lookup = _.debounce(typeahead.lookup, 500, { leading: true });
|
||||
}
|
||||
|
||||
this.linkElement.keydown(evt => {
|
||||
// trigger typeahead on down arrow or enter key
|
||||
if (evt.keyCode === 40 || evt.keyCode === 13) {
|
||||
@@ -263,6 +268,7 @@ export function formDropdownDirective() {
|
||||
lookupText: '@',
|
||||
placeholder: '@',
|
||||
startOpen: '@',
|
||||
debounce: '@',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -83,6 +83,10 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop
|
||||
body.toggleClass('sidemenu-hidden');
|
||||
});
|
||||
|
||||
scope.$watch(() => playlistSrv.isPlaying, function(newValue) {
|
||||
elem.toggleClass('playlist-active', newValue === true);
|
||||
});
|
||||
|
||||
// tooltip removal fix
|
||||
// manage page classes
|
||||
var pageClass;
|
||||
|
||||
@@ -19,7 +19,6 @@ export class HelpCtrl {
|
||||
],
|
||||
Dashboard: [
|
||||
{ keys: ['mod+s'], description: 'Save dashboard' },
|
||||
{ keys: ['mod+h'], description: 'Hide row controls' },
|
||||
{ keys: ['d', 'r'], description: 'Refresh all panels' },
|
||||
{ keys: ['d', 's'], description: 'Dashboard settings' },
|
||||
{ keys: ['d', 'v'], description: 'Toggle in-active / view mode' },
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import coreModule from 'app/core/core_module';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import config from 'app/core/config';
|
||||
|
||||
const template = `
|
||||
<div class="modal-body">
|
||||
@@ -60,16 +61,11 @@ export class OrgSwitchCtrl {
|
||||
|
||||
setUsingOrg(org) {
|
||||
return this.backendSrv.post('/api/user/using/' + org.orgId).then(() => {
|
||||
const re = /orgId=\d+/gi;
|
||||
this.setWindowLocationHref(this.getWindowLocationHref().replace(re, 'orgId=' + org.orgId));
|
||||
this.setWindowLocation(config.appSubUrl + (config.appSubUrl.endsWith('/') ? '' : '/') + '?orgId=' + org.orgId);
|
||||
});
|
||||
}
|
||||
|
||||
getWindowLocationHref() {
|
||||
return window.location.href;
|
||||
}
|
||||
|
||||
setWindowLocationHref(href: string) {
|
||||
setWindowLocation(href: string) {
|
||||
window.location.href = href;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,11 +23,13 @@ export function queryPartEditorDirective($compile, templateSrv) {
|
||||
scope: {
|
||||
part: '=',
|
||||
handleEvent: '&',
|
||||
debounce: '@',
|
||||
},
|
||||
link: function postLink($scope, elem) {
|
||||
var part = $scope.part;
|
||||
var partDef = part.def;
|
||||
var $paramsContainer = elem.find('.query-part-parameters');
|
||||
var debounceLookup = $scope.debounce;
|
||||
|
||||
$scope.partActions = [];
|
||||
|
||||
@@ -128,6 +130,10 @@ export function queryPartEditorDirective($compile, templateSrv) {
|
||||
var items = this.source(this.query, $.proxy(this.process, this));
|
||||
return items ? this.process(items) : items;
|
||||
};
|
||||
|
||||
if (debounceLookup) {
|
||||
typeahead.lookup = _.debounce(typeahead.lookup, 500, { leading: true });
|
||||
}
|
||||
}
|
||||
|
||||
$scope.showActionsMenu = function() {
|
||||
|
||||
@@ -6,7 +6,9 @@ export function geminiScrollbar() {
|
||||
return {
|
||||
restrict: 'A',
|
||||
link: function(scope, elem, attrs) {
|
||||
let scrollbar = new PerfectScrollbar(elem[0]);
|
||||
let scrollbar = new PerfectScrollbar(elem[0], {
|
||||
wheelPropagation: true,
|
||||
});
|
||||
let lastPos = 0;
|
||||
|
||||
appEvents.on(
|
||||
|
||||
@@ -18,10 +18,6 @@ function (_, $, coreModule) {
|
||||
elem.toggleClass('panel-in-fullscreen', false);
|
||||
});
|
||||
|
||||
$scope.$watch('ctrl.playlistSrv.isPlaying', function(newValue) {
|
||||
elem.toggleClass('playlist-active', newValue === true);
|
||||
});
|
||||
|
||||
$scope.$watch('ctrl.dashboardViewState.state.editview', function(newValue) {
|
||||
if (newValue) {
|
||||
elem.toggleClass('dashboard-page--settings-opening', _.isString(newValue));
|
||||
|
||||
@@ -22,6 +22,7 @@ function (_, $, coreModule) {
|
||||
segment: "=",
|
||||
getOptions: "&",
|
||||
onChange: "&",
|
||||
debounce: "@",
|
||||
},
|
||||
link: function($scope, elem) {
|
||||
var $input = $(inputTemplate);
|
||||
@@ -30,6 +31,7 @@ function (_, $, coreModule) {
|
||||
var options = null;
|
||||
var cancelBlur = null;
|
||||
var linkMode = true;
|
||||
var debounceLookup = $scope.debounce;
|
||||
|
||||
$input.appendTo(elem);
|
||||
$button.appendTo(elem);
|
||||
@@ -135,6 +137,10 @@ function (_, $, coreModule) {
|
||||
return items ? this.process(items) : items;
|
||||
};
|
||||
|
||||
if (debounceLookup) {
|
||||
typeahead.lookup = _.debounce(typeahead.lookup, 500, {leading: true});
|
||||
}
|
||||
|
||||
$button.keydown(function(evt) {
|
||||
// trigger typeahead on down arrow or enter key
|
||||
if (evt.keyCode === 40 || evt.keyCode === 13) {
|
||||
|
||||
@@ -150,9 +150,9 @@ export class SearchSrv {
|
||||
if (hit.folderId) {
|
||||
section = {
|
||||
id: hit.folderId,
|
||||
uid: hit.uid,
|
||||
uid: hit.folderUid,
|
||||
title: hit.folderTitle,
|
||||
url: hit.url,
|
||||
url: hit.folderUrl,
|
||||
items: [],
|
||||
icon: 'fa fa-folder-open',
|
||||
toggle: this.toggleFolder.bind(this),
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import * as fileExport from '../utils/file_export';
|
||||
import { beforeEach, expect } from 'test/lib/common';
|
||||
|
||||
describe('file_export', () => {
|
||||
let ctx: any = {};
|
||||
|
||||
beforeEach(() => {
|
||||
ctx.seriesList = [
|
||||
{
|
||||
alias: 'series_1',
|
||||
datapoints: [
|
||||
[1, 1500026100000],
|
||||
[2, 1500026200000],
|
||||
[null, 1500026300000],
|
||||
[null, 1500026400000],
|
||||
[null, 1500026500000],
|
||||
[6, 1500026600000],
|
||||
],
|
||||
},
|
||||
{
|
||||
alias: 'series_2',
|
||||
datapoints: [[11, 1500026100000], [12, 1500026200000], [13, 1500026300000], [15, 1500026500000]],
|
||||
},
|
||||
];
|
||||
|
||||
ctx.timeFormat = 'X'; // Unix timestamp (seconds)
|
||||
});
|
||||
|
||||
describe('when exporting series as rows', () => {
|
||||
it('should export points in proper order', () => {
|
||||
let text = fileExport.convertSeriesListToCsv(ctx.seriesList, ctx.timeFormat);
|
||||
const expectedText =
|
||||
'Series;Time;Value\n' +
|
||||
'series_1;1500026100;1\n' +
|
||||
'series_1;1500026200;2\n' +
|
||||
'series_1;1500026300;null\n' +
|
||||
'series_1;1500026400;null\n' +
|
||||
'series_1;1500026500;null\n' +
|
||||
'series_1;1500026600;6\n' +
|
||||
'series_2;1500026100;11\n' +
|
||||
'series_2;1500026200;12\n' +
|
||||
'series_2;1500026300;13\n' +
|
||||
'series_2;1500026500;15\n';
|
||||
|
||||
expect(text).toBe(expectedText);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when exporting series as columns', () => {
|
||||
it('should export points in proper order', () => {
|
||||
let text = fileExport.convertSeriesListToCsvColumns(ctx.seriesList, ctx.timeFormat);
|
||||
const expectedText =
|
||||
'Time;series_1;series_2\n' +
|
||||
'1500026100;1;11\n' +
|
||||
'1500026200;2;12\n' +
|
||||
'1500026300;null;13\n' +
|
||||
'1500026400;null;null\n' +
|
||||
'1500026500;null;15\n' +
|
||||
'1500026600;6;null\n';
|
||||
|
||||
expect(text).toBe(expectedText);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -20,9 +20,6 @@ describe('ManageDashboards', () => {
|
||||
icon: 'fa fa-folder',
|
||||
tags: [],
|
||||
isStarred: false,
|
||||
folderId: 410,
|
||||
folderTitle: 'afolder',
|
||||
folderSlug: 'afolder',
|
||||
},
|
||||
],
|
||||
tags: [],
|
||||
@@ -77,9 +74,6 @@ describe('ManageDashboards', () => {
|
||||
icon: 'fa fa-folder',
|
||||
tags: [],
|
||||
isStarred: false,
|
||||
folderId: 410,
|
||||
folderTitle: 'afolder',
|
||||
folderSlug: 'afolder',
|
||||
},
|
||||
],
|
||||
tags: [],
|
||||
@@ -112,8 +106,9 @@ describe('ManageDashboards', () => {
|
||||
tags: [],
|
||||
isStarred: false,
|
||||
folderId: 410,
|
||||
folderTitle: 'afolder',
|
||||
folderSlug: 'afolder',
|
||||
folderUid: 'uid',
|
||||
folderTitle: 'Folder',
|
||||
folderUrl: '/dashboards/f/uid/folder',
|
||||
},
|
||||
{
|
||||
id: 500,
|
||||
|
||||
@@ -7,6 +7,12 @@ jest.mock('app/core/services/context_srv', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('app/core/config', () => {
|
||||
return {
|
||||
appSubUrl: '/subUrl',
|
||||
};
|
||||
});
|
||||
|
||||
describe('OrgSwitcher', () => {
|
||||
describe('when switching org', () => {
|
||||
let expectedHref;
|
||||
@@ -25,8 +31,7 @@ describe('OrgSwitcher', () => {
|
||||
|
||||
const orgSwitcherCtrl = new OrgSwitchCtrl(backendSrvStub);
|
||||
|
||||
orgSwitcherCtrl.getWindowLocationHref = () => 'http://localhost:3000?orgId=1&from=now-3h&to=now';
|
||||
orgSwitcherCtrl.setWindowLocationHref = href => (expectedHref = href);
|
||||
orgSwitcherCtrl.setWindowLocation = href => (expectedHref = href);
|
||||
|
||||
return orgSwitcherCtrl.setUsingOrg({ orgId: 2 });
|
||||
});
|
||||
@@ -35,8 +40,8 @@ describe('OrgSwitcher', () => {
|
||||
expect(expectedUsingUrl).toBe('/api/user/using/2');
|
||||
});
|
||||
|
||||
it('should switch orgId in url', () => {
|
||||
expect(expectedHref).toBe('http://localhost:3000?orgId=2&from=now-3h&to=now');
|
||||
it('should switch orgId in url and redirect to home page', () => {
|
||||
expect(expectedHref).toBe('/subUrl/?orgId=2');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -190,7 +190,9 @@ describe('SearchSrv', () => {
|
||||
title: 'dash in folder1 1',
|
||||
type: 'dash-db',
|
||||
folderId: 1,
|
||||
folderUid: 'uid',
|
||||
folderTitle: 'folder1',
|
||||
folderUrl: '/dashboards/f/uid/folder1',
|
||||
},
|
||||
])
|
||||
);
|
||||
@@ -206,6 +208,11 @@ describe('SearchSrv', () => {
|
||||
|
||||
it('should group results by folder', () => {
|
||||
expect(results).toHaveLength(2);
|
||||
expect(results[0].id).toEqual(0);
|
||||
expect(results[1].id).toEqual(1);
|
||||
expect(results[1].uid).toEqual('uid');
|
||||
expect(results[1].title).toEqual('folder1');
|
||||
expect(results[1].url).toEqual('/dashboards/f/uid/folder1');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -3,19 +3,27 @@ import moment from 'moment';
|
||||
import { saveAs } from 'file-saver';
|
||||
|
||||
const DEFAULT_DATETIME_FORMAT = 'YYYY-MM-DDTHH:mm:ssZ';
|
||||
const POINT_TIME_INDEX = 1;
|
||||
const POINT_VALUE_INDEX = 0;
|
||||
|
||||
export function exportSeriesListToCsv(seriesList, dateTimeFormat = DEFAULT_DATETIME_FORMAT, excel = false) {
|
||||
export function convertSeriesListToCsv(seriesList, dateTimeFormat = DEFAULT_DATETIME_FORMAT, excel = false) {
|
||||
var text = (excel ? 'sep=;\n' : '') + 'Series;Time;Value\n';
|
||||
_.each(seriesList, function(series) {
|
||||
_.each(series.datapoints, function(dp) {
|
||||
text += series.alias + ';' + moment(dp[1]).format(dateTimeFormat) + ';' + dp[0] + '\n';
|
||||
text +=
|
||||
series.alias + ';' + moment(dp[POINT_TIME_INDEX]).format(dateTimeFormat) + ';' + dp[POINT_VALUE_INDEX] + '\n';
|
||||
});
|
||||
});
|
||||
return text;
|
||||
}
|
||||
|
||||
export function exportSeriesListToCsv(seriesList, dateTimeFormat = DEFAULT_DATETIME_FORMAT, excel = false) {
|
||||
var text = convertSeriesListToCsv(seriesList, dateTimeFormat, excel);
|
||||
saveSaveBlob(text, 'grafana_data_export.csv');
|
||||
}
|
||||
|
||||
export function exportSeriesListToCsvColumns(seriesList, dateTimeFormat = DEFAULT_DATETIME_FORMAT, excel = false) {
|
||||
var text = (excel ? 'sep=;\n' : '') + 'Time;';
|
||||
export function convertSeriesListToCsvColumns(seriesList, dateTimeFormat = DEFAULT_DATETIME_FORMAT, excel = false) {
|
||||
let text = (excel ? 'sep=;\n' : '') + 'Time;';
|
||||
// add header
|
||||
_.each(seriesList, function(series) {
|
||||
text += series.alias + ';';
|
||||
@@ -24,14 +32,15 @@ export function exportSeriesListToCsvColumns(seriesList, dateTimeFormat = DEFAUL
|
||||
text += '\n';
|
||||
|
||||
// process data
|
||||
seriesList = mergeSeriesByTime(seriesList);
|
||||
var dataArr = [[]];
|
||||
var sIndex = 1;
|
||||
_.each(seriesList, function(series) {
|
||||
var cIndex = 0;
|
||||
dataArr.push([]);
|
||||
_.each(series.datapoints, function(dp) {
|
||||
dataArr[0][cIndex] = moment(dp[1]).format(dateTimeFormat);
|
||||
dataArr[sIndex][cIndex] = dp[0];
|
||||
dataArr[0][cIndex] = moment(dp[POINT_TIME_INDEX]).format(dateTimeFormat);
|
||||
dataArr[sIndex][cIndex] = dp[POINT_VALUE_INDEX];
|
||||
cIndex++;
|
||||
});
|
||||
sIndex++;
|
||||
@@ -46,6 +55,44 @@ export function exportSeriesListToCsvColumns(seriesList, dateTimeFormat = DEFAUL
|
||||
text = text.substring(0, text.length - 1);
|
||||
text += '\n';
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect all unique timestamps from series list and use it to fill
|
||||
* missing points by null.
|
||||
*/
|
||||
function mergeSeriesByTime(seriesList) {
|
||||
let timestamps = [];
|
||||
for (let i = 0; i < seriesList.length; i++) {
|
||||
let seriesPoints = seriesList[i].datapoints;
|
||||
for (let j = 0; j < seriesPoints.length; j++) {
|
||||
timestamps.push(seriesPoints[j][POINT_TIME_INDEX]);
|
||||
}
|
||||
}
|
||||
timestamps = _.sortedUniq(timestamps.sort());
|
||||
|
||||
for (let i = 0; i < seriesList.length; i++) {
|
||||
let seriesPoints = seriesList[i].datapoints;
|
||||
let seriesTimestamps = _.map(seriesPoints, p => p[POINT_TIME_INDEX]);
|
||||
let extendedSeries = [];
|
||||
let pointIndex;
|
||||
for (let j = 0; j < timestamps.length; j++) {
|
||||
pointIndex = _.sortedIndexOf(seriesTimestamps, timestamps[j]);
|
||||
if (pointIndex !== -1) {
|
||||
extendedSeries.push(seriesPoints[pointIndex]);
|
||||
} else {
|
||||
extendedSeries.push([null, timestamps[j]]);
|
||||
}
|
||||
}
|
||||
seriesList[i].datapoints = extendedSeries;
|
||||
}
|
||||
return seriesList;
|
||||
}
|
||||
|
||||
export function exportSeriesListToCsvColumns(seriesList, dateTimeFormat = DEFAULT_DATETIME_FORMAT, excel = false) {
|
||||
let text = convertSeriesListToCsvColumns(seriesList, dateTimeFormat, excel);
|
||||
saveSaveBlob(text, 'grafana_data_export.csv');
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user