mirror of https://github.com/grafana/grafana
* 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
commit
b5572b23b6
@ -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'; |
@ -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 { |
@ -0,0 +1,2 @@ |
||||
export { DashLinksContainerCtrl } from './DashLinksContainerCtrl'; |
||||
export { DashLinksEditorCtrl } from './DashLinksEditorCtrl'; |
@ -0,0 +1 @@ |
||||
export { DashNavCtrl } from './DashNavCtrl'; |
@ -0,0 +1 @@ |
||||
export { SettingsCtrl } from './SettingsCtrl'; |
@ -0,0 +1 @@ |
||||
export { ExportDataModalCtrl } from './ExportDataModalCtrl'; |
@ -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', () => { |
@ -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'; |
@ -0,0 +1 @@ |
||||
export { SubMenuCtrl } from './SubMenuCtrl'; |
@ -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,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'; |
@ -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']); |
||||
|
@ -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,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) { |
@ -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(); |
||||
}); |
||||
}); |
||||
}); |
@ -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 { |
@ -0,0 +1 @@ |
||||
export { MoveToFolderCtrl } from './MoveToFolderCtrl'; |
@ -0,0 +1 @@ |
||||
export { uploadDashboardDirective } from './uploadDashboardDirective'; |
@ -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); |
||||
|
@ -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); |
||||
}); |
||||
}); |
@ -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