mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'master' into poc_token_auth
* master: (32 commits) Fixed react key warning for loki start page Disable query should trigger refresh added docs entry for check_for_updates config flag, fixes ##14940 Explore: Fix scanning for logs Moved ad hoc filters and upload directive Moved dashboard srv and snapshot ctrl Moved share modal Moved dashboard save modals to components folder Moved unsaved changes service and modal Removed unused alertingSrv Moved view state srv to services Moved timepicker to components Moved submenu into components dir Moved dashboard settings to components Moved dashboard permissions into components dir Moved history component, added start draft of frontend code style guide fix: Use custom whitelist for XSS sanitizer to allow class and style attributes Began work on improving structure and organization of components under features/dashboard, #14062 Fix a typo in changelog Update ROADMAP.md ...
This commit is contained in:
commit
b5572b23b6
@ -12,7 +12,7 @@
|
||||
* **Auth**: Prevent password reset when login form is disabled or either LDAP or Auth Proxy is enabled [#14246](https://github.com/grafana/grafana/issues/14246), thx [@SilverFire](https://github.com/SilverFire)
|
||||
* **Dataproxy**: Override incoming Authorization header [#13815](https://github.com/grafana/grafana/issues/13815), thx [@kornholi](https://github.com/kornholi)
|
||||
* **Admin**: Fix prevent removing last grafana admin permissions [#11067](https://github.com/grafana/grafana/issues/11067), thx [@danielbh](https://github.com/danielbh)
|
||||
* **Templating**: Escaping "Custom" template variables [#13754](https://github.com/grafana/grafana/issues/13754), thx [@IntegersOfK]req(https://github.com/IntegersOfK)
|
||||
* **Templating**: Escaping "Custom" template variables [#13754](https://github.com/grafana/grafana/issues/13754), thx [@IntegersOfK](https://github.com/IntegersOfK)
|
||||
* **Admin**: When multiple user invitations, all links are the same as the first user who was invited [#14483](https://github.com/grafana/grafana/issues/14483)
|
||||
* **LDAP**: Upgrade go-ldap to v3 [#14548](https://github.com/grafana/grafana/issues/14548)
|
||||
* **Proxy whitelist**: Add CIDR capability to auth_proxy whitelist [#14546](https://github.com/grafana/grafana/issues/14546), thx [@jacobrichard](https://github.com/jacobrichard)
|
||||
|
18
ROADMAP.md
18
ROADMAP.md
@ -5,18 +5,22 @@ But it will give you an idea of our current vision and plan.
|
||||
|
||||
### Short term (1-2 months)
|
||||
- PRs & Bugs
|
||||
- Multi-Stat panel
|
||||
- React Panel Support
|
||||
- React Query Editor Support
|
||||
- Metrics & Log Explore UI
|
||||
|
||||
- Grafana UI library shared between grafana & plugins
|
||||
- Seperate visualization from panels
|
||||
- More reuse between Explore & dashboard
|
||||
- Explore logging support for more data sources
|
||||
|
||||
### Mid term (2-4 months)
|
||||
- React Panels
|
||||
- Change visualization (panel type) on the fly.
|
||||
- Templating Query Editor UI Plugin hook
|
||||
- Backend plugins
|
||||
- Drilldown links
|
||||
- Dashboards as code workflows
|
||||
- React migration
|
||||
- New panels
|
||||
|
||||
### Long term (4 - 8 months)
|
||||
- Alerting improvements (silence, per series tracking, etc)
|
||||
- Progress on React migration
|
||||
|
||||
### In a distant future far far away
|
||||
- Meta queries
|
||||
|
@ -188,8 +188,8 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
"defaultRegion": "us-west-1"
|
||||
},
|
||||
"secureJsonData": {
|
||||
"accessKey": "Ol4pIDpeKSA6XikgOl4p", //should not be encoded
|
||||
"secretKey": "dGVzdCBrZXkgYmxlYXNlIGRvbid0IHN0ZWFs" //should be Base-64 encoded
|
||||
"accessKey": "Ol4pIDpeKSA6XikgOl4p",
|
||||
"secretKey": "dGVzdCBrZXkgYmxlYXNlIGRvbid0IHN0ZWFs"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
@ -391,6 +391,12 @@ value is `true`.
|
||||
If you want to track Grafana usage via Google analytics specify *your* Universal
|
||||
Analytics ID here. By default this feature is disabled.
|
||||
|
||||
### check_for_updates
|
||||
|
||||
Set to false to disable all checks to https://grafana.com for new versions of Grafana and installed plugins. Check is used
|
||||
in some UI views to notify that a Grafana or plugin update exists. This option does not cause any auto updates, nor
|
||||
send any sensitive information.
|
||||
|
||||
<hr />
|
||||
|
||||
## [dashboards]
|
||||
|
@ -52,6 +52,7 @@ Filter Option | Example | Raw | Interpolated | Description
|
||||
`csv`| ${servers:csv} | `'test1', 'test2'` | `test1,test2` | Formats multi-value variable as a comma-separated string
|
||||
`distributed`| ${servers:distributed} | `'test1', 'test2'` | `test1,servers=test2` | Formats multi-value variable in custom format for OpenTSDB.
|
||||
`lucene`| ${servers:lucene} | `'test', 'test2'` | `("test" OR "test2")` | Formats multi-value variable as a lucene expression.
|
||||
`percentencode` | ${servers:percentencode} | `'foo()bar BAZ', 'test2'` | `{foo%28%29bar%20BAZ%2Ctest2}` | Formats multi-value variable into a glob, percent-encoded.
|
||||
|
||||
Test the formatting options on the [Grafana Play site](http://play.grafana.org/d/cJtIfcWiz/template-variable-formatting-options?orgId=1).
|
||||
|
||||
|
@ -336,7 +336,7 @@ func addGettingStartedPanelToHomeDashboard(dash *simplejson.Json) {
|
||||
"id": 123123,
|
||||
"gridPos": map[string]interface{}{
|
||||
"x": 0,
|
||||
"y": 3,
|
||||
"y": 0,
|
||||
"w": 24,
|
||||
"h": 4,
|
||||
},
|
||||
|
@ -236,7 +236,7 @@ export class KeybindingSrv {
|
||||
shareScope.dashboard = dashboard;
|
||||
|
||||
appEvents.emit('show-modal', {
|
||||
src: 'public/app/features/dashboard/partials/shareModal.html',
|
||||
src: 'public/app/features/dashboard/components/ShareModal/template.html',
|
||||
scope: shareScope,
|
||||
});
|
||||
}
|
||||
|
@ -44,9 +44,25 @@ export function findMatchesInText(haystack: string, needle: string): TextMatch[]
|
||||
return matches;
|
||||
}
|
||||
|
||||
const XSSWL = Object.keys(xss.whiteList).reduce((acc, element) => {
|
||||
acc[element] = xss.whiteList[element].concat(['class', 'style']);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const sanitizeXSS = new xss.FilterXSS({
|
||||
whiteList: XSSWL
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns string safe from XSS attacks.
|
||||
*
|
||||
* Even though we allow the style-attribute, there's still default filtering applied to it
|
||||
* Info: https://github.com/leizongmin/js-xss#customize-css-filter
|
||||
* Whitelist: https://github.com/leizongmin/js-css-filter/blob/master/lib/default.js
|
||||
*/
|
||||
export function sanitize (unsanitizedString: string): string {
|
||||
try {
|
||||
return xss(unsanitizedString);
|
||||
return sanitizeXSS.process(unsanitizedString);
|
||||
} catch (error) {
|
||||
console.log('String could not be sanitized', unsanitizedString);
|
||||
return unsanitizedString;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import './annotations/all';
|
||||
import './templating/all';
|
||||
import './plugins/all';
|
||||
import './dashboard/all';
|
||||
import './dashboard';
|
||||
import './playlist/all';
|
||||
import './panel/all';
|
||||
import './org/all';
|
||||
|
@ -1,13 +0,0 @@
|
||||
import coreModule from 'app/core/core_module';
|
||||
|
||||
export class AlertingSrv {
|
||||
dashboard: any;
|
||||
alerts: any[];
|
||||
|
||||
init(dashboard, alerts) {
|
||||
this.dashboard = dashboard;
|
||||
this.alerts = alerts || [];
|
||||
}
|
||||
}
|
||||
|
||||
coreModule.service('alertingSrv', AlertingSrv);
|
@ -1,45 +0,0 @@
|
||||
import './dashboard_ctrl';
|
||||
import './alerting_srv';
|
||||
import './history/history';
|
||||
import './dashboard_loader_srv';
|
||||
import './dashnav/dashnav';
|
||||
import './submenu/submenu';
|
||||
import './save_as_modal';
|
||||
import './save_modal';
|
||||
import './save_provisioned_modal';
|
||||
import './shareModalCtrl';
|
||||
import './share_snapshot_ctrl';
|
||||
import './dashboard_srv';
|
||||
import './view_state_srv';
|
||||
import './validation_srv';
|
||||
import './time_srv';
|
||||
import './unsaved_changes_srv';
|
||||
import './unsaved_changes_modal';
|
||||
import './timepicker/timepicker';
|
||||
import './upload';
|
||||
import './export/export_modal';
|
||||
import './export_data/export_data_modal';
|
||||
import './ad_hoc_filters';
|
||||
import './repeat_option/repeat_option';
|
||||
import './dashgrid/DashboardGridDirective';
|
||||
import './dashgrid/RowOptions';
|
||||
import './folder_picker/folder_picker';
|
||||
import './move_to_folder_modal/move_to_folder';
|
||||
import './settings/settings';
|
||||
import './panellinks/module';
|
||||
import './dashlinks/module';
|
||||
|
||||
// angular wrappers
|
||||
import { react2AngularDirective } from 'app/core/utils/react2angular';
|
||||
import DashboardPermissions from './permissions/DashboardPermissions';
|
||||
|
||||
react2AngularDirective('dashboardPermissions', DashboardPermissions, ['dashboardId', 'folder']);
|
||||
|
||||
import coreModule from 'app/core/core_module';
|
||||
import { FolderDashboardsCtrl } from './folder_dashboards_ctrl';
|
||||
import { DashboardImportCtrl } from './dashboard_import_ctrl';
|
||||
import { CreateFolderCtrl } from './create_folder_ctrl';
|
||||
|
||||
coreModule.controller('FolderDashboardsCtrl', FolderDashboardsCtrl);
|
||||
coreModule.controller('DashboardImportCtrl', DashboardImportCtrl);
|
||||
coreModule.controller('CreateFolderCtrl', CreateFolderCtrl);
|
@ -0,0 +1 @@
|
||||
export { AdHocFiltersCtrl } from './AdHocFiltersCtrl';
|
@ -2,7 +2,7 @@ import angular from 'angular';
|
||||
import { saveAs } from 'file-saver';
|
||||
|
||||
import coreModule from 'app/core/core_module';
|
||||
import { DashboardExporter } from './exporter';
|
||||
import { DashboardExporter } from './DashboardExporter';
|
||||
|
||||
export class DashExportCtrl {
|
||||
dash: any;
|
||||
@ -66,7 +66,7 @@ export class DashExportCtrl {
|
||||
export function dashExportDirective() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
templateUrl: 'public/app/features/dashboard/export/export_modal.html',
|
||||
templateUrl: 'public/app/features/dashboard/components/DashExportModal/template.html',
|
||||
controller: DashExportCtrl,
|
||||
bindToController: true,
|
||||
controllerAs: 'ctrl',
|
@ -6,8 +6,8 @@ jest.mock('app/core/store', () => {
|
||||
|
||||
import _ from 'lodash';
|
||||
import config from 'app/core/config';
|
||||
import { DashboardExporter } from '../export/exporter';
|
||||
import { DashboardModel } from '../dashboard_model';
|
||||
import { DashboardExporter } from './DashboardExporter';
|
||||
import { DashboardModel } from '../../dashboard_model';
|
||||
|
||||
describe('given dashboard with repeated panels', () => {
|
||||
let dash, exported;
|
@ -1,6 +1,6 @@
|
||||
import config from 'app/core/config';
|
||||
import _ from 'lodash';
|
||||
import { DashboardModel } from '../dashboard_model';
|
||||
import { DashboardModel } from '../../dashboard_model';
|
||||
|
||||
export class DashboardExporter {
|
||||
constructor(private datasourceSrv) {}
|
@ -0,0 +1,2 @@
|
||||
export { DashboardExporter } from './DashboardExporter';
|
||||
export { DashExportCtrl } from './DashExportCtrl';
|
@ -1,6 +1,6 @@
|
||||
import angular from 'angular';
|
||||
import _ from 'lodash';
|
||||
import { iconMap } from './editor';
|
||||
import { iconMap } from './DashLinksEditorCtrl';
|
||||
|
||||
function dashLinksContainer() {
|
||||
return {
|
@ -11,7 +11,7 @@ export let iconMap = {
|
||||
cloud: 'fa-cloud',
|
||||
};
|
||||
|
||||
export class DashLinkEditorCtrl {
|
||||
export class DashLinksEditorCtrl {
|
||||
dashboard: any;
|
||||
iconMap: any;
|
||||
mode: any;
|
||||
@ -65,8 +65,8 @@ export class DashLinkEditorCtrl {
|
||||
function dashLinksEditor() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
controller: DashLinkEditorCtrl,
|
||||
templateUrl: 'public/app/features/dashboard/dashlinks/editor.html',
|
||||
controller: DashLinksEditorCtrl,
|
||||
templateUrl: 'public/app/features/dashboard/components/DashLinks/editor.html',
|
||||
bindToController: true,
|
||||
controllerAs: 'ctrl',
|
||||
scope: {
|
@ -0,0 +1,2 @@
|
||||
export { DashLinksContainerCtrl } from './DashLinksContainerCtrl';
|
||||
export { DashLinksEditorCtrl } from './DashLinksEditorCtrl';
|
@ -1,7 +1,7 @@
|
||||
import moment from 'moment';
|
||||
import angular from 'angular';
|
||||
import { appEvents, NavModel } from 'app/core/core';
|
||||
import { DashboardModel } from '../dashboard_model';
|
||||
import { DashboardModel } from '../../dashboard_model';
|
||||
|
||||
export class DashNavCtrl {
|
||||
dashboard: DashboardModel;
|
||||
@ -60,7 +60,7 @@ export class DashNavCtrl {
|
||||
modalScope.dashboard = this.dashboard;
|
||||
|
||||
appEvents.emit('show-modal', {
|
||||
src: 'public/app/features/dashboard/partials/shareModal.html',
|
||||
src: 'public/app/features/dashboard/components/ShareModal/template.html',
|
||||
scope: modalScope,
|
||||
});
|
||||
}
|
||||
@ -107,7 +107,7 @@ export class DashNavCtrl {
|
||||
export function dashNavDirective() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
templateUrl: 'public/app/features/dashboard/dashnav/dashnav.html',
|
||||
templateUrl: 'public/app/features/dashboard/components/DashNav/template.html',
|
||||
controller: DashNavCtrl,
|
||||
bindToController: true,
|
||||
controllerAs: 'ctrl',
|
@ -0,0 +1 @@
|
||||
export { DashNavCtrl } from './DashNavCtrl';
|
@ -8,11 +8,11 @@ import {
|
||||
addDashboardPermission,
|
||||
removeDashboardPermission,
|
||||
updateDashboardPermission,
|
||||
} from '../state/actions';
|
||||
} from '../../state/actions';
|
||||
import PermissionList from 'app/core/components/PermissionList/PermissionList';
|
||||
import AddPermission from 'app/core/components/PermissionList/AddPermission';
|
||||
import PermissionsInfo from 'app/core/components/PermissionList/PermissionsInfo';
|
||||
import { connectWithStore } from '../../../core/utils/connectWithReduxStore';
|
||||
import { connectWithStore } from 'app/core/utils/connectWithReduxStore';
|
||||
|
||||
export interface Props {
|
||||
dashboardId: number;
|
@ -1,5 +1,5 @@
|
||||
import { coreModule, appEvents, contextSrv } from 'app/core/core';
|
||||
import { DashboardModel } from '../dashboard_model';
|
||||
import { DashboardModel } from '../../dashboard_model';
|
||||
import $ from 'jquery';
|
||||
import _ from 'lodash';
|
||||
import angular from 'angular';
|
||||
@ -230,7 +230,7 @@ export class SettingsCtrl {
|
||||
export function dashboardSettings() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
templateUrl: 'public/app/features/dashboard/settings/settings.html',
|
||||
templateUrl: 'public/app/features/dashboard/components/DashboardSettings/template.html',
|
||||
controller: SettingsCtrl,
|
||||
bindToController: true,
|
||||
controllerAs: 'ctrl',
|
@ -0,0 +1 @@
|
||||
export { SettingsCtrl } from './SettingsCtrl';
|
@ -51,7 +51,8 @@
|
||||
on-change="ctrl.onFolderChange($folder)"
|
||||
enable-create-new="true"
|
||||
is-valid-selection="true"
|
||||
label-class="width-7">
|
||||
label-class="width-7"
|
||||
dashboard-id="ctrl.dashboard.id">
|
||||
</folder-picker>
|
||||
<gf-form-switch class="gf-form" label="Editable" tooltip="Uncheck, then save and reload to disable all dashboard editing" checked="ctrl.dashboard.editable" label-class="width-7">
|
||||
</gf-form-switch>
|
@ -31,7 +31,7 @@ export class ExportDataModalCtrl {
|
||||
export function exportDataModal() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
templateUrl: 'public/app/features/dashboard/export_data/export_data_modal.html',
|
||||
templateUrl: 'public/app/features/dashboard/components/ExportDataModal/template.html',
|
||||
controller: ExportDataModalCtrl,
|
||||
controllerAs: 'ctrl',
|
||||
scope: {
|
@ -0,0 +1 @@
|
||||
export { ExportDataModalCtrl } from './ExportDataModalCtrl';
|
@ -21,6 +21,7 @@ export class FolderPickerCtrl {
|
||||
hasValidationError: boolean;
|
||||
validationError: any;
|
||||
isEditor: boolean;
|
||||
dashboardId?: number;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private backendSrv, private validationSrv, private contextSrv) {
|
||||
@ -144,7 +145,13 @@ export class FolderPickerCtrl {
|
||||
if (this.isEditor) {
|
||||
folder = rootFolder;
|
||||
} else {
|
||||
folder = result.length > 0 ? result[0] : resetFolder;
|
||||
// We shouldn't assign a random folder without the user actively choosing it on a persisted dashboard
|
||||
const isPersistedDashBoard = this.dashboardId ? true : false;
|
||||
if (isPersistedDashBoard) {
|
||||
folder = resetFolder;
|
||||
} else {
|
||||
folder = result.length > 0 ? result[0] : resetFolder;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -161,7 +168,7 @@ export class FolderPickerCtrl {
|
||||
export function folderPicker() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
templateUrl: 'public/app/features/dashboard/folder_picker/folder_picker.html',
|
||||
templateUrl: 'public/app/features/dashboard/components/FolderPicker/template.html',
|
||||
controller: FolderPickerCtrl,
|
||||
bindToController: true,
|
||||
controllerAs: 'ctrl',
|
||||
@ -176,6 +183,7 @@ export function folderPicker() {
|
||||
exitFolderCreation: '&',
|
||||
enableCreateNew: '@',
|
||||
enableReset: '@',
|
||||
dashboardId: '<?',
|
||||
},
|
||||
};
|
||||
}
|
@ -0,0 +1 @@
|
||||
export { FolderPickerCtrl } from './FolderPickerCtrl';
|
@ -1,4 +1,4 @@
|
||||
import { SaveDashboardAsModalCtrl } from '../save_as_modal';
|
||||
import { SaveDashboardAsModalCtrl } from './SaveDashboardAsModalCtrl';
|
||||
import { describe, it, expect } from 'test/lib/common';
|
||||
|
||||
describe('saving dashboard as', () => {
|
@ -25,7 +25,8 @@ const template = `
|
||||
enter-folder-creation="ctrl.onEnterFolderCreation()"
|
||||
exit-folder-creation="ctrl.onExitFolderCreation()"
|
||||
enable-create-new="true"
|
||||
label-class="width-7">
|
||||
label-class="width-7"
|
||||
dashboard-id="ctrl.clone.id">
|
||||
</folder-picker>
|
||||
</div>
|
||||
</div>
|
@ -1,4 +1,4 @@
|
||||
import { SaveDashboardModalCtrl } from '../save_modal';
|
||||
import { SaveDashboardModalCtrl } from './SaveDashboardModalCtrl';
|
||||
|
||||
const setup = (timeChanged, variableValuesChanged, cb) => {
|
||||
const dash = {
|
@ -1,4 +1,4 @@
|
||||
import { SaveProvisionedDashboardModalCtrl } from '../save_provisioned_modal';
|
||||
import { SaveProvisionedDashboardModalCtrl } from './SaveProvisionedDashboardModalCtrl';
|
||||
|
||||
describe('SaveProvisionedDashboardModalCtrl', () => {
|
||||
const json = {
|
@ -0,0 +1,2 @@
|
||||
export { SaveDashboardAsModalCtrl } from './SaveDashboardAsModalCtrl';
|
||||
export { SaveDashboardModalCtrl } from './SaveDashboardModalCtrl';
|
@ -1,7 +1,6 @@
|
||||
import '../shareModalCtrl';
|
||||
import { ShareModalCtrl } from '../shareModalCtrl';
|
||||
import config from 'app/core/config';
|
||||
import { LinkSrv } from 'app/features/dashboard/panellinks/link_srv';
|
||||
import { ShareModalCtrl } from './ShareModalCtrl';
|
||||
|
||||
describe('ShareModalCtrl', () => {
|
||||
const ctx = {
|
@ -0,0 +1,2 @@
|
||||
export { ShareModalCtrl } from './ShareModalCtrl';
|
||||
export { ShareSnapshotCtrl } from './ShareSnapshotCtrl';
|
@ -1,7 +1,7 @@
|
||||
import angular from 'angular';
|
||||
import _ from 'lodash';
|
||||
|
||||
export class SubmenuCtrl {
|
||||
export class SubMenuCtrl {
|
||||
annotations: any;
|
||||
variables: any;
|
||||
dashboard: any;
|
||||
@ -29,8 +29,8 @@ export class SubmenuCtrl {
|
||||
export function submenuDirective() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
templateUrl: 'public/app/features/dashboard/submenu/submenu.html',
|
||||
controller: SubmenuCtrl,
|
||||
templateUrl: 'public/app/features/dashboard/components/SubMenu/template.html',
|
||||
controller: SubMenuCtrl,
|
||||
bindToController: true,
|
||||
controllerAs: 'ctrl',
|
||||
scope: {
|
@ -0,0 +1 @@
|
||||
export { SubMenuCtrl } from './SubMenuCtrl';
|
@ -159,7 +159,7 @@ export class TimePickerCtrl {
|
||||
export function settingsDirective() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
templateUrl: 'public/app/features/dashboard/timepicker/settings.html',
|
||||
templateUrl: 'public/app/features/dashboard/components/TimePicker/settings.html',
|
||||
controller: TimePickerCtrl,
|
||||
bindToController: true,
|
||||
controllerAs: 'ctrl',
|
||||
@ -172,7 +172,7 @@ export function settingsDirective() {
|
||||
export function timePickerDirective() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
templateUrl: 'public/app/features/dashboard/timepicker/timepicker.html',
|
||||
templateUrl: 'public/app/features/dashboard/components/TimePicker/template.html',
|
||||
controller: TimePickerCtrl,
|
||||
bindToController: true,
|
||||
controllerAs: 'ctrl',
|
||||
@ -185,5 +185,5 @@ export function timePickerDirective() {
|
||||
angular.module('grafana.directives').directive('gfTimePickerSettings', settingsDirective);
|
||||
angular.module('grafana.directives').directive('gfTimePicker', timePickerDirective);
|
||||
|
||||
import { inputDateDirective } from './input_date';
|
||||
import { inputDateDirective } from './validation';
|
||||
angular.module('grafana.directives').directive('inputDatetime', inputDateDirective);
|
@ -0,0 +1 @@
|
||||
export { TimePickerCtrl } from './TimePickerCtrl';
|
@ -0,0 +1 @@
|
||||
export { UnsavedChangesModalCtrl } from './UnsavedChangesModalCtrl';
|
@ -1,6 +1,6 @@
|
||||
import _ from 'lodash';
|
||||
import { HistoryListCtrl } from 'app/features/dashboard/history/history';
|
||||
import { versions, compare, restore } from './history_mocks';
|
||||
import { HistoryListCtrl } from './HistoryListCtrl';
|
||||
import { versions, compare, restore } from './__mocks__/history';
|
||||
import $q from 'q';
|
||||
|
||||
describe('HistoryListCtrl', () => {
|
@ -1,12 +1,10 @@
|
||||
import './history_srv';
|
||||
|
||||
import _ from 'lodash';
|
||||
import angular from 'angular';
|
||||
import moment from 'moment';
|
||||
|
||||
import locationUtil from 'app/core/utils/location_util';
|
||||
import { DashboardModel } from '../dashboard_model';
|
||||
import { HistoryListOpts, RevisionsModel, CalculateDiffOptions, HistorySrv } from './history_srv';
|
||||
import { DashboardModel } from '../../dashboard_model';
|
||||
import { HistoryListOpts, RevisionsModel, CalculateDiffOptions, HistorySrv } from './HistorySrv';
|
||||
|
||||
export class HistoryListCtrl {
|
||||
appending: boolean;
|
||||
@ -200,7 +198,7 @@ export class HistoryListCtrl {
|
||||
export function dashboardHistoryDirective() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
templateUrl: 'public/app/features/dashboard/history/history.html',
|
||||
templateUrl: 'public/app/features/dashboard/components/VersionHistory/template.html',
|
||||
controller: HistoryListCtrl,
|
||||
bindToController: true,
|
||||
controllerAs: 'ctrl',
|
@ -1,7 +1,6 @@
|
||||
import '../history/history_srv';
|
||||
import { versions, restore } from './history_mocks';
|
||||
import { HistorySrv } from '../history/history_srv';
|
||||
import { DashboardModel } from '../dashboard_model';
|
||||
import { versions, restore } from './__mocks__/history';
|
||||
import { HistorySrv } from './HistorySrv';
|
||||
import { DashboardModel } from '../../dashboard_model';
|
||||
jest.mock('app/core/store');
|
||||
|
||||
describe('historySrv', () => {
|
@ -1,6 +1,6 @@
|
||||
import _ from 'lodash';
|
||||
import coreModule from 'app/core/core_module';
|
||||
import { DashboardModel } from '../dashboard_model';
|
||||
import { DashboardModel } from '../../dashboard_model';
|
||||
|
||||
export interface HistoryListOpts {
|
||||
limit: number;
|
@ -0,0 +1,2 @@
|
||||
export { HistoryListCtrl } from './HistoryListCtrl';
|
||||
export { HistorySrv } from './HistorySrv';
|
@ -22,7 +22,6 @@ export class DashboardCtrl {
|
||||
private keybindingSrv,
|
||||
private timeSrv,
|
||||
private variableSrv,
|
||||
private alertingSrv,
|
||||
private dashboardSrv,
|
||||
private unsavedChangesSrv,
|
||||
private dashboardViewStateSrv,
|
||||
@ -54,7 +53,6 @@ export class DashboardCtrl {
|
||||
|
||||
// init services
|
||||
this.timeSrv.init(dashboard);
|
||||
this.alertingSrv.init(dashboard, data.alerts);
|
||||
this.annotationsSrv.init(dashboard);
|
||||
|
||||
// template values service needs to initialize completely before
|
||||
|
@ -1,25 +0,0 @@
|
||||
import { FolderPageLoader } from './folder_page_loader';
|
||||
|
||||
export class FolderPermissionsCtrl {
|
||||
navModel: any;
|
||||
folderId: number;
|
||||
uid: string;
|
||||
dashboard: any;
|
||||
meta: any;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private backendSrv, navModelSrv, private $routeParams, $location) {
|
||||
if (this.$routeParams.uid) {
|
||||
this.uid = $routeParams.uid;
|
||||
|
||||
new FolderPageLoader(this.backendSrv).load(this, this.uid, 'manage-folder-permissions').then(folder => {
|
||||
if ($location.path() !== folder.meta.url) {
|
||||
$location.path(`${folder.meta.url}/permissions`).replace();
|
||||
}
|
||||
|
||||
this.dashboard = folder.dashboard;
|
||||
this.meta = folder.meta;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
35
public/app/features/dashboard/index.ts
Normal file
35
public/app/features/dashboard/index.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import './dashboard_ctrl';
|
||||
import './time_srv';
|
||||
import './repeat_option/repeat_option';
|
||||
import './dashgrid/DashboardGridDirective';
|
||||
import './dashgrid/RowOptions';
|
||||
import './panellinks/module';
|
||||
|
||||
// Services
|
||||
import './services/DashboardViewStateSrv';
|
||||
import './services/UnsavedChangesSrv';
|
||||
import './services/DashboardLoaderSrv';
|
||||
import './services/DashboardSrv';
|
||||
|
||||
// Components
|
||||
import './components/DashLinks';
|
||||
import './components/DashExportModal';
|
||||
import './components/DashNav';
|
||||
import './components/ExportDataModal';
|
||||
import './components/FolderPicker';
|
||||
import './components/VersionHistory';
|
||||
import './components/DashboardSettings';
|
||||
import './components/SubMenu';
|
||||
import './components/TimePicker';
|
||||
import './components/UnsavedChangesModal';
|
||||
import './components/SaveModals';
|
||||
import './components/ShareModal';
|
||||
import './components/AdHocFilters';
|
||||
|
||||
import DashboardPermissions from './components/DashboardPermissions/DashboardPermissions';
|
||||
|
||||
// angular wrappers
|
||||
import { react2AngularDirective } from 'app/core/utils/react2angular';
|
||||
|
||||
react2AngularDirective('dashboardPermissions', DashboardPermissions, ['dashboardId', 'folder']);
|
||||
|
@ -166,6 +166,7 @@ export class QueryEditorRow extends PureComponent<Props, State> {
|
||||
|
||||
onDisableQuery = () => {
|
||||
this.props.query.hide = !this.props.query.hide;
|
||||
this.onExecuteQuery();
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ChangeTracker } from 'app/features/dashboard/change_tracker';
|
||||
import { ChangeTracker } from './ChangeTracker';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { DashboardModel } from '../dashboard_model';
|
||||
import { PanelModel } from '../panel_model';
|
@ -1,6 +1,6 @@
|
||||
import angular from 'angular';
|
||||
import _ from 'lodash';
|
||||
import { DashboardModel } from './dashboard_model';
|
||||
import { DashboardModel } from '../dashboard_model';
|
||||
|
||||
export class ChangeTracker {
|
||||
current: any;
|
@ -1,5 +1,5 @@
|
||||
import coreModule from 'app/core/core_module';
|
||||
import { DashboardModel } from './dashboard_model';
|
||||
import { DashboardModel } from '../dashboard_model';
|
||||
import locationUtil from 'app/core/utils/location_util';
|
||||
|
||||
export class DashboardSrv {
|
@ -1,7 +1,5 @@
|
||||
//import { describe, beforeEach, it, expect, angularMocks } from 'test/lib/common';
|
||||
import 'app/features/dashboard/view_state_srv';
|
||||
import config from 'app/core/config';
|
||||
import { DashboardViewState } from '../view_state_srv';
|
||||
import { DashboardViewStateSrv } from './DashboardViewStateSrv';
|
||||
import { DashboardModel } from '../dashboard_model';
|
||||
|
||||
describe('when updating view state', () => {
|
||||
@ -33,7 +31,7 @@ describe('when updating view state', () => {
|
||||
location.search = jest.fn(() => {
|
||||
return { fullscreen: true, edit: true, panelId: 1 };
|
||||
});
|
||||
viewState = new DashboardViewState($scope, location, {});
|
||||
viewState = new DashboardViewStateSrv($scope, location, {});
|
||||
});
|
||||
|
||||
it('should update querystring and view state', () => {
|
||||
@ -55,7 +53,7 @@ describe('when updating view state', () => {
|
||||
|
||||
describe('to fullscreen false', () => {
|
||||
beforeEach(() => {
|
||||
viewState = new DashboardViewState($scope, location, {});
|
||||
viewState = new DashboardViewStateSrv($scope, location, {});
|
||||
});
|
||||
it('should remove params from query string', () => {
|
||||
viewState.update({ fullscreen: true, panelId: 1, edit: true });
|
@ -2,11 +2,11 @@ import angular from 'angular';
|
||||
import _ from 'lodash';
|
||||
import config from 'app/core/config';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { DashboardModel } from './dashboard_model';
|
||||
import { DashboardModel } from '../dashboard_model';
|
||||
|
||||
// represents the transient view state
|
||||
// like fullscreen panel & edit
|
||||
export class DashboardViewState {
|
||||
export class DashboardViewStateSrv {
|
||||
state: any;
|
||||
panelScopes: any;
|
||||
$scope: any;
|
||||
@ -168,7 +168,7 @@ export class DashboardViewState {
|
||||
export function dashboardViewStateSrv($location, $timeout) {
|
||||
return {
|
||||
create: $scope => {
|
||||
return new DashboardViewState($scope, $location, $timeout);
|
||||
return new DashboardViewStateSrv($scope, $location, $timeout);
|
||||
},
|
||||
};
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import angular from 'angular';
|
||||
import { ChangeTracker } from './change_tracker';
|
||||
import { ChangeTracker } from './ChangeTracker';
|
||||
|
||||
/** @ngInject */
|
||||
export function unsavedChangesSrv(this: any, $rootScope, $q, $location, $timeout, contextSrv, dashboardSrv, $window) {
|
@ -80,7 +80,7 @@ export const editPanelJson = (dashboard: DashboardModel, panel: PanelModel) => {
|
||||
|
||||
export const sharePanel = (dashboard: DashboardModel, panel: PanelModel) => {
|
||||
appEvents.emit('show-modal', {
|
||||
src: 'public/app/features/dashboard/partials/shareModal.html',
|
||||
src: 'public/app/features/dashboard/components/ShareModal/template.html',
|
||||
model: {
|
||||
dashboard: dashboard,
|
||||
panel: panel,
|
||||
|
@ -5,7 +5,7 @@ import { connect } from 'react-redux';
|
||||
import { ExploreId, ExploreItemState } from 'app/types/explore';
|
||||
import { StoreState } from 'app/types';
|
||||
|
||||
import { toggleGraph } from './state/actions';
|
||||
import { toggleTable } from './state/actions';
|
||||
import Table from './Table';
|
||||
import Panel from './Panel';
|
||||
import TableModel from 'app/core/table_model';
|
||||
@ -16,12 +16,12 @@ interface TableContainerProps {
|
||||
onClickCell: (key: string, value: string) => void;
|
||||
showingTable: boolean;
|
||||
tableResult?: TableModel;
|
||||
toggleGraph: typeof toggleGraph;
|
||||
toggleTable: typeof toggleTable;
|
||||
}
|
||||
|
||||
export class TableContainer extends PureComponent<TableContainerProps> {
|
||||
onClickTableButton = () => {
|
||||
this.props.toggleGraph(this.props.exploreId);
|
||||
this.props.toggleTable(this.props.exploreId);
|
||||
};
|
||||
|
||||
render() {
|
||||
@ -43,7 +43,7 @@ function mapStateToProps(state: StoreState, { exploreId }) {
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
toggleGraph,
|
||||
toggleTable,
|
||||
};
|
||||
|
||||
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(TableContainer));
|
||||
|
42
public/app/features/explore/state/reducers.test.ts
Normal file
42
public/app/features/explore/state/reducers.test.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { Action, ActionTypes } from './actionTypes';
|
||||
import { itemReducer, makeExploreItemState } from './reducers';
|
||||
import { ExploreId } from 'app/types/explore';
|
||||
|
||||
describe('Explore item reducer', () => {
|
||||
describe('scanning', () => {
|
||||
test('should start scanning', () => {
|
||||
let state = makeExploreItemState();
|
||||
const action: Action = {
|
||||
type: ActionTypes.ScanStart,
|
||||
payload: {
|
||||
exploreId: ExploreId.left,
|
||||
scanner: jest.fn(),
|
||||
},
|
||||
};
|
||||
state = itemReducer(state, action);
|
||||
expect(state.scanning).toBeTruthy();
|
||||
expect(state.scanner).toBe(action.payload.scanner);
|
||||
});
|
||||
test('should stop scanning', () => {
|
||||
let state = makeExploreItemState();
|
||||
const start: Action = {
|
||||
type: ActionTypes.ScanStart,
|
||||
payload: {
|
||||
exploreId: ExploreId.left,
|
||||
scanner: jest.fn(),
|
||||
},
|
||||
};
|
||||
state = itemReducer(state, start);
|
||||
expect(state.scanning).toBeTruthy();
|
||||
const action: Action = {
|
||||
type: ActionTypes.ScanStop,
|
||||
payload: {
|
||||
exploreId: ExploreId.left,
|
||||
},
|
||||
};
|
||||
state = itemReducer(state, action);
|
||||
expect(state.scanning).toBeFalsy();
|
||||
expect(state.scanner).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
@ -20,7 +20,7 @@ const DEFAULT_GRAPH_INTERVAL = 15 * 1000;
|
||||
/**
|
||||
* Returns a fresh Explore area state
|
||||
*/
|
||||
const makeExploreItemState = (): ExploreItemState => ({
|
||||
export const makeExploreItemState = (): ExploreItemState => ({
|
||||
StartPage: undefined,
|
||||
containerWidth: 0,
|
||||
datasourceInstance: null,
|
||||
@ -48,7 +48,7 @@ const makeExploreItemState = (): ExploreItemState => ({
|
||||
/**
|
||||
* Global Explore state that handles multiple Explore areas and the split state
|
||||
*/
|
||||
const initialExploreState: ExploreState = {
|
||||
export const initialExploreState: ExploreState = {
|
||||
split: null,
|
||||
left: makeExploreItemState(),
|
||||
right: makeExploreItemState(),
|
||||
@ -57,7 +57,7 @@ const initialExploreState: ExploreState = {
|
||||
/**
|
||||
* Reducer for an Explore area, to be used by the global Explore reducer.
|
||||
*/
|
||||
const itemReducer = (state, action: Action): ExploreItemState => {
|
||||
export const itemReducer = (state, action: Action): ExploreItemState => {
|
||||
switch (action.type) {
|
||||
case ActionTypes.AddQueryRow: {
|
||||
const { initialQueries, modifiedQueries, queryTransactions } = state;
|
||||
@ -360,13 +360,19 @@ const itemReducer = (state, action: Action): ExploreItemState => {
|
||||
}
|
||||
|
||||
case ActionTypes.ScanStart: {
|
||||
return { ...state, scanning: true };
|
||||
return { ...state, scanning: true, scanner: action.payload.scanner };
|
||||
}
|
||||
|
||||
case ActionTypes.ScanStop: {
|
||||
const { queryTransactions } = state;
|
||||
const nextQueryTransactions = queryTransactions.filter(qt => qt.scanning && !qt.done);
|
||||
return { ...state, queryTransactions: nextQueryTransactions, scanning: false, scanRange: undefined };
|
||||
return {
|
||||
...state,
|
||||
queryTransactions: nextQueryTransactions,
|
||||
scanning: false,
|
||||
scanRange: undefined,
|
||||
scanner: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
case ActionTypes.SetQueries: {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { DashboardImportCtrl } from '../dashboard_import_ctrl';
|
||||
import config from '../../../core/config';
|
||||
import { DashboardImportCtrl } from './DashboardImportCtrl';
|
||||
import config from 'app/core/config';
|
||||
|
||||
describe('DashboardImportCtrl', () => {
|
||||
const ctx: any = {};
|
@ -1,4 +1,4 @@
|
||||
import { FolderPageLoader } from './folder_page_loader';
|
||||
import { FolderPageLoader } from './services/FolderPageLoader';
|
||||
import locationUtil from 'app/core/utils/location_util';
|
||||
|
||||
export class FolderDashboardsCtrl {
|
@ -46,7 +46,7 @@ export class MoveToFolderCtrl {
|
||||
export function moveToFolderModal() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
templateUrl: 'public/app/features/dashboard/move_to_folder_modal/move_to_folder.html',
|
||||
templateUrl: 'public/app/features/manage-dashboards/components/MoveToFolderModal/template.html',
|
||||
controller: MoveToFolderCtrl,
|
||||
bindToController: true,
|
||||
controllerAs: 'ctrl',
|
@ -0,0 +1 @@
|
||||
export { MoveToFolderCtrl } from './MoveToFolderCtrl';
|
@ -0,0 +1 @@
|
||||
export { uploadDashboardDirective } from './uploadDashboardDirective';
|
@ -11,7 +11,7 @@ const template = `
|
||||
`;
|
||||
|
||||
/** @ngInject */
|
||||
function uploadDashboardDirective(timer, $location) {
|
||||
export function uploadDashboardDirective(timer, $location) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
template: template,
|
@ -1,7 +1,21 @@
|
||||
import coreModule from 'app/core/core_module';
|
||||
// Services
|
||||
export { ValidationSrv } from './services/ValidationSrv';
|
||||
|
||||
// Components
|
||||
export * from './components/MoveToFolderModal';
|
||||
export * from './components/UploadDashboard';
|
||||
|
||||
// Controllers
|
||||
import { DashboardListCtrl } from './DashboardListCtrl';
|
||||
import { SnapshotListCtrl } from './SnapshotListCtrl';
|
||||
import { FolderDashboardsCtrl } from './FolderDashboardsCtrl';
|
||||
import { DashboardImportCtrl } from './DashboardImportCtrl';
|
||||
import { CreateFolderCtrl } from './CreateFolderCtrl';
|
||||
|
||||
import coreModule from 'app/core/core_module';
|
||||
|
||||
coreModule.controller('DashboardListCtrl', DashboardListCtrl);
|
||||
coreModule.controller('SnapshotListCtrl', SnapshotListCtrl);
|
||||
coreModule.controller('FolderDashboardsCtrl', FolderDashboardsCtrl);
|
||||
coreModule.controller('DashboardImportCtrl', DashboardImportCtrl);
|
||||
coreModule.controller('CreateFolderCtrl', CreateFolderCtrl);
|
||||
|
@ -4,12 +4,13 @@ import appEvents from 'app/core/app_events';
|
||||
import _ from 'lodash';
|
||||
import { toUrlParams } from 'app/core/utils/url';
|
||||
|
||||
class PlaylistSrv {
|
||||
export class PlaylistSrv {
|
||||
private cancelPromise: any;
|
||||
private dashboards: any;
|
||||
private dashboards: Array<{ uri: string }>;
|
||||
private index: number;
|
||||
private interval: any;
|
||||
private interval: number;
|
||||
private startUrl: string;
|
||||
private numberOfLoops = 0;
|
||||
isPlaying: boolean;
|
||||
|
||||
/** @ngInject */
|
||||
@ -20,8 +21,15 @@ class PlaylistSrv {
|
||||
|
||||
const playedAllDashboards = this.index > this.dashboards.length - 1;
|
||||
if (playedAllDashboards) {
|
||||
window.location.href = this.startUrl;
|
||||
return;
|
||||
this.numberOfLoops++;
|
||||
|
||||
// This does full reload of the playlist to keep memory in check due to existing leaks but at the same time
|
||||
// we do not want page to flicker after each full loop.
|
||||
if (this.numberOfLoops >= 3) {
|
||||
window.location.href = this.startUrl;
|
||||
return;
|
||||
}
|
||||
this.index = 0;
|
||||
}
|
||||
|
||||
const dash = this.dashboards[this.index];
|
||||
@ -46,8 +54,8 @@ class PlaylistSrv {
|
||||
this.index = 0;
|
||||
this.isPlaying = true;
|
||||
|
||||
this.backendSrv.get(`/api/playlists/${playlistId}`).then(playlist => {
|
||||
this.backendSrv.get(`/api/playlists/${playlistId}/dashboards`).then(dashboards => {
|
||||
return this.backendSrv.get(`/api/playlists/${playlistId}`).then(playlist => {
|
||||
return this.backendSrv.get(`/api/playlists/${playlistId}/dashboards`).then(dashboards => {
|
||||
this.dashboards = dashboards;
|
||||
this.interval = kbn.interval_to_ms(playlist.interval);
|
||||
this.next();
|
||||
|
103
public/app/features/playlist/specs/playlist_srv.test.ts
Normal file
103
public/app/features/playlist/specs/playlist_srv.test.ts
Normal file
@ -0,0 +1,103 @@
|
||||
import { PlaylistSrv } from '../playlist_srv';
|
||||
|
||||
const dashboards = [{ uri: 'dash1' }, { uri: 'dash2' }];
|
||||
|
||||
const createPlaylistSrv = (): [PlaylistSrv, { url: jest.MockInstance<any> }] => {
|
||||
const mockBackendSrv = {
|
||||
get: jest.fn(url => {
|
||||
switch (url) {
|
||||
case '/api/playlists/1':
|
||||
return Promise.resolve({ interval: '1s' });
|
||||
case '/api/playlists/1/dashboards':
|
||||
return Promise.resolve(dashboards);
|
||||
default:
|
||||
throw new Error(`Unexpected url=${url}`);
|
||||
}
|
||||
}),
|
||||
};
|
||||
|
||||
const mockLocation = {
|
||||
url: jest.fn(),
|
||||
search: () => ({}),
|
||||
};
|
||||
|
||||
const mockTimeout = jest.fn();
|
||||
(mockTimeout as any).cancel = jest.fn();
|
||||
|
||||
return [new PlaylistSrv(mockLocation, mockTimeout, mockBackendSrv), mockLocation];
|
||||
};
|
||||
|
||||
const mockWindowLocation = (): [jest.MockInstance<any>, () => void] => {
|
||||
const oldLocation = window.location;
|
||||
const hrefMock = jest.fn();
|
||||
|
||||
// JSDom defines window in a way that you cannot tamper with location so this seems to be the only way to change it.
|
||||
// https://github.com/facebook/jest/issues/5124#issuecomment-446659510
|
||||
delete window.location;
|
||||
window.location = {} as any;
|
||||
|
||||
// Only mocking href as that is all this test needs, but otherwise there is lots of things missing, so keep that
|
||||
// in mind if this is reused.
|
||||
Object.defineProperty(window.location, 'href', {
|
||||
set: hrefMock,
|
||||
get: hrefMock,
|
||||
});
|
||||
const unmock = () => {
|
||||
window.location = oldLocation;
|
||||
};
|
||||
return [hrefMock, unmock];
|
||||
};
|
||||
|
||||
describe('PlaylistSrv', () => {
|
||||
let srv: PlaylistSrv;
|
||||
let mockLocationService: { url: jest.MockInstance<any> };
|
||||
let hrefMock: jest.MockInstance<any>;
|
||||
let unmockLocation: () => void;
|
||||
const initialUrl = 'http://localhost/playlist';
|
||||
|
||||
beforeEach(() => {
|
||||
[srv, mockLocationService] = createPlaylistSrv();
|
||||
[hrefMock, unmockLocation] = mockWindowLocation();
|
||||
|
||||
// This will be cached in the srv when start() is called
|
||||
hrefMock.mockReturnValue(initialUrl);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
unmockLocation();
|
||||
});
|
||||
|
||||
it('runs all dashboards in cycle and reloads page after 3 cycles', async () => {
|
||||
await srv.start(1);
|
||||
|
||||
for (let i = 0; i < 6; i++) {
|
||||
expect(mockLocationService.url).toHaveBeenLastCalledWith(`dashboard/${dashboards[i % 2].uri}?`);
|
||||
srv.next();
|
||||
}
|
||||
|
||||
expect(hrefMock).toHaveBeenCalledTimes(2);
|
||||
expect(hrefMock).toHaveBeenLastCalledWith(initialUrl);
|
||||
});
|
||||
|
||||
it('keeps the refresh counter value after restarting', async () => {
|
||||
await srv.start(1);
|
||||
|
||||
// 1 complete loop
|
||||
for (let i = 0; i < 3; i++) {
|
||||
expect(mockLocationService.url).toHaveBeenLastCalledWith(`dashboard/${dashboards[i % 2].uri}?`);
|
||||
srv.next();
|
||||
}
|
||||
|
||||
srv.stop();
|
||||
await srv.start(1);
|
||||
|
||||
// Another 2 loops
|
||||
for (let i = 0; i < 4; i++) {
|
||||
expect(mockLocationService.url).toHaveBeenLastCalledWith(`dashboard/${dashboards[i % 2].uri}?`);
|
||||
srv.next();
|
||||
}
|
||||
|
||||
expect(hrefMock).toHaveBeenCalledTimes(3);
|
||||
expect(hrefMock).toHaveBeenLastCalledWith(initialUrl);
|
||||
});
|
||||
});
|
@ -275,6 +275,11 @@ describe('templateSrv', () => {
|
||||
expect(result).toBe('test,test2');
|
||||
});
|
||||
|
||||
it('multi value and percentencode format should render percent-encoded string', () => {
|
||||
const result = _templateSrv.formatValue(['foo()bar BAZ', 'test2'], 'percentencode');
|
||||
expect(result).toBe('%7Bfoo%28%29bar%20BAZ%2Ctest2%7D');
|
||||
});
|
||||
|
||||
it('slash should be properly escaped in regex format', () => {
|
||||
const result = _templateSrv.formatValue('Gi3/14', 'regex');
|
||||
expect(result).toBe('Gi3\\/14');
|
||||
|
@ -77,6 +77,15 @@ export class TemplateSrv {
|
||||
return '(' + quotedValues.join(' OR ') + ')';
|
||||
}
|
||||
|
||||
// encode string according to RFC 3986; in contrast to encodeURIComponent()
|
||||
// also the sub-delims "!", "'", "(", ")" and "*" are encoded;
|
||||
// unicode handling uses UTF-8 as in ECMA-262.
|
||||
encodeURIComponentStrict(str) {
|
||||
return encodeURIComponent(str).replace(/[!'()*]/g, (c) => {
|
||||
return '%' + c.charCodeAt(0).toString(16).toUpperCase();
|
||||
});
|
||||
}
|
||||
|
||||
formatValue(value, format, variable) {
|
||||
// for some scopedVars there is no variable
|
||||
variable = variable || {};
|
||||
@ -118,6 +127,13 @@ export class TemplateSrv {
|
||||
}
|
||||
return value;
|
||||
}
|
||||
case 'percentencode': {
|
||||
// like glob, but url escaped
|
||||
if (_.isArray(value)) {
|
||||
return this.encodeURIComponentStrict('{' + value.join(',') + '}');
|
||||
}
|
||||
return this.encodeURIComponentStrict(value);
|
||||
}
|
||||
default: {
|
||||
if (_.isArray(value)) {
|
||||
return '{' + value.join(',') + '}';
|
||||
|
@ -26,7 +26,7 @@ export default (props: any) => (
|
||||
<div>
|
||||
<h2>Loki Cheat Sheet</h2>
|
||||
{CHEAT_SHEET_ITEMS.map(item => (
|
||||
<div className="cheat-sheet-item" key={item.expression}>
|
||||
<div className="cheat-sheet-item" key={item.title}>
|
||||
<div className="cheat-sheet-item__title">{item.title}</div>
|
||||
{item.expression && (
|
||||
<div
|
||||
|
@ -10,23 +10,6 @@
|
||||
"id": null,
|
||||
"links": [],
|
||||
"panels": [
|
||||
{
|
||||
"content": "<div class=\"text-center dashboard-header\">\n <span>Home Dashboard</span>\n</div>",
|
||||
"editable": true,
|
||||
"id": 1,
|
||||
"links": [],
|
||||
"mode": "html",
|
||||
"style": {},
|
||||
"title": "",
|
||||
"transparent": true,
|
||||
"type": "text",
|
||||
"gridPos": {
|
||||
"w": 24,
|
||||
"h": 3,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"folderId": 0,
|
||||
"headings": true,
|
||||
@ -45,7 +28,7 @@
|
||||
"w": 12,
|
||||
"h": 17,
|
||||
"x": 0,
|
||||
"y": 6
|
||||
"y": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -60,7 +43,7 @@
|
||||
"w": 12,
|
||||
"h": 17,
|
||||
"x": 12,
|
||||
"y": 6
|
||||
"y": 1
|
||||
}
|
||||
}
|
||||
],
|
||||
|
62
style_guides/frontend.md
Normal file
62
style_guides/frontend.md
Normal file
@ -0,0 +1,62 @@
|
||||
# Frontend Style Guide
|
||||
|
||||
Generally we follow the Airbnb [React Style Guide](https://github.com/airbnb/javascript/tree/master/react).
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Basic Rules](#basic-rules)
|
||||
1. [File & Component Organization](#Organization)
|
||||
1. [Naming](#naming)
|
||||
1. [Declaration](#declaration)
|
||||
1. [Props](#props)
|
||||
1. [Refs](#refs)
|
||||
1. [Methods](#methods)
|
||||
1. [Ordering](#ordering)
|
||||
|
||||
## Basic rules
|
||||
|
||||
* Try to keep files small and focused and break large components up into sub components.
|
||||
|
||||
## Organization
|
||||
|
||||
* Components and types that needs to be used by external plugins needs to go into @grafana/ui
|
||||
* Components should get their own folder under features/xxx/components
|
||||
* Sub components can live in that component folders, so not small component needs their own folder
|
||||
* Place test next to their component file (same dir)
|
||||
* Mocks in __mocks__ dir
|
||||
* Test utils in __tests__ dir
|
||||
* Component sass should live in the same folder as component code
|
||||
* State logic & domain models should live in features/xxx/state
|
||||
* Containers (pages) can live in feature root features/xxx
|
||||
* up for debate?
|
||||
|
||||
## Props
|
||||
|
||||
* Name callback props & handlers with a "on" prefix.
|
||||
|
||||
```tsx
|
||||
// good
|
||||
onChange = () => {
|
||||
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<MyComponent onChange={this.onChange} />
|
||||
);
|
||||
}
|
||||
|
||||
// bad
|
||||
handleChange = () => {
|
||||
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<MyComponent changed={this.handleChange} />
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user