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
  ...
pull/14912/head
bergquist 6 years ago
commit b5572b23b6
  1. 2
      CHANGELOG.md
  2. 16
      ROADMAP.md
  3. 4
      docs/sources/http_api/data_source.md
  4. 6
      docs/sources/installation/configuration.md
  5. 1
      docs/sources/reference/templating.md
  6. 2
      pkg/api/dashboard.go
  7. 2
      public/app/core/services/keybindingSrv.ts
  8. 18
      public/app/core/utils/text.ts
  9. 2
      public/app/features/all.ts
  10. 13
      public/app/features/dashboard/alerting_srv.ts
  11. 45
      public/app/features/dashboard/all.ts
  12. 0
      public/app/features/dashboard/components/AdHocFilters/AdHocFiltersCtrl.ts
  13. 1
      public/app/features/dashboard/components/AdHocFilters/index.ts
  14. 4
      public/app/features/dashboard/components/DashExportModal/DashExportCtrl.ts
  15. 4
      public/app/features/dashboard/components/DashExportModal/DashboardExporter.test.ts
  16. 2
      public/app/features/dashboard/components/DashExportModal/DashboardExporter.ts
  17. 2
      public/app/features/dashboard/components/DashExportModal/index.ts
  18. 0
      public/app/features/dashboard/components/DashExportModal/template.html
  19. 2
      public/app/features/dashboard/components/DashLinks/DashLinksContainerCtrl.ts
  20. 6
      public/app/features/dashboard/components/DashLinks/DashLinksEditorCtrl.ts
  21. 0
      public/app/features/dashboard/components/DashLinks/editor.html
  22. 2
      public/app/features/dashboard/components/DashLinks/index.ts
  23. 6
      public/app/features/dashboard/components/DashNav/DashNavCtrl.ts
  24. 1
      public/app/features/dashboard/components/DashNav/index.ts
  25. 0
      public/app/features/dashboard/components/DashNav/template.html
  26. 4
      public/app/features/dashboard/components/DashboardPermissions/DashboardPermissions.tsx
  27. 4
      public/app/features/dashboard/components/DashboardSettings/SettingsCtrl.ts
  28. 1
      public/app/features/dashboard/components/DashboardSettings/index.ts
  29. 3
      public/app/features/dashboard/components/DashboardSettings/template.html
  30. 2
      public/app/features/dashboard/components/ExportDataModal/ExportDataModalCtrl.ts
  31. 1
      public/app/features/dashboard/components/ExportDataModal/index.ts
  32. 0
      public/app/features/dashboard/components/ExportDataModal/template.html
  33. 10
      public/app/features/dashboard/components/FolderPicker/FolderPickerCtrl.ts
  34. 1
      public/app/features/dashboard/components/FolderPicker/index.ts
  35. 0
      public/app/features/dashboard/components/FolderPicker/template.html
  36. 2
      public/app/features/dashboard/components/SaveModals/SaveDashboardAsModalCtrl.test.ts
  37. 3
      public/app/features/dashboard/components/SaveModals/SaveDashboardAsModalCtrl.ts
  38. 2
      public/app/features/dashboard/components/SaveModals/SaveDashboardModalCtrl.test.ts
  39. 0
      public/app/features/dashboard/components/SaveModals/SaveDashboardModalCtrl.ts
  40. 2
      public/app/features/dashboard/components/SaveModals/SaveProvisionedDashboardModalCtrl.test.ts
  41. 0
      public/app/features/dashboard/components/SaveModals/SaveProvisionedDashboardModalCtrl.ts
  42. 2
      public/app/features/dashboard/components/SaveModals/index.ts
  43. 3
      public/app/features/dashboard/components/ShareModal/ShareModalCtrl.test.ts
  44. 0
      public/app/features/dashboard/components/ShareModal/ShareModalCtrl.ts
  45. 0
      public/app/features/dashboard/components/ShareModal/ShareSnapshotCtrl.ts
  46. 2
      public/app/features/dashboard/components/ShareModal/index.ts
  47. 0
      public/app/features/dashboard/components/ShareModal/template.html
  48. 6
      public/app/features/dashboard/components/SubMenu/SubMenuCtrl.ts
  49. 1
      public/app/features/dashboard/components/SubMenu/index.ts
  50. 0
      public/app/features/dashboard/components/SubMenu/template.html
  51. 6
      public/app/features/dashboard/components/TimePicker/TimePickerCtrl.ts
  52. 1
      public/app/features/dashboard/components/TimePicker/index.ts
  53. 0
      public/app/features/dashboard/components/TimePicker/settings.html
  54. 0
      public/app/features/dashboard/components/TimePicker/template.html
  55. 0
      public/app/features/dashboard/components/TimePicker/validation.ts
  56. 0
      public/app/features/dashboard/components/UnsavedChangesModal/UnsavedChangesModalCtrl.ts
  57. 1
      public/app/features/dashboard/components/UnsavedChangesModal/index.ts
  58. 4
      public/app/features/dashboard/components/VersionHistory/HistoryListCtrl.test.ts
  59. 8
      public/app/features/dashboard/components/VersionHistory/HistoryListCtrl.ts
  60. 7
      public/app/features/dashboard/components/VersionHistory/HistorySrv.test.ts
  61. 2
      public/app/features/dashboard/components/VersionHistory/HistorySrv.ts
  62. 0
      public/app/features/dashboard/components/VersionHistory/__mocks__/history.ts
  63. 2
      public/app/features/dashboard/components/VersionHistory/index.ts
  64. 0
      public/app/features/dashboard/components/VersionHistory/template.html
  65. 2
      public/app/features/dashboard/dashboard_ctrl.ts
  66. 25
      public/app/features/dashboard/folder_permissions_ctrl.ts
  67. 35
      public/app/features/dashboard/index.ts
  68. 1
      public/app/features/dashboard/panel_editor/QueryEditorRow.tsx
  69. 2
      public/app/features/dashboard/services/ChangeTracker.test.ts
  70. 2
      public/app/features/dashboard/services/ChangeTracker.ts
  71. 0
      public/app/features/dashboard/services/DashboardLoaderSrv.ts
  72. 2
      public/app/features/dashboard/services/DashboardSrv.ts
  73. 8
      public/app/features/dashboard/services/DashboardViewStateSrv.test.ts
  74. 6
      public/app/features/dashboard/services/DashboardViewStateSrv.ts
  75. 2
      public/app/features/dashboard/services/UnsavedChangesSrv.ts
  76. 2
      public/app/features/dashboard/utils/panel.ts
  77. 8
      public/app/features/explore/TableContainer.tsx
  78. 42
      public/app/features/explore/state/reducers.test.ts
  79. 16
      public/app/features/explore/state/reducers.ts
  80. 0
      public/app/features/manage-dashboards/CreateFolderCtrl.ts
  81. 4
      public/app/features/manage-dashboards/DashboardImportCtrl.test.ts
  82. 0
      public/app/features/manage-dashboards/DashboardImportCtrl.ts
  83. 2
      public/app/features/manage-dashboards/FolderDashboardsCtrl.ts
  84. 2
      public/app/features/manage-dashboards/components/MoveToFolderModal/MoveToFolderCtrl.ts
  85. 1
      public/app/features/manage-dashboards/components/MoveToFolderModal/index.ts
  86. 0
      public/app/features/manage-dashboards/components/MoveToFolderModal/template.html
  87. 1
      public/app/features/manage-dashboards/components/UploadDashboard/index.ts
  88. 2
      public/app/features/manage-dashboards/components/UploadDashboard/uploadDashboardDirective.ts
  89. 16
      public/app/features/manage-dashboards/index.ts
  90. 0
      public/app/features/manage-dashboards/services/FolderPageLoader.ts
  91. 0
      public/app/features/manage-dashboards/services/ValidationSrv.ts
  92. 18
      public/app/features/playlist/playlist_srv.ts
  93. 103
      public/app/features/playlist/specs/playlist_srv.test.ts
  94. 5
      public/app/features/templating/specs/template_srv.test.ts
  95. 16
      public/app/features/templating/template_srv.ts
  96. 2
      public/app/plugins/datasource/loki/components/LokiCheatSheet.tsx
  97. 21
      public/dashboards/home.json
  98. 62
      style_guides/frontend.md

@ -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)

@ -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) {
@ -143,10 +144,16 @@ export class FolderPickerCtrl {
if (!folder) {
if (this.isEditor) {
folder = rootFolder;
} else {
// 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;
}
}
}
this.folder = folder;
@ -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;
});
}
}
}

@ -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));

@ -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,9 +21,16 @@ class PlaylistSrv {
const playedAllDashboards = this.index > this.dashboards.length - 1;
if (playedAllDashboards) {
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];
const queryParams = this.$location.search();
@ -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();

@ -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
}
}
],

@ -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…
Cancel
Save