Merge branch 'master' into 10630_folder_api

This commit is contained in:
Marcus Efraimsson
2018-02-12 15:14:41 +01:00
123 changed files with 1773 additions and 790 deletions
+1 -7
View File
@@ -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;
-1
View File
@@ -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' },
+3 -7
View File
@@ -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() {
+3 -1
View File
@@ -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(
-4
View File
@@ -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) {
+2 -2
View File
@@ -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),
+64
View File
@@ -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,
+9 -4
View File
@@ -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');
});
});
});
+7
View File
@@ -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');
});
});
+53 -6
View File
@@ -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');
}