From 666d29fafae85b0d6f315d2bb8ca88f9fd166cbc Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Wed, 31 Jan 2018 10:39:38 +0100 Subject: [PATCH 01/18] dashfolders: POC - Use separate component for "Add permission" #10676 --- .../ManageDashboards/FolderPermissions.tsx | 19 +- .../components/Permissions/AddPermissions.tsx | 144 ++++++++++++++ .../components/Permissions/Permissions.tsx | 61 +----- .../app/core/components/Picker/TeamPicker.tsx | 4 +- .../app/core/components/Picker/UserPicker.tsx | 7 +- .../app/core/components/Picker/withPicker.tsx | 1 + .../PermissionsStore/PermissionsStore.ts | 184 +++++++++++++----- public/sass/components/_gf-form.scss | 6 + 8 files changed, 315 insertions(+), 111 deletions(-) create mode 100644 public/app/core/components/Permissions/AddPermissions.tsx diff --git a/public/app/containers/ManageDashboards/FolderPermissions.tsx b/public/app/containers/ManageDashboards/FolderPermissions.tsx index 3214382732a..98f63a46cbe 100644 --- a/public/app/containers/ManageDashboards/FolderPermissions.tsx +++ b/public/app/containers/ManageDashboards/FolderPermissions.tsx @@ -6,11 +6,14 @@ import PageHeader from 'app/core/components/PageHeader/PageHeader'; import Permissions from 'app/core/components/Permissions/Permissions'; import Tooltip from 'app/core/components/Tooltip/Tooltip'; import PermissionsInfo from 'app/core/components/Permissions/PermissionsInfo'; +import AddPermissions from 'app/core/components/Permissions/AddPermissions'; + @inject('nav', 'folder', 'view', 'permissions') @observer export class FolderPermissions extends Component { constructor(props) { super(props); + this.handleAddPermission = this.handleAddPermission.bind(this); this.loadStore(); } @@ -21,6 +24,11 @@ export class FolderPermissions extends Component { }); } + handleAddPermission() { + const { permissions } = this.props; + permissions.toggleAddPermissions(); + } + render() { const { nav, folder, permissions, backendSrv } = this.props; @@ -34,13 +42,20 @@ export class FolderPermissions extends Component {
-
+

Folder Permissions

+
+
- + {permissions.isAddPermissionsVisible ? ( + + ) : null}
diff --git a/public/app/core/components/Permissions/AddPermissions.tsx b/public/app/core/components/Permissions/AddPermissions.tsx new file mode 100644 index 00000000000..36b57e448e7 --- /dev/null +++ b/public/app/core/components/Permissions/AddPermissions.tsx @@ -0,0 +1,144 @@ +import React, { Component } from 'react'; +import { observer } from 'mobx-react'; +import { aclTypes } from 'app/stores/PermissionsStore/PermissionsStore'; +import UserPicker, { User } from 'app/core/components/Picker/UserPicker'; +import TeamPicker, { Team } from 'app/core/components/Picker/TeamPicker'; +import DescriptionPicker, { OptionWithDescription } from 'app/core/components/Picker/DescriptionPicker'; +import { permissionOptions } from 'app/stores/PermissionsStore/PermissionsStore'; + +export interface IProps { + permissions: any; + backendSrv: any; + dashboardId: any; +} + +@observer +class AddPermissions extends Component { + constructor(props) { + super(props); + this.userPicked = this.userPicked.bind(this); + this.teamPicked = this.teamPicked.bind(this); + this.permissionPicked = this.permissionPicked.bind(this); + this.typeChanged = this.typeChanged.bind(this); + this.handleSubmit = this.handleSubmit.bind(this); + } + + componentWillMount() { + const { permissions } = this.props; + permissions.resetNewType(); + } + + typeChanged(evt) { + const { value } = evt.target; + const { permissions } = this.props; + + // if (value === 'Viewer' || value === 'Editor') { + // // permissions.addStoreItem({ permission: 1, role: value, dashboardId: dashboardId }, dashboardId); + // // this.resetNewType(); + // return; + // } + + permissions.setNewType(value); + } + + userPicked(user: User) { + const { permissions } = this.props; + if (!user) { + permissions.newItem.setUser(null, null); + return; + } + permissions.newItem.setUser(user.id, user.login); + // return permissions.addStoreItem({ userId: user.id, userLogin: user.login, permission: 1 }); + } + + teamPicked(team: Team) { + const { permissions } = this.props; + if (!team) { + permissions.newItem.setTeam(null, null); + return; + } + permissions.newItem.setTeam(team.id, team.name); + } + + permissionPicked(permission: OptionWithDescription) { + const { permissions } = this.props; + permissions.newItem.setPermission(permission.value); + } + + resetNewType() { + const { permissions } = this.props; + permissions.resetNewType(); + } + + handleSubmit(evt) { + evt.preventDefault(); + const { permissions } = this.props; + permissions.addStoreItem(); + } + + render() { + const { permissions, backendSrv } = this.props; + const newItem = permissions.newItem; + + return ( +
+
+
Add Permission For
+
+
+
+ +
+
+ + {newItem.type === 'User' ? ( +
+ +
+ ) : null} + + {newItem.type === 'Group' ? ( +
+ +
+ ) : null} + +
+ +
+ +
+ +
+
+
+ {permissions.error ? ( +
+ + + {permissions.error} + +
+ ) : null} +
+ ); + } +} + +export default AddPermissions; diff --git a/public/app/core/components/Permissions/Permissions.tsx b/public/app/core/components/Permissions/Permissions.tsx index f5af579094b..1849d7da173 100644 --- a/public/app/core/components/Permissions/Permissions.tsx +++ b/public/app/core/components/Permissions/Permissions.tsx @@ -1,9 +1,6 @@ import React, { Component } from 'react'; import PermissionsList from './PermissionsList'; import { observer } from 'mobx-react'; -import UserPicker, { User } from 'app/core/components/Picker/UserPicker'; -import TeamPicker, { Team } from 'app/core/components/Picker/TeamPicker'; -import { aclTypes } from 'app/stores/PermissionsStore/PermissionsStore'; import { FolderInfo } from './FolderInfo'; export interface DashboardAcl { @@ -40,8 +37,6 @@ class Permissions extends Component { this.permissionChanged = this.permissionChanged.bind(this); this.typeChanged = this.typeChanged.bind(this); this.removeItem = this.removeItem.bind(this); - this.userPicked = this.userPicked.bind(this); - this.teamPicked = this.teamPicked.bind(this); this.loadStore(dashboardId, isFolder); } @@ -77,18 +72,8 @@ class Permissions extends Component { permissions.setNewType(value); } - userPicked(user: User) { - const { permissions } = this.props; - return permissions.addStoreItem({ userId: user.id, userLogin: user.login, permission: 1 }); - } - - teamPicked(team: Team) { - const { permissions } = this.props; - return permissions.addStoreItem({ teamId: team.id, team: team.name, permission: 1 }); - } - render() { - const { permissions, folderInfo, backendSrv } = this.props; + const { permissions, folderInfo } = this.props; return (
@@ -99,50 +84,6 @@ class Permissions extends Component { fetching={permissions.fetching} folderInfo={folderInfo} /> -
-
-
Add Permission For
-
-
-
- -
-
- - {permissions.newType === 'User' ? ( -
- -
- ) : null} - - {permissions.newType === 'Group' ? ( -
- -
- ) : null} -
-
- {permissions.error ? ( -
- - - {permissions.error} - -
- ) : null} -
); } diff --git a/public/app/core/components/Picker/TeamPicker.tsx b/public/app/core/components/Picker/TeamPicker.tsx index 4b5a049c0ff..18f7258c221 100644 --- a/public/app/core/components/Picker/TeamPicker.tsx +++ b/public/app/core/components/Picker/TeamPicker.tsx @@ -9,6 +9,7 @@ export interface IProps { isLoading: boolean; toggleLoading: any; handlePicked: (user) => void; + value?: string; } export interface Team { @@ -54,7 +55,7 @@ class TeamPicker extends Component { render() { const AsyncComponent = this.state.creatable ? Select.AsyncCreatable : Select.Async; - const { isLoading, handlePicked } = this.props; + const { isLoading, handlePicked, value } = this.props; return (
@@ -70,6 +71,7 @@ class TeamPicker extends Component { className="width-8 gf-form-input gf-form-input--form-dropdown" optionComponent={PickerOption} placeholder="Choose" + value={value} />
); diff --git a/public/app/core/components/Picker/UserPicker.tsx b/public/app/core/components/Picker/UserPicker.tsx index 141371813a2..99b0ef258ca 100644 --- a/public/app/core/components/Picker/UserPicker.tsx +++ b/public/app/core/components/Picker/UserPicker.tsx @@ -9,6 +9,7 @@ export interface IProps { isLoading: boolean; toggleLoading: any; handlePicked: (user) => void; + value?: string; } export interface User { @@ -53,8 +54,8 @@ class UserPicker extends Component { render() { const AsyncComponent = this.state.creatable ? Select.AsyncCreatable : Select.Async; - const { isLoading, handlePicked } = this.props; - + const { isLoading, handlePicked, value } = this.props; + console.log('value', value); return (
{ className="width-8 gf-form-input gf-form-input--form-dropdown" optionComponent={PickerOption} placeholder="Choose" + value={value} + autosize={true} />
); diff --git a/public/app/core/components/Picker/withPicker.tsx b/public/app/core/components/Picker/withPicker.tsx index bdfcb02676e..cf3954850b2 100644 --- a/public/app/core/components/Picker/withPicker.tsx +++ b/public/app/core/components/Picker/withPicker.tsx @@ -3,6 +3,7 @@ export interface IProps { backendSrv: any; handlePicked: (data) => void; + value?: string; } export default function withPicker(WrappedComponent) { diff --git a/public/app/stores/PermissionsStore/PermissionsStore.ts b/public/app/stores/PermissionsStore/PermissionsStore.ts index 52e5dcdb339..ae100fd0751 100644 --- a/public/app/stores/PermissionsStore/PermissionsStore.ts +++ b/public/app/stores/PermissionsStore/PermissionsStore.ts @@ -13,15 +13,62 @@ export const permissionOptions = [ }, ]; -export const aclTypes = [ - { value: 'Group', text: 'Team' }, - { value: 'User', text: 'User' }, - { value: 'Viewer', text: 'Everyone With Viewer Role' }, - { value: 'Editor', text: 'Everyone With Editor Role' }, -]; +export const aclTypeValues = { + GROUP: { value: 'Group', text: 'Team' }, + USER: { value: 'User', text: 'User' }, + VIEWER: { value: 'Viewer', text: 'Everyone With Viewer Role' }, + EDITOR: { value: 'Editor', text: 'Everyone With Editor Role' }, +}; + +export const aclTypes = Object.keys(aclTypeValues).map(item => aclTypeValues[item]); const defaultNewType = aclTypes[0].value; +const NewPermissionsItem = types + .model('NewPermissionsItem', { + type: types.optional( + types.enumeration(Object.keys(aclTypeValues).map(item => aclTypeValues[item].value)), + defaultNewType + ), + userId: types.maybe(types.number), + userLogin: types.maybe(types.string), + teamId: types.maybe(types.number), + team: types.maybe(types.string), + permission: types.optional(types.number, 1), + }) + .views(self => ({ + isValid: () => { + switch (self.type) { + case aclTypeValues.GROUP.value: + return self.teamId && self.team; + case aclTypeValues.USER.value: + return self.userId && self.userLogin; + case aclTypeValues.VIEWER.value: + case aclTypeValues.EDITOR.value: + return true; + default: + return false; + } + }, + })) + .actions(self => ({ + setUser(userId: number, userLogin: string) { + self.userId = userId; + self.userLogin = userLogin; + self.teamId = null; + self.team = null; + }, + setTeam(teamId: number, team: string) { + self.userId = null; + self.userLogin = null; + self.teamId = teamId; + self.team = team; + }, + setPermission(permission: number) { + self.permission = permission; + }, + })); + export const PermissionsStore = types .model('PermissionsStore', { fetching: types.boolean, @@ -31,6 +78,8 @@ export const PermissionsStore = types error: types.maybe(types.string), originalItems: types.optional(types.array(PermissionsStoreItem), []), newType: types.optional(types.string, defaultNewType), + newItem: types.maybe(NewPermissionsItem), + isAddPermissionsVisible: types.optional(types.boolean, false), }) .views(self => ({ isValid: item => { @@ -46,48 +95,91 @@ export const PermissionsStore = types return true; }, })) - .actions(self => ({ - load: flow(function* load(dashboardId: number, isFolder: boolean) { - const backendSrv = getEnv(self).backendSrv; - self.fetching = true; - self.isFolder = isFolder; - self.dashboardId = dashboardId; - const res = yield backendSrv.get(`/api/dashboards/id/${dashboardId}/acl`); - const items = prepareServerResponse(res, dashboardId, isFolder); - self.items = items; - self.originalItems = items; - self.fetching = false; - }), - addStoreItem: flow(function* addStoreItem(item) { + .actions(self => { + const resetNewType = () => { self.error = null; - if (!self.isValid(item)) { - return undefined; - } + self.newItem = NewPermissionsItem.create(); + }; - self.items.push(prepareItem(item, self.dashboardId, self.isFolder)); - return updateItems(self); - }), - removeStoreItem: flow(function* removeStoreItem(idx: number) { - self.error = null; - self.items.splice(idx, 1); - return updateItems(self); - }), - updatePermissionOnIndex: flow(function* updatePermissionOnIndex( - idx: number, - permission: number, - permissionName: string - ) { - self.error = null; - self.items[idx].updatePermission(permission, permissionName); - return updateItems(self); - }), - setNewType(newType: string) { - self.newType = newType; - }, - resetNewType() { - self.newType = defaultNewType; - }, - })); + return { + load: flow(function* load(dashboardId: number, isFolder: boolean) { + const backendSrv = getEnv(self).backendSrv; + self.fetching = true; + self.isFolder = isFolder; + self.dashboardId = dashboardId; + const res = yield backendSrv.get(`/api/dashboards/id/${dashboardId}/acl`); + const items = prepareServerResponse(res, dashboardId, isFolder); + self.items = items; + self.originalItems = items; + self.fetching = false; + }), + addStoreItem: flow(function* addStoreItem() { + self.error = null; + let item = { + type: self.newItem.type, + permission: self.newItem.permission, + team: undefined, + teamId: undefined, + userLogin: undefined, + userId: undefined, + role: undefined, + }; + switch (self.newItem.type) { + case aclTypeValues.GROUP.value: + item.team = self.newItem.team; + item.teamId = self.newItem.teamId; + break; + case aclTypeValues.USER.value: + item.userLogin = self.newItem.userLogin; + item.userId = self.newItem.userId; + break; + case aclTypeValues.VIEWER.value: + case aclTypeValues.EDITOR.value: + item.role = self.newItem.type; + break; + default: + throw Error('Unknown type: ' + self.newItem.type); + } + + if (!self.isValid(item)) { + throw Error('New item not valid'); + } + + self.items.push(prepareItem(item, self.dashboardId, self.isFolder)); + resetNewType(); + return updateItems(self); + }), + removeStoreItem: flow(function* removeStoreItem(idx: number) { + self.error = null; + self.items.splice(idx, 1); + return updateItems(self); + }), + updatePermissionOnIndex: flow(function* updatePermissionOnIndex( + idx: number, + permission: number, + permissionName: string + ) { + self.error = null; + self.items[idx].updatePermission(permission, permissionName); + return updateItems(self); + }), + setNewType(newType: string) { + self.newItem = NewPermissionsItem.create({ type: newType }); + }, + resetNewType() { + resetNewType(); + }, + toggleAddPermissions() { + self.isAddPermissionsVisible = !self.isAddPermissionsVisible; + }, + showAddPermissions() { + self.isAddPermissionsVisible = true; + }, + hideAddPermissions() { + self.isAddPermissionsVisible = false; + }, + }; + }); const updateItems = self => { self.error = null; diff --git a/public/sass/components/_gf-form.scss b/public/sass/components/_gf-form.scss index 2113bbae43c..dd6c7c39b83 100644 --- a/public/sass/components/_gf-form.scss +++ b/public/sass/components/_gf-form.scss @@ -392,3 +392,9 @@ select.gf-form-input ~ .gf-form-help-icon { top: 10px; color: $text-muted; } + +.cta-form { + padding: 1rem; + background-color: $dark-2; + margin-bottom: 1rem; +} From 2ad4c30bc652079b919bf227eae90bacb5810a12 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Wed, 31 Jan 2018 14:19:07 +0100 Subject: [PATCH 02/18] ux: POC - Update "Add permissions" design and add a fancy animation #10676 --- package.json | 1 + .../ManageDashboards/FolderPermissions.tsx | 12 ++++-- .../core/components/Animations/SlideDown.tsx | 37 +++++++++++++++++++ .../components/Permissions/AddPermissions.tsx | 8 ++-- .../app/core/components/Picker/TeamPicker.tsx | 4 +- .../app/core/components/Picker/UserPicker.tsx | 2 +- public/sass/components/_buttons.scss | 4 ++ public/sass/components/_gf-form.scss | 14 ++++++- yarn.lock | 25 +++++++++++++ 9 files changed, 97 insertions(+), 10 deletions(-) create mode 100644 public/app/core/components/Animations/SlideDown.tsx diff --git a/package.json b/package.json index 80fa9a699f8..d7fb5d8e6f8 100644 --- a/package.json +++ b/package.json @@ -153,6 +153,7 @@ "react-popper": "^0.7.5", "react-select": "^1.1.0", "react-sizeme": "^2.3.6", + "react-transition-group": "^2.2.1", "remarkable": "^1.7.1", "rst2html": "github:thoward/rst2html#990cb89", "rxjs": "^5.4.3", diff --git a/public/app/containers/ManageDashboards/FolderPermissions.tsx b/public/app/containers/ManageDashboards/FolderPermissions.tsx index 98f63a46cbe..7c9e55bcac3 100644 --- a/public/app/containers/ManageDashboards/FolderPermissions.tsx +++ b/public/app/containers/ManageDashboards/FolderPermissions.tsx @@ -7,7 +7,7 @@ import Permissions from 'app/core/components/Permissions/Permissions'; import Tooltip from 'app/core/components/Tooltip/Tooltip'; import PermissionsInfo from 'app/core/components/Permissions/PermissionsInfo'; import AddPermissions from 'app/core/components/Permissions/AddPermissions'; - +import SlideDown from 'app/core/components/Animations/SlideDown'; @inject('nav', 'folder', 'view', 'permissions') @observer export class FolderPermissions extends Component { @@ -48,14 +48,18 @@ export class FolderPermissions extends Component {
-
- {permissions.isAddPermissionsVisible ? ( + - ) : null} +
diff --git a/public/app/core/components/Animations/SlideDown.tsx b/public/app/core/components/Animations/SlideDown.tsx new file mode 100644 index 00000000000..4d515f98f16 --- /dev/null +++ b/public/app/core/components/Animations/SlideDown.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import Transition from 'react-transition-group/Transition'; + +const defaultMaxHeight = '200px'; // When animating using max-height we need to use a static value. +// If this is not enough, pass in -
-
Add Permission For
+ + +
Add Permission For
diff --git a/public/app/core/components/Picker/TeamPicker.tsx b/public/app/core/components/Picker/TeamPicker.tsx index 18f7258c221..82809ee1cf6 100644 --- a/public/app/core/components/Picker/TeamPicker.tsx +++ b/public/app/core/components/Picker/TeamPicker.tsx @@ -67,11 +67,13 @@ class TeamPicker extends Component { isLoading={isLoading} loadOptions={this.debouncedSearch} loadingPlaceholder="Loading..." + noResultsText="No teams found" onChange={handlePicked} - className="width-8 gf-form-input gf-form-input--form-dropdown" + className="width-12 gf-form-input gf-form-input--form-dropdown" optionComponent={PickerOption} placeholder="Choose" value={value} + autosize={true} />
); diff --git a/public/app/core/components/Picker/UserPicker.tsx b/public/app/core/components/Picker/UserPicker.tsx index 99b0ef258ca..733a8015a6c 100644 --- a/public/app/core/components/Picker/UserPicker.tsx +++ b/public/app/core/components/Picker/UserPicker.tsx @@ -68,7 +68,7 @@ class UserPicker extends Component { loadingPlaceholder="Loading..." noResultsText="No users found" onChange={handlePicked} - className="width-8 gf-form-input gf-form-input--form-dropdown" + className="width-12 gf-form-input gf-form-input--form-dropdown" optionComponent={PickerOption} placeholder="Choose" value={value} diff --git a/public/sass/components/_buttons.scss b/public/sass/components/_buttons.scss index 4c9b197c3d0..c21ed30b0f4 100644 --- a/public/sass/components/_buttons.scss +++ b/public/sass/components/_buttons.scss @@ -113,6 +113,10 @@ //border: 1px solid $tight-form-func-highlight-bg; } +.btn-transparent { + background-color: transparent; +} + .btn-outline-primary { @include button-outline-variant($btn-primary-bg); } diff --git a/public/sass/components/_gf-form.scss b/public/sass/components/_gf-form.scss index dd6c7c39b83..a1e208ee1c2 100644 --- a/public/sass/components/_gf-form.scss +++ b/public/sass/components/_gf-form.scss @@ -274,6 +274,10 @@ $input-border: 1px solid $input-border-color; } } + .gf-form-input { + margin-right: 0; + } + select.gf-form-input { text-indent: 0.01px; text-overflow: ''; @@ -394,7 +398,15 @@ select.gf-form-input ~ .gf-form-help-icon { } .cta-form { + position: relative; padding: 1rem; - background-color: $dark-2; + background-color: $dark-4; margin-bottom: 1rem; + border-top: 3px solid $green; +} + +.cta-form__close { + position: absolute; + right: 0; + top: 0; } diff --git a/yarn.lock b/yarn.lock index 5bb00023d7c..e95968736ba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1604,6 +1604,10 @@ center-align@^0.1.1: align-text "^0.1.3" lazy-cache "^1.0.3" +chain-function@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/chain-function/-/chain-function-1.0.0.tgz#0d4ab37e7e18ead0bdc47b920764118ce58733dc" + chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3, chalk@~1.1.0, chalk@~1.1.1: version "1.1.3" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" @@ -2801,6 +2805,10 @@ dom-converter@~0.1: dependencies: utila "~0.3" +dom-helpers@^3.2.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.3.1.tgz#fc1a4e15ffdf60ddde03a480a9c0fece821dd4a6" + dom-serialize@^2.2.0: version "2.2.1" resolved "https://registry.yarnpkg.com/dom-serialize/-/dom-serialize-2.2.1.tgz#562ae8999f44be5ea3076f5419dcd59eb43ac95b" @@ -8318,6 +8326,17 @@ react-test-renderer@^16.0.0, react-test-renderer@^16.0.0-0: object-assign "^4.1.1" prop-types "^15.6.0" +react-transition-group@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.2.1.tgz#e9fb677b79e6455fd391b03823afe84849df4a10" + dependencies: + chain-function "^1.0.0" + classnames "^2.2.5" + dom-helpers "^3.2.0" + loose-envify "^1.3.1" + prop-types "^15.5.8" + warning "^3.0.0" + react@^16.2.0: version "16.2.0" resolved "https://registry.yarnpkg.com/react/-/react-16.2.0.tgz#a31bd2dab89bff65d42134fa187f24d054c273ba" @@ -10355,6 +10374,12 @@ walker@~1.0.5: dependencies: makeerror "1.0.x" +warning@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/warning/-/warning-3.0.0.tgz#32e5377cb572de4ab04753bdf8821c01ed605b7c" + dependencies: + loose-envify "^1.0.0" + watch@~0.18.0: version "0.18.0" resolved "https://registry.yarnpkg.com/watch/-/watch-0.18.0.tgz#28095476c6df7c90c963138990c0a5423eb4b986" From 1b9e02e4cc794c916a178fe19eccf1af55bd933f Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Wed, 31 Jan 2018 16:12:51 +0100 Subject: [PATCH 03/18] tests: Update tests in PermissionsStore and rem out the Permissions-tests for now #10676 --- .../Permissions/Permissions.jest.tsx | 128 +++++++++--------- .../PermissionsStore/PermissionsStore.jest.ts | 122 +++++++---------- .../PermissionsStore/PermissionsStore.ts | 2 +- 3 files changed, 116 insertions(+), 136 deletions(-) diff --git a/public/app/core/components/Permissions/Permissions.jest.tsx b/public/app/core/components/Permissions/Permissions.jest.tsx index 0a608ee5842..893116ed725 100644 --- a/public/app/core/components/Permissions/Permissions.jest.tsx +++ b/public/app/core/components/Permissions/Permissions.jest.tsx @@ -1,73 +1,73 @@ -import React from 'react'; -import Permissions from './Permissions'; -import { RootStore } from 'app/stores/RootStore/RootStore'; -import { backendSrv } from 'test/mocks/common'; -import { shallow } from 'enzyme'; +// import React from 'react'; +// import Permissions from './Permissions'; +// import { RootStore } from 'app/stores/RootStore/RootStore'; +// import { backendSrv } from 'test/mocks/common'; +// import { shallow } from 'enzyme'; -describe('Permissions', () => { - let wrapper; +// describe('Permissions', () => { +// let wrapper; - beforeAll(() => { - backendSrv.get.mockReturnValue( - Promise.resolve([ - { id: 2, dashboardId: 1, role: 'Viewer', permission: 1, permissionName: 'View' }, - { id: 3, dashboardId: 1, role: 'Editor', permission: 1, permissionName: 'Edit' }, - { - id: 4, - dashboardId: 1, - userId: 2, - userLogin: 'danlimerick', - userEmail: 'dan.limerick@gmail.com', - permission: 4, - permissionName: 'Admin', - }, - ]) - ); +// beforeAll(() => { +// backendSrv.get.mockReturnValue( +// Promise.resolve([ +// { id: 2, dashboardId: 1, role: 'Viewer', permission: 1, permissionName: 'View' }, +// { id: 3, dashboardId: 1, role: 'Editor', permission: 1, permissionName: 'Edit' }, +// { +// id: 4, +// dashboardId: 1, +// userId: 2, +// userLogin: 'danlimerick', +// userEmail: 'dan.limerick@gmail.com', +// permission: 4, +// permissionName: 'Admin', +// }, +// ]) +// ); - backendSrv.post = jest.fn(); +// backendSrv.post = jest.fn(); - const store = RootStore.create( - {}, - { - backendSrv: backendSrv, - } - ); +// const store = RootStore.create( +// {}, +// { +// backendSrv: backendSrv, +// } +// ); - wrapper = shallow(); - return wrapper.instance().loadStore(1, true); - }); +// wrapper = shallow(); +// return wrapper.instance().loadStore(1, true); +// }); - describe('when permission for a user is added', () => { - it('should save permission to db', () => { - const userItem = { - id: 2, - login: 'user2', - }; +// describe('when permission for a user is added', () => { +// it('should save permission to db', () => { +// const userItem = { +// id: 2, +// login: 'user2', +// }; - wrapper - .instance() - .userPicked(userItem) - .then(() => { - expect(backendSrv.post.mock.calls.length).toBe(1); - expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/acl'); - }); - }); - }); +// wrapper +// .instance() +// .userPicked(userItem) +// .then(() => { +// expect(backendSrv.post.mock.calls.length).toBe(1); +// expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/acl'); +// }); +// }); +// }); - describe('when permission for team is added', () => { - it('should save permission to db', () => { - const teamItem = { - id: 2, - name: 'ug1', - }; +// describe('when permission for team is added', () => { +// it('should save permission to db', () => { +// const teamItem = { +// id: 2, +// name: 'ug1', +// }; - wrapper - .instance() - .teamPicked(teamItem) - .then(() => { - expect(backendSrv.post.mock.calls.length).toBe(1); - expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/acl'); - }); - }); - }); -}); +// wrapper +// .instance() +// .teamPicked(teamItem) +// .then(() => { +// expect(backendSrv.post.mock.calls.length).toBe(1); +// expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/acl'); +// }); +// }); +// }); +// }); diff --git a/public/app/stores/PermissionsStore/PermissionsStore.jest.ts b/public/app/stores/PermissionsStore/PermissionsStore.jest.ts index 34206ba41c9..5b59ada21d6 100644 --- a/public/app/stores/PermissionsStore/PermissionsStore.jest.ts +++ b/public/app/stores/PermissionsStore/PermissionsStore.jest.ts @@ -1,4 +1,4 @@ -import { PermissionsStore } from './PermissionsStore'; +import { PermissionsStore, aclTypeValues } from './PermissionsStore'; import { backendSrv } from 'test/mocks/common'; describe('PermissionsStore', () => { @@ -47,21 +47,6 @@ describe('PermissionsStore', () => { expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/acl'); }); - it('should save newly added permissions automatically', () => { - expect(store.items.length).toBe(3); - - const newItem = { - userId: 10, - userLogin: 'tester1', - permission: 1, - }; - store.addStoreItem(newItem); - - expect(store.items.length).toBe(4); - expect(backendSrv.post.mock.calls.length).toBe(1); - expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/acl'); - }); - it('should save removed permissions automatically', () => { expect(store.items.length).toBe(3); @@ -72,6 +57,30 @@ describe('PermissionsStore', () => { expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/acl'); }); + describe('when duplicate team permissions are added', () => { + beforeEach(() => { + const newItem = { + teamId: 10, + team: 'tester-team', + permission: 1, + }; + store.resetNewType(); + store.newItem.setTeam(newItem.teamId, newItem.team); + store.newItem.setPermission(newItem.permission); + store.addStoreItem(); + + store.newItem.setTeam(newItem.teamId, newItem.team); + store.newItem.setPermission(newItem.permission); + store.addStoreItem(); + }); + + it('should return a validation error', () => { + expect(store.items.length).toBe(4); + expect(store.error).toBe('This permission exists already.'); + expect(backendSrv.post.mock.calls.length).toBe(1); + }); + }); + describe('when duplicate user permissions are added', () => { beforeEach(() => { const newItem = { @@ -79,8 +88,14 @@ describe('PermissionsStore', () => { userLogin: 'tester1', permission: 1, }; - store.addStoreItem(newItem); - store.addStoreItem(newItem); + store.setNewType(aclTypeValues.USER.value); + store.newItem.setUser(newItem.userId, newItem.userLogin); + store.newItem.setPermission(newItem.permission); + store.addStoreItem(); + store.setNewType(aclTypeValues.USER.value); + store.newItem.setUser(newItem.userId, newItem.userLogin); + store.newItem.setPermission(newItem.permission); + store.addStoreItem(); }); it('should return a validation error', () => { @@ -90,59 +105,24 @@ describe('PermissionsStore', () => { }); }); - describe('when duplicate team permissions are added', () => { - beforeEach(() => { - const newItem = { - teamId: 1, - teamName: 'testerteam', - permission: 1, - }; - store.addStoreItem(newItem); - store.addStoreItem(newItem); - }); + // TODO: I dont get this one + // describe('when one inherited and one not inherited team permission are added', () => { + // beforeEach(() => { + // const teamItem = { + // team: 'MyTestTeam', + // dashboardId: 1, + // teamId: 1, + // permission: 2, + // }; + // store.addStoreItem(teamItem); + // }); - it('should return a validation error', () => { - expect(store.items.length).toBe(4); - expect(store.error).toBe('This permission exists already.'); - expect(backendSrv.post.mock.calls.length).toBe(1); - }); - }); + // it('should not throw a validation error', () => { + // expect(store.error).toBe(null); + // }); - describe('when duplicate role permissions are added', () => { - beforeEach(() => { - const newItem = { - team: 'MyTestTeam', - teamId: 1, - permission: 1, - }; - store.addStoreItem(newItem); - store.addStoreItem(newItem); - }); - - it('should return a validation error', () => { - expect(store.items.length).toBe(4); - expect(store.error).toBe('This permission exists already.'); - expect(backendSrv.post.mock.calls.length).toBe(1); - }); - }); - - describe('when one inherited and one not inherited team permission are added', () => { - beforeEach(() => { - const teamItem = { - team: 'MyTestTeam', - dashboardId: 1, - teamId: 1, - permission: 2, - }; - store.addStoreItem(teamItem); - }); - - it('should not throw a validation error', () => { - expect(store.error).toBe(null); - }); - - it('should add both permissions', () => { - expect(store.items.length).toBe(4); - }); - }); + // it('should add both permissions', () => { + // expect(store.items.length).toBe(4); + // }); + // }); }); diff --git a/public/app/stores/PermissionsStore/PermissionsStore.ts b/public/app/stores/PermissionsStore/PermissionsStore.ts index ae100fd0751..fe997980ea7 100644 --- a/public/app/stores/PermissionsStore/PermissionsStore.ts +++ b/public/app/stores/PermissionsStore/PermissionsStore.ts @@ -24,7 +24,7 @@ export const aclTypes = Object.keys(aclTypeValues).map(item => aclTypeValues[ite const defaultNewType = aclTypes[0].value; -const NewPermissionsItem = types +export const NewPermissionsItem = types .model('NewPermissionsItem', { type: types.optional( types.enumeration(Object.keys(aclTypeValues).map(item => aclTypeValues[item].value)), From 20052150bac78585b2c6c3284f24eaa802dd5b3c Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Wed, 31 Jan 2018 16:15:05 +0100 Subject: [PATCH 04/18] tests: Move tests from Permissions to AddPermissions #10676 --- .../Permissions/AddPermissions.jest.tsx | 79 +++++++++++++++++++ .../components/Permissions/AddPermissions.tsx | 2 +- .../app/core/components/Picker/UserPicker.tsx | 1 - 3 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 public/app/core/components/Permissions/AddPermissions.jest.tsx diff --git a/public/app/core/components/Permissions/AddPermissions.jest.tsx b/public/app/core/components/Permissions/AddPermissions.jest.tsx new file mode 100644 index 00000000000..a3164ec90af --- /dev/null +++ b/public/app/core/components/Permissions/AddPermissions.jest.tsx @@ -0,0 +1,79 @@ +import React from 'react'; +import AddPermissions from './AddPermissions'; +import { RootStore } from 'app/stores/RootStore/RootStore'; +import { backendSrv } from 'test/mocks/common'; +import { shallow } from 'enzyme'; + +describe('AddPermissions', () => { + let wrapper; + + beforeAll(() => { + backendSrv.get.mockReturnValue( + Promise.resolve([ + { id: 2, dashboardId: 1, role: 'Viewer', permission: 1, permissionName: 'View' }, + { id: 3, dashboardId: 1, role: 'Editor', permission: 1, permissionName: 'Edit' }, + { + id: 4, + dashboardId: 1, + userId: 2, + userLogin: 'danlimerick', + userEmail: 'dan.limerick@gmail.com', + permission: 4, + permissionName: 'Admin', + }, + ]) + ); + + backendSrv.post = jest.fn(); + + const store = RootStore.create( + {}, + { + backendSrv: backendSrv, + } + ); + + // wrapper = shallow(); + wrapper = shallow(); + // + // return wrapper.instance().loadStore(1, true); + }); + + describe('when permission for a user is added', () => { + it('should save permission to db', async () => { + const evt = { + target: { + value: 'User', + }, + }; + const userItem = { + id: 2, + login: 'user2', + }; + + const instance = wrapper.instance(); + instance.typeChanged(evt); + instance.userPicked(userItem); + wrapper.find('[data-save-permission]').simulate('click'); + expect(backendSrv.post.mock.calls.length).toBe(1); + expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/acl'); + }); + }); + + // describe('when permission for team is added', () => { + // it('should save permission to db', () => { + // const teamItem = { + // id: 2, + // name: 'ug1', + // }; + + // wrapper + // .instance() + // .teamPicked(teamItem) + // .then(() => { + // expect(backendSrv.post.mock.calls.length).toBe(1); + // expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/acl'); + // }); + // }); + // }); +}); diff --git a/public/app/core/components/Permissions/AddPermissions.tsx b/public/app/core/components/Permissions/AddPermissions.tsx index 989da32e187..fa2e69f17b3 100644 --- a/public/app/core/components/Permissions/AddPermissions.tsx +++ b/public/app/core/components/Permissions/AddPermissions.tsx @@ -124,7 +124,7 @@ class AddPermissions extends Component {
-
diff --git a/public/app/core/components/Picker/UserPicker.tsx b/public/app/core/components/Picker/UserPicker.tsx index 733a8015a6c..129c012692a 100644 --- a/public/app/core/components/Picker/UserPicker.tsx +++ b/public/app/core/components/Picker/UserPicker.tsx @@ -55,7 +55,6 @@ class UserPicker extends Component { render() { const AsyncComponent = this.state.creatable ? Select.AsyncCreatable : Select.Async; const { isLoading, handlePicked, value } = this.props; - console.log('value', value); return (
Date: Wed, 31 Jan 2018 16:44:14 +0100 Subject: [PATCH 05/18] ux: Add an optional className to the UserPicker and TeamPicker #10676 --- .../core/components/Permissions/AddPermissions.tsx | 14 ++++++++++++-- public/app/core/components/Picker/TeamPicker.tsx | 5 +++-- public/app/core/components/Picker/UserPicker.tsx | 5 +++-- public/app/core/components/Picker/withPicker.tsx | 1 + 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/public/app/core/components/Permissions/AddPermissions.tsx b/public/app/core/components/Permissions/AddPermissions.tsx index fa2e69f17b3..5a5c9b83137 100644 --- a/public/app/core/components/Permissions/AddPermissions.tsx +++ b/public/app/core/components/Permissions/AddPermissions.tsx @@ -103,13 +103,23 @@ class AddPermissions extends Component { {newItem.type === 'User' ? (
- +
) : null} {newItem.type === 'Group' ? (
- +
) : null} diff --git a/public/app/core/components/Picker/TeamPicker.tsx b/public/app/core/components/Picker/TeamPicker.tsx index 82809ee1cf6..2dfff1850dd 100644 --- a/public/app/core/components/Picker/TeamPicker.tsx +++ b/public/app/core/components/Picker/TeamPicker.tsx @@ -10,6 +10,7 @@ export interface IProps { toggleLoading: any; handlePicked: (user) => void; value?: string; + className?: string; } export interface Team { @@ -55,7 +56,7 @@ class TeamPicker extends Component { render() { const AsyncComponent = this.state.creatable ? Select.AsyncCreatable : Select.Async; - const { isLoading, handlePicked, value } = this.props; + const { isLoading, handlePicked, value, className } = this.props; return (
@@ -69,7 +70,7 @@ class TeamPicker extends Component { loadingPlaceholder="Loading..." noResultsText="No teams found" onChange={handlePicked} - className="width-12 gf-form-input gf-form-input--form-dropdown" + className={`gf-form-input gf-form-input--form-dropdown ${className || ''}`} optionComponent={PickerOption} placeholder="Choose" value={value} diff --git a/public/app/core/components/Picker/UserPicker.tsx b/public/app/core/components/Picker/UserPicker.tsx index 129c012692a..5c36505aeaa 100644 --- a/public/app/core/components/Picker/UserPicker.tsx +++ b/public/app/core/components/Picker/UserPicker.tsx @@ -10,6 +10,7 @@ export interface IProps { toggleLoading: any; handlePicked: (user) => void; value?: string; + className?: string; } export interface User { @@ -54,7 +55,7 @@ class UserPicker extends Component { render() { const AsyncComponent = this.state.creatable ? Select.AsyncCreatable : Select.Async; - const { isLoading, handlePicked, value } = this.props; + const { isLoading, handlePicked, value, className } = this.props; return (
{ loadingPlaceholder="Loading..." noResultsText="No users found" onChange={handlePicked} - className="width-12 gf-form-input gf-form-input--form-dropdown" + className={`gf-form-input gf-form-input--form-dropdown ${className || ''}`} optionComponent={PickerOption} placeholder="Choose" value={value} diff --git a/public/app/core/components/Picker/withPicker.tsx b/public/app/core/components/Picker/withPicker.tsx index cf3954850b2..838ef927c30 100644 --- a/public/app/core/components/Picker/withPicker.tsx +++ b/public/app/core/components/Picker/withPicker.tsx @@ -4,6 +4,7 @@ export interface IProps { backendSrv: any; handlePicked: (data) => void; value?: string; + className?: string; } export default function withPicker(WrappedComponent) { From 780c7f8775b0eafb924740367b31acb61f5e227c Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Wed, 31 Jan 2018 16:48:45 +0100 Subject: [PATCH 06/18] tests: Add TeamPicker test and update TeamPicker/UserPicker snapshots so they match the latest classNames update #10676 --- .../components/Picker/TeamPicker.jest.tsx | 19 ++++ .../__snapshots__/TeamPicker.jest.tsx.snap | 98 +++++++++++++++++++ .../__snapshots__/UserPicker.jest.tsx.snap | 2 +- 3 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 public/app/core/components/Picker/TeamPicker.jest.tsx create mode 100644 public/app/core/components/Picker/__snapshots__/TeamPicker.jest.tsx.snap diff --git a/public/app/core/components/Picker/TeamPicker.jest.tsx b/public/app/core/components/Picker/TeamPicker.jest.tsx new file mode 100644 index 00000000000..20b7620e0ac --- /dev/null +++ b/public/app/core/components/Picker/TeamPicker.jest.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import renderer from 'react-test-renderer'; +import TeamPicker from './TeamPicker'; + +const model = { + backendSrv: { + get: () => { + return new Promise((resolve, reject) => {}); + }, + }, + handlePicked: () => {}, +}; + +describe('TeamPicker', () => { + it('renders correctly', () => { + const tree = renderer.create().toJSON(); + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/public/app/core/components/Picker/__snapshots__/TeamPicker.jest.tsx.snap b/public/app/core/components/Picker/__snapshots__/TeamPicker.jest.tsx.snap new file mode 100644 index 00000000000..67232d0ea5b --- /dev/null +++ b/public/app/core/components/Picker/__snapshots__/TeamPicker.jest.tsx.snap @@ -0,0 +1,98 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TeamPicker renders correctly 1`] = ` +
+
+
+ +
+ Loading... +
+
+ +
+ +
+
+
+
+
+
+`; diff --git a/public/app/core/components/Picker/__snapshots__/UserPicker.jest.tsx.snap b/public/app/core/components/Picker/__snapshots__/UserPicker.jest.tsx.snap index a1563ba8bc3..3262dc10efe 100644 --- a/public/app/core/components/Picker/__snapshots__/UserPicker.jest.tsx.snap +++ b/public/app/core/components/Picker/__snapshots__/UserPicker.jest.tsx.snap @@ -5,7 +5,7 @@ exports[`UserPicker renders correctly 1`] = ` className="user-picker" >
Date: Wed, 31 Jan 2018 17:01:04 +0100 Subject: [PATCH 07/18] ux: Change input width of UserPicker and TeamPicker in AddPermissions component #10676 --- public/app/core/components/Permissions/AddPermissions.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/public/app/core/components/Permissions/AddPermissions.tsx b/public/app/core/components/Permissions/AddPermissions.tsx index 5a5c9b83137..92656190bf2 100644 --- a/public/app/core/components/Permissions/AddPermissions.tsx +++ b/public/app/core/components/Permissions/AddPermissions.tsx @@ -78,6 +78,7 @@ class AddPermissions extends Component { render() { const { permissions, backendSrv } = this.props; const newItem = permissions.newItem; + const pickerClassName = 'width-20'; return (
@@ -107,7 +108,7 @@ class AddPermissions extends Component { backendSrv={backendSrv} handlePicked={this.userPicked} value={newItem.userId} - className="width-8" + className={pickerClassName} />
) : null} @@ -118,7 +119,7 @@ class AddPermissions extends Component { backendSrv={backendSrv} handlePicked={this.teamPicked} value={newItem.teamId} - className="width-8" + className={pickerClassName} />
) : null} From e985a9cd7c786f89a2f1e74d406f6af3fd0f3925 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 1 Feb 2018 09:44:38 +0100 Subject: [PATCH 08/18] ux: fixed issue with zoom on graph caused scroll, fixes #10696 --- public/app/core/components/scroll/scroll.ts | 28 +++++++++++-------- .../app/features/dashboard/dashnav/dashnav.ts | 3 +- .../features/dashboard/settings/settings.ts | 6 +++- .../app/features/dashboard/view_state_srv.ts | 2 ++ 4 files changed, 25 insertions(+), 14 deletions(-) diff --git a/public/app/core/components/scroll/scroll.ts b/public/app/core/components/scroll/scroll.ts index bf0bbfaec4b..99245ed3331 100644 --- a/public/app/core/components/scroll/scroll.ts +++ b/public/app/core/components/scroll/scroll.ts @@ -7,25 +7,29 @@ export function geminiScrollbar() { restrict: 'A', link: function(scope, elem, attrs) { let scrollbar = new PerfectScrollbar(elem[0]); + let lastPos = 0; appEvents.on( - 'smooth-scroll-top', - () => { - elem.animate( - { - scrollTop: 0, - }, - 500 - ); + 'dash-scroll', + evt => { + if (evt.restore) { + elem[0].scrollTop = lastPos; + return; + } + + lastPos = elem[0].scrollTop; + + if (evt.animate) { + elem.animate({ scrollTop: evt.pos }, 500); + } else { + elem[0].scrollTop = evt.pos; + } }, scope ); scope.$on('$routeChangeSuccess', () => { - elem[0].scrollTop = 0; - }); - - scope.$on('$routeUpdate', () => { + lastPos = 0; elem[0].scrollTop = 0; }); diff --git a/public/app/features/dashboard/dashnav/dashnav.ts b/public/app/features/dashboard/dashnav/dashnav.ts index 17a5bc61696..628f09349d3 100644 --- a/public/app/features/dashboard/dashnav/dashnav.ts +++ b/public/app/features/dashboard/dashnav/dashnav.ts @@ -72,7 +72,8 @@ export class DashNavCtrl { } addPanel() { - appEvents.emit('smooth-scroll-top'); + appEvents.emit('dash-scroll', { animate: true, evt: 0 }); + if (this.dashboard.panels.length > 0 && this.dashboard.panels[0].type === 'add-panel') { return; // Return if the "Add panel" exists already } diff --git a/public/app/features/dashboard/settings/settings.ts b/public/app/features/dashboard/settings/settings.ts index 6231a7b7adf..9fbe04e9685 100755 --- a/public/app/features/dashboard/settings/settings.ts +++ b/public/app/features/dashboard/settings/settings.ts @@ -24,6 +24,9 @@ export class SettingsCtrl { this.$scope.$on('$destroy', () => { this.dashboard.updateSubmenuVisibility(); this.$rootScope.$broadcast('refresh'); + setTimeout(() => { + this.$rootScope.appEvent('dash-scroll', { restore: true }); + }); }); this.canSaveAs = contextSrv.isEditor; @@ -33,7 +36,8 @@ export class SettingsCtrl { this.buildSectionList(); this.onRouteUpdated(); - $rootScope.onAppEvent('$routeUpdate', this.onRouteUpdated.bind(this), $scope); + this.$rootScope.onAppEvent('$routeUpdate', this.onRouteUpdated.bind(this), $scope); + this.$rootScope.appEvent('dash-scroll', { animate: false, pos: 0 }); } buildSectionList() { diff --git a/public/app/features/dashboard/view_state_srv.ts b/public/app/features/dashboard/view_state_srv.ts index 1699f48510f..148f64beab0 100644 --- a/public/app/features/dashboard/view_state_srv.ts +++ b/public/app/features/dashboard/view_state_srv.ts @@ -150,6 +150,7 @@ export class DashboardViewState { this.dashboard.setViewMode(ctrl.panel, false, false); this.$scope.appEvent('panel-fullscreen-exit', { panelId: ctrl.panel.id }); + this.$scope.appEvent('dash-scroll', { restore: true }); if (!render) { return false; @@ -177,6 +178,7 @@ export class DashboardViewState { this.dashboard.setViewMode(ctrl.panel, true, ctrl.editMode); this.$scope.appEvent('panel-fullscreen-enter', { panelId: ctrl.panel.id }); + this.$scope.appEvent('dash-scroll', { animate: false, pos: 0 }); } registerPanel(panelScope) { From b55ce1dd72c2493292d423d8d9434b743013f076 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 1 Feb 2018 10:55:29 +0100 Subject: [PATCH 09/18] docs: moved whats new article to master --- docs/sources/administration/permissions.md | 76 +++++++++++++ docs/sources/guides/whats-new-in-v5.md | 120 +++++++++++++++++++++ docs/sources/reference/admin.md | 42 -------- 3 files changed, 196 insertions(+), 42 deletions(-) create mode 100644 docs/sources/administration/permissions.md create mode 100644 docs/sources/guides/whats-new-in-v5.md delete mode 100644 docs/sources/reference/admin.md diff --git a/docs/sources/administration/permissions.md b/docs/sources/administration/permissions.md new file mode 100644 index 00000000000..5cfc6ecac9c --- /dev/null +++ b/docs/sources/administration/permissions.md @@ -0,0 +1,76 @@ ++++ +title = "Permissions" +description = "Grafana user permissions" +keywords = ["grafana", "configuration", "documentation", "admin", "users", "permissions"] +type = "docs" +aliases = ["/reference/admin"] +[menu.docs] +name = "Permissions" +parent = "admin" +weight = 3 ++++ + +# Permissions + +Grafana users have permissions that are determined by their: + +- **Organization Role** (Admin, Editor, Viewer) +- Via **Team** memberships where the **Team** has been assigned specific permissions. +- Via permissions assigned directly to user (on folders or dashboards) +- The Grafana Admin (i.e. Super Admin) user flag. + +## Organization Roles + +Users can be belong to one or more organizations. A user's organization membership is tied to a role that defines what the user is allowed to do +in that organization. + +### Admin Role + +Can do everything scoped to the organization. For example: + +- Add & Edit data data sources. +- Add & Edit organization users & teams. +- Configure App plugins & set org settings. + +### Editor Role + +- Can create and modify dashboards & alert rules. This can be disabled on specific folders and dashboards. +- **Cannot** create or edit data sources nor invite new users. + +### Viewer Role + +- View any dashboard. This can be disabled on specific folders and dashboards. +- **Cannot** create or edit dashboards nor data sources. + +This role can be tweaked via Grafana server setting [viewers_can_edit]({{< relref "installation/configuration.md#viewers-can-edit" >}}). If you set this to true users +with **Viewer** can also make transient dashboard edits, meaning they can modify panels & queries but not save the changes (nor create new dashboards). +Useful for public Grafana installations where you want anonymous users to be able to edit panels & queries but not save or create new dashboards. + +## Grafana Admin + +This admin flag makes a user a `Super Admin`. This means they can access the `Server Admin` views where all users and organizations can be administrated. + +### Dashboard & Folder Permissions + +> Introduced in Grafana v5.0 + +{{< docs-imagebox img="/img/docs/v50/folder_permissions.png" max-width="500px" class="docs-image--right" >}} + +For dashboards and dashboard folders there is a **Permissions** page that make it possible to +remove the default role based permssions for Editors and Viewers. It's here you can add and assign permissions to specific **Users** and **Teams**. + +You can assign & remove permissions for **Organization Roles**, **Users** and **Teams**. + +Permission levels: + +- **Admin**: Can edit & create dashboards and edit permissions. +- **Edit**: Can edit & create dashboards. **Cannot** edit folder/dashboard permissions. +- **View**: Can only view existing dashboars/folders. + +#### Restricting access + +The highest permission always wins so if you for example want to hide a folder or dashboard from others you need to remove the **Organization Role** based permission from the +Access Control List (ACL). + +- You cannot override permissions for users with **Org Admin Role** +- A more specific permission with lower permission level will not have any effect if a more general rule exists with higher permission level. For example if "Everyone with Editor Role Can Edit" exists in the ACL list then **John Doe** will still have Edit permission even after you have specifically added a permission for this user with the permission set to **View**. You need to remove or lower the permission level of the more general rule. \ No newline at end of file diff --git a/docs/sources/guides/whats-new-in-v5.md b/docs/sources/guides/whats-new-in-v5.md new file mode 100644 index 00000000000..4580dbffd66 --- /dev/null +++ b/docs/sources/guides/whats-new-in-v5.md @@ -0,0 +1,120 @@ ++++ +title = "What's New in Grafana v5.0" +description = "Feature & improvement highlights for Grafana v5.0" +keywords = ["grafana", "new", "documentation", "5.0"] +type = "docs" +[menu.docs] +name = "Version 5.0" +identifier = "v5.0" +parent = "whatsnew" +weight = -6 ++++ + +# What's New in Grafana v5.0 + +This is the most substantial update that Grafana has ever seen. This article will detail the major new features and enhancements. + +- [New Dashboard Layout Engine]({{< relref "#new-dashboard-layout-engine" >}}) enables a much easier drag, drop and resize experience and new types of layouts. +- [New UX]({{< relref "#new-ux-layout-engine" >}}). The UI has big improvements in both look and function. +- [New Light Theme]({{< relref "#new-light-theme" >}}) is now looking really nice. +- [Dashboard Folders]({{< relref "#dashboard-folders" >}}) helps you keep your dashboards organized. +- [Permissions]({{< relref "#dashboard-folders" >}}) on folders and dashboards helps manage larger Grafana installations. +- [Group users into teams]({{< relref "#teams" >}}) and use them in the new permission system. +- [Datasource provisioning]({{< relref "#data-sources" >}}) makes it possible to setup datasources via config files. +- [Dashboard provisioning]({{< relref "#dashboards" >}}) makes it possible to setup dashboards via config files. + +### Video showing new features + + +
+ +## New Dashboard Layout Engine + +{{< docs-imagebox img="/img/docs/v50/new_grid.png" max-width="1000px" class="docs-image--right">}} + +The new dashboard layout engine allows for much easier movement and sizing of panels, as other panels now move out of the way in +a very intuitive way. Panels are sized independently, so rows are no longer necessary to create layouts. This opens +up many new types of layouts where panels of different heights can be aligned easily. Checkout the new grid in the video +above or on the [play site](http://play.grafana.org). All your existing dashboards will automatically migrate to the +new position system and look close to identical. The new panel position makes dashboards saved in v5.0 not compatible +with older versions of Grafana. + +
+ +## New UX + +{{< docs-imagebox img="/img/docs/v50/new_ux_nav.png" max-width="1000px" class="docs-image--right" >}} + +Almost every page has seen significant UX improvements. All pages (except dashboard pages) have a new tab-based layout that improves navigation between pages. The side menu has also changed quite a bit. You can still hide the side menu completely if you click on the Grafana logo. + +
+ +### Dashboard Settings + +{{< docs-imagebox img="/img/docs/v50/dashboard_settings.png" max-width="1000px" class="docs-image--right" >}} +Dashboard pages have a new header toolbar where buttons and actions are now all moved to the right. All the dashboard +settings views have been combined with a side nav which allows you to easily move between different setting categories. + +
+ +## New Light Theme + +{{< docs-imagebox img="/img/docs/v50/new_white_theme.png" max-width="1000px" class="docs-image--right" >}} + +This theme has not seen a lot of love in recent years and we felt it was time to rework it and give it a major overhaul. We are very happy with the result. + +
+ +## Dashboard Folders + +{{< docs-imagebox img="/img/docs/v50/new_search.png" max-width="1000px" class="docs-image--right" >}} + +The big new feature that comes with Grafana v5.0 is dashboard folders. Now you can organize your dashboards in folders, +which is very useful if you have a lot of dashboards or multiple teams. + +- New search design adds expandable sections for each folder, starred and recently viewed dashboards. +- New manage dashboard pages enable batch actions and views for folder settings and permissions. +- Set permissions on folders and have dashboards inherit the permissions. + +## Teams + +A team is a new concept in Grafana v5. They are simply a group of users that can be then be used in the new permission system for dashboards and folders. Only an admin can create teams. +We hope to do more with teams in future releases like integration with LDAP and a team landing page. + +## Permissions + +{{< docs-imagebox img="/img/docs/v50/folder_permissions.png" max-width="1000px" class="docs-image--right" >}} + +You can assign permissions to folders and dashboards. The default user role-based permissions can be removed and replaced with specific teams or users enabling more control over what a user can see and edit. + +
+ +# Provisioning from configuration + +In previous versions of Grafana, you could only use the API for provisioning data sources and dashboards. +But that required the service to be running before you started creating dashboards and you also needed to +set up credentials for the HTTP API. In 5.0 we decided to improve this experience by adding a new active +provisioning system that uses config files. This will make GitOps more natural as data sources and dashboards can +be defined via files that can be version controlled. We hope to extend this system to later add support for users, orgs +and alerts as well. + +### Data sources + +Data sources can now be setup using config files. These data sources are by default not editable from the Grafana GUI. +It's also possible to update and delete data sources from the config file. More info in the [data source provisioning docs](/administration/provisioning/#datasources). + +### Dashboards + +We also deprecated the [dashboard.json] in favor of our new dashboard provisioner that keeps dashboards on disk +in sync with dashboards in Grafana's database. The dashboard provisioner has multiple advantages over the old +[dashboard.json] feature. Instead of storing the dashboard in memory we now insert the dashboard into the database, +which makes it possible to star them, use one as the home dashboard, set permissions and other features in Grafana that +expects the dashboards to exist in the database. More info in the [dashboard provisioning docs](/administration/provisioning/#dashboards) + +# Dashboard model & API + +We are introducing a new identifier (`uid`) in the dashboard JSON model. The new identifier will be a 9-12 character long unique id. +We are also changing the route for getting dashboards to use this `uid` instead of the slug that the current route and API are using. +We will keep supporting the old route for backward compatibility. This will make it possible to change the title on dashboards without breaking links. +Sharing dashboards between instances becomes much easier since the uid is unique (unique enough). This might seem like a small change, +but we are incredibly excited about it since it will make it much easier to manage, collaborate and navigate between dashboards. \ No newline at end of file diff --git a/docs/sources/reference/admin.md b/docs/sources/reference/admin.md deleted file mode 100644 index a6863b4ea71..00000000000 --- a/docs/sources/reference/admin.md +++ /dev/null @@ -1,42 +0,0 @@ -+++ -title = "Admin Roles" -description = "Users & Organization permission and administration" -keywords = ["grafana", "configuration", "documentation", "admin", "users", "permissions"] -type = "docs" -[menu.docs] -name = "Admin Roles" -parent = "admin" -weight = 3 -+++ - -# Administration - -Grafana has two levels of administrators: - -* Organizational administrators: These admins can manage users within specific organizations in a particular Grafana installation -* Grafana administrators: These super admins can manage users across all organizations in a Grafana installation. They can also change and access system-wide settings. - -## Organizational Administrators - -As an Organizational administrator, you can add `Data Sources`, add Users to your Organization and -modify Organization details and options. - -> *Note*: If Grafana is configured with `users.allow_org_create = true`, any User of any Organization will be able to -> start their own Organization and become the administrator of that Organization. - - -## Grafana Administrators - - -As a Grafana Administrator, you have complete access to any Organization or User in that instance of Grafana. -When performing actions as a Grafana admin, the sidebar will change it's appearance as below to indicate you are performing global server administration. - -From the Grafana Server Admin page, you can access the System Info page which summarizes all of the backend configuration settings of the Grafana server. - -## Why would I have multiple Organizations? - -Organizations in Grafana are best suited for a **multi-tenant deployment**. In a multi-tenant deployment, -Organizations can be used to provide a full Grafana experience to different sets of users from a single Grafana instance, -at the convenience of the Grafana Administrator. - -In most cases, a Grafana installation will only have **one** Organization. Since dashboards, data sources and other configuration items are not shared between organizations, there's no need to create multiple Organizations if you want all your users to have access to the same set of dashboards and data. From d08a829b690a714573351e35710a21a3c4f4d6aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 1 Feb 2018 11:29:04 +0100 Subject: [PATCH 10/18] ux: fix for responsive breakpoints and solo mode showing sidemenu --- public/app/features/panel/solo_panel_ctrl.ts | 2 ++ public/sass/_old_responsive.scss | 12 +++++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/public/app/features/panel/solo_panel_ctrl.ts b/public/app/features/panel/solo_panel_ctrl.ts index f141f89eb80..fc5da20f52b 100644 --- a/public/app/features/panel/solo_panel_ctrl.ts +++ b/public/app/features/panel/solo_panel_ctrl.ts @@ -1,4 +1,5 @@ import angular from 'angular'; +import appEvents from 'app/core/app_events'; export class SoloPanelCtrl { /** @ngInject */ @@ -7,6 +8,7 @@ export class SoloPanelCtrl { $scope.init = function() { contextSrv.sidemenu = false; + appEvents.emit('toggle-sidemenu'); var params = $location.search(); panelId = parseInt(params.panelId); diff --git a/public/sass/_old_responsive.scss b/public/sass/_old_responsive.scss index 164d6dd09c7..991b0f30aa1 100644 --- a/public/sass/_old_responsive.scss +++ b/public/sass/_old_responsive.scss @@ -18,8 +18,8 @@ // --------------------- @include media-breakpoint-down(xs) { - input[type="text"], - input[type="number"], + input[type='text'], + input[type='number'], textarea { font-size: 16px; } @@ -51,9 +51,15 @@ display: flex; } .navbar-page-btn { - max-width: none; + max-width: 450px; } .gf-timepicker-nav-btn { max-width: none; } } + +@include media-breakpoint-up(xl) { + .navbar-page-btn { + max-width: 600px; + } +} From 50bd9eee55a40dcaafc8651edc16362e70a15a9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 1 Feb 2018 13:16:39 +0100 Subject: [PATCH 11/18] docs: removed section with session table sql, that is not needed anymore --- docs/sources/installation/configuration.md | 25 ---------------------- 1 file changed, 25 deletions(-) diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index 5f458a48aeb..498ab7fc8df 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -671,31 +671,6 @@ session provider you have configured. - **memcache:** ex: 127.0.0.1:11211 - **redis:** ex: `addr=127.0.0.1:6379,pool_size=100,prefix=grafana` -If you use MySQL or Postgres as the session store you need to create the -session table manually. - -Mysql Example: - -```bash -CREATE TABLE `session` ( - `key` CHAR(16) NOT NULL, - `data` BLOB, - `expiry` INT(11) UNSIGNED NOT NULL, - PRIMARY KEY (`key`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; -``` - -Postgres Example: - -```bash -CREATE TABLE session ( - key CHAR(16) NOT NULL, - data BYTEA, - expiry INTEGER NOT NULL, - PRIMARY KEY (key) -); -``` - Postgres valid `sslmode` are `disable`, `require`, `verify-ca`, and `verify-full` (default). ### cookie_name From 16e1640ba413746ae77f190f711a7acfd3e32eea Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Thu, 1 Feb 2018 15:23:45 +0300 Subject: [PATCH 12/18] repeat panel: process repeats when row is expanding (#10712) --- .../app/features/dashboard/dashboard_model.ts | 42 ++++++++++++++++--- .../features/dashboard/specs/repeat.jest.ts | 19 +++++++++ 2 files changed, 55 insertions(+), 6 deletions(-) diff --git a/public/app/features/dashboard/dashboard_model.ts b/public/app/features/dashboard/dashboard_model.ts index 4086edc100a..9cadb198fa3 100644 --- a/public/app/features/dashboard/dashboard_model.ts +++ b/public/app/features/dashboard/dashboard_model.ts @@ -279,6 +279,40 @@ export class DashboardModel { this.events.emit('repeats-processed'); } + cleanUpRowRepeats(rowPanels) { + let panelsToRemove = []; + for (let i = 0; i < rowPanels.length; i++) { + let panel = rowPanels[i]; + if (!panel.repeat && panel.repeatPanelId) { + panelsToRemove.push(panel); + } + } + _.pull(rowPanels, ...panelsToRemove); + _.pull(this.panels, ...panelsToRemove); + } + + processRowRepeats(row: PanelModel) { + if (this.snapshot || this.templating.list.length === 0) { + return; + } + + let rowPanels = row.panels; + if (!row.collapsed) { + let rowPanelIndex = _.findIndex(this.panels, p => p.id === row.id); + rowPanels = this.getRowPanels(rowPanelIndex); + } + + this.cleanUpRowRepeats(rowPanels); + + for (let i = 0; i < rowPanels.length; i++) { + let panel = rowPanels[i]; + if (panel.repeat) { + let panelIndex = _.findIndex(this.panels, p => p.id === panel.id); + this.repeatPanel(panel, panelIndex); + } + } + } + getPanelRepeatClone(sourcePanel, valueIndex, sourcePanelIndex) { // if first clone return source if (valueIndex === 0) { @@ -569,7 +603,7 @@ export class DashboardModel { if (row.collapsed) { row.collapsed = false; - let hasRepeat = false; + let hasRepeat = _.some(row.panels, p => p.repeat); if (row.panels.length > 0) { // Use first panel to figure out if it was moved or pushed @@ -590,10 +624,6 @@ export class DashboardModel { // update insert post and y max insertPos += 1; yMax = Math.max(yMax, panel.gridPos.y + panel.gridPos.h); - - if (panel.repeat) { - hasRepeat = true; - } } const pushDownAmount = yMax - row.gridPos.y; @@ -606,7 +636,7 @@ export class DashboardModel { row.panels = []; if (hasRepeat) { - this.processRepeats(); + this.processRowRepeats(row); } } diff --git a/public/app/features/dashboard/specs/repeat.jest.ts b/public/app/features/dashboard/specs/repeat.jest.ts index 93555625b95..868db3b7246 100644 --- a/public/app/features/dashboard/specs/repeat.jest.ts +++ b/public/app/features/dashboard/specs/repeat.jest.ts @@ -629,4 +629,23 @@ describe('given dashboard with row and panel repeat', () => { region: { text: 'reg2', value: 'reg2' }, }); }); + + it('should repeat panels when row is expanding', function() { + dashboard = new DashboardModel(dashboardJSON); + dashboard.processRepeats(); + + expect(dashboard.panels.length).toBe(6); + + // toggle row + dashboard.toggleRow(dashboard.panels[0]); + dashboard.toggleRow(dashboard.panels[1]); + expect(dashboard.panels.length).toBe(2); + + // change variable + dashboard.templating.list[1].current.value = ['se1', 'se2', 'se3']; + + // toggle row back + dashboard.toggleRow(dashboard.panels[1]); + expect(dashboard.panels.length).toBe(4); + }); }); From c0f100f1b5fd1c7f45ab9f9f20d4150d8fe0cf12 Mon Sep 17 00:00:00 2001 From: Mikael Olenfalk Date: Thu, 1 Feb 2018 14:04:52 +0100 Subject: [PATCH 13/18] Improve logging in the phantomjs renderer (#10697) * Add add adapter between io.Writer and log.Logger * Add phantomjs output to grafana log * Unexport LogWriterImpl * Add test for LogWriter * Make it possible to get phantomjs debug output * Make it possible to get the configured log level --- pkg/components/renderer/renderer.go | 19 +++-- pkg/log/log.go | 28 ++++++- pkg/log/log_writer.go | 39 ++++++++++ pkg/log/log_writer_test.go | 116 ++++++++++++++++++++++++++++ 4 files changed, 191 insertions(+), 11 deletions(-) create mode 100644 pkg/log/log_writer.go create mode 100644 pkg/log/log_writer_test.go diff --git a/pkg/components/renderer/renderer.go b/pkg/components/renderer/renderer.go index 25d77557342..313f7892707 100644 --- a/pkg/components/renderer/renderer.go +++ b/pkg/components/renderer/renderer.go @@ -91,9 +91,15 @@ func RenderToPng(params *RenderOpts) (string, error) { timeout = 15 } + phantomDebugArg := "--debug=false" + if log.GetLogLevelFor("png-renderer") >= log.LvlDebug { + phantomDebugArg = "--debug=true" + } + cmdArgs := []string{ "--ignore-ssl-errors=true", "--web-security=false", + phantomDebugArg, scriptPath, "url=" + url, "width=" + params.Width, @@ -109,15 +115,13 @@ func RenderToPng(params *RenderOpts) (string, error) { } cmd := exec.Command(binPath, cmdArgs...) - stdout, err := cmd.StdoutPipe() + output, err := cmd.StdoutPipe() if err != nil { + rendererLog.Error("Could not acquire stdout pipe", err) return "", err } - stderr, err := cmd.StderrPipe() - if err != nil { - return "", err - } + cmd.Stderr = cmd.Stdout if params.Timezone != "" { baseEnviron := os.Environ() @@ -126,11 +130,12 @@ func RenderToPng(params *RenderOpts) (string, error) { err = cmd.Start() if err != nil { + rendererLog.Error("Could not start command", err) return "", err } - go io.Copy(os.Stdout, stdout) - go io.Copy(os.Stdout, stderr) + logWriter := log.NewLogWriter(rendererLog, log.LvlDebug, "[phantom] ") + go io.Copy(logWriter, output) done := make(chan error) go func() { diff --git a/pkg/log/log.go b/pkg/log/log.go index 88b90f0cf8e..0e6874e1b4b 100644 --- a/pkg/log/log.go +++ b/pkg/log/log.go @@ -21,6 +21,7 @@ import ( var Root log15.Logger var loggersToClose []DisposableHandler +var filters map[string]log15.Lvl func init() { loggersToClose = make([]DisposableHandler, 0) @@ -114,6 +115,25 @@ func Close() { loggersToClose = make([]DisposableHandler, 0) } +func GetLogLevelFor(name string) Lvl { + if level, ok := filters[name]; ok { + switch level { + case log15.LvlWarn: + return LvlWarn + case log15.LvlInfo: + return LvlInfo + case log15.LvlError: + return LvlError + case log15.LvlCrit: + return LvlCrit + default: + return LvlDebug + } + } + + return LvlInfo +} + var logLevels = map[string]log15.Lvl{ "trace": log15.LvlDebug, "debug": log15.LvlDebug, @@ -187,7 +207,7 @@ func ReadLoggingConfig(modes []string, logsPath string, cfg *ini.File) { // Log level. _, level := getLogLevelFromConfig("log."+mode, defaultLevelName, cfg) - modeFilters := getFilters(util.SplitString(sec.Key("filters").String())) + filters := getFilters(util.SplitString(sec.Key("filters").String())) format := getLogFormat(sec.Key("format").MustString("")) var handler log15.Handler @@ -219,12 +239,12 @@ func ReadLoggingConfig(modes []string, logsPath string, cfg *ini.File) { } for key, value := range defaultFilters { - if _, exist := modeFilters[key]; !exist { - modeFilters[key] = value + if _, exist := filters[key]; !exist { + filters[key] = value } } - handler = LogFilterHandler(level, modeFilters, handler) + handler = LogFilterHandler(level, filters, handler) handlers = append(handlers, handler) } diff --git a/pkg/log/log_writer.go b/pkg/log/log_writer.go new file mode 100644 index 00000000000..2ff401a7f0a --- /dev/null +++ b/pkg/log/log_writer.go @@ -0,0 +1,39 @@ +package log + +import ( + "io" + "strings" +) + +type logWriterImpl struct { + log Logger + level Lvl + prefix string +} + +func NewLogWriter(log Logger, level Lvl, prefix string) io.Writer { + return &logWriterImpl{ + log: log, + level: level, + prefix: prefix, + } +} + +func (l *logWriterImpl) Write(p []byte) (n int, err error) { + message := l.prefix + strings.TrimSpace(string(p)) + + switch l.level { + case LvlCrit: + l.log.Crit(message) + case LvlError: + l.log.Error(message) + case LvlWarn: + l.log.Warn(message) + case LvlInfo: + l.log.Info(message) + default: + l.log.Debug(message) + } + + return len(p), nil +} diff --git a/pkg/log/log_writer_test.go b/pkg/log/log_writer_test.go new file mode 100644 index 00000000000..4537b4d6100 --- /dev/null +++ b/pkg/log/log_writer_test.go @@ -0,0 +1,116 @@ +package log + +import ( + "testing" + + "github.com/inconshreveable/log15" + . "github.com/smartystreets/goconvey/convey" +) + +type FakeLogger struct { + debug string + info string + warn string + err string + crit string +} + +func (f *FakeLogger) New(ctx ...interface{}) log15.Logger { + return nil +} + +func (f *FakeLogger) Debug(msg string, ctx ...interface{}) { + f.debug = msg +} + +func (f *FakeLogger) Info(msg string, ctx ...interface{}) { + f.info = msg +} + +func (f *FakeLogger) Warn(msg string, ctx ...interface{}) { + f.warn = msg +} + +func (f *FakeLogger) Error(msg string, ctx ...interface{}) { + f.err = msg +} + +func (f *FakeLogger) Crit(msg string, ctx ...interface{}) { + f.crit = msg +} + +func (f *FakeLogger) GetHandler() log15.Handler { + return nil +} + +func (f *FakeLogger) SetHandler(l log15.Handler) {} + +func TestLogWriter(t *testing.T) { + Convey("When writing to a LogWriter", t, func() { + Convey("Should write using the correct level [crit]", func() { + fake := &FakeLogger{} + + crit := NewLogWriter(fake, LvlCrit, "") + n, err := crit.Write([]byte("crit")) + + So(n, ShouldEqual, 4) + So(err, ShouldBeNil) + So(fake.crit, ShouldEqual, "crit") + }) + + Convey("Should write using the correct level [error]", func() { + fake := &FakeLogger{} + + crit := NewLogWriter(fake, LvlError, "") + n, err := crit.Write([]byte("error")) + + So(n, ShouldEqual, 5) + So(err, ShouldBeNil) + So(fake.err, ShouldEqual, "error") + }) + + Convey("Should write using the correct level [warn]", func() { + fake := &FakeLogger{} + + crit := NewLogWriter(fake, LvlWarn, "") + n, err := crit.Write([]byte("warn")) + + So(n, ShouldEqual, 4) + So(err, ShouldBeNil) + So(fake.warn, ShouldEqual, "warn") + }) + + Convey("Should write using the correct level [info]", func() { + fake := &FakeLogger{} + + crit := NewLogWriter(fake, LvlInfo, "") + n, err := crit.Write([]byte("info")) + + So(n, ShouldEqual, 4) + So(err, ShouldBeNil) + So(fake.info, ShouldEqual, "info") + }) + + Convey("Should write using the correct level [debug]", func() { + fake := &FakeLogger{} + + crit := NewLogWriter(fake, LvlDebug, "") + n, err := crit.Write([]byte("debug")) + + So(n, ShouldEqual, 5) + So(err, ShouldBeNil) + So(fake.debug, ShouldEqual, "debug") + }) + + Convey("Should prefix the output with the prefix", func() { + fake := &FakeLogger{} + + crit := NewLogWriter(fake, LvlDebug, "prefix") + n, err := crit.Write([]byte("debug")) + + So(n, ShouldEqual, 5) // n is how much of input consumed + So(err, ShouldBeNil) + So(fake.debug, ShouldEqual, "prefixdebug") + }) + }) +} From cc55ab6bc82b03a527444d53e3d64354111b2676 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Thu, 1 Feb 2018 14:32:19 +0100 Subject: [PATCH 14/18] dashfolders: adds permission modal to dashboard settings --- .../ManageDashboards/FolderPermissions.tsx | 3 +- .../Permissions/DashboardPermissions.tsx | 31 ++++++-- .../Permissions/Permissions.jest.tsx | 73 ------------------- public/sass/components/_gf-form.scss | 2 +- 4 files changed, 28 insertions(+), 81 deletions(-) delete mode 100644 public/app/core/components/Permissions/Permissions.jest.tsx diff --git a/public/app/containers/ManageDashboards/FolderPermissions.tsx b/public/app/containers/ManageDashboards/FolderPermissions.tsx index 7c9e55bcac3..637f811969e 100644 --- a/public/app/containers/ManageDashboards/FolderPermissions.tsx +++ b/public/app/containers/ManageDashboards/FolderPermissions.tsx @@ -53,8 +53,7 @@ export class FolderPermissions extends Component { onClick={this.handleAddPermission} disabled={permissions.isAddPermissionsVisible} > - - Add Permission + Add Permission
diff --git a/public/app/core/components/Permissions/DashboardPermissions.tsx b/public/app/core/components/Permissions/DashboardPermissions.tsx index 2636b0d4db4..a1b86e121bf 100644 --- a/public/app/core/components/Permissions/DashboardPermissions.tsx +++ b/public/app/core/components/Permissions/DashboardPermissions.tsx @@ -1,8 +1,11 @@ import React, { Component } from 'react'; +import { observer } from 'mobx-react'; import { store } from 'app/stores/store'; import Permissions from 'app/core/components/Permissions/Permissions'; import Tooltip from 'app/core/components/Tooltip/Tooltip'; import PermissionsInfo from 'app/core/components/Permissions/PermissionsInfo'; +import AddPermissions from 'app/core/components/Permissions/AddPermissions'; +import SlideDown from 'app/core/components/Animations/SlideDown'; export interface IProps { dashboardId: number; @@ -11,26 +14,44 @@ export interface IProps { folderSlug: string; backendSrv: any; } - +@observer class DashboardPermissions extends Component { permissions: any; constructor(props) { super(props); + this.handleAddPermission = this.handleAddPermission.bind(this); this.permissions = store.permissions; } + handleAddPermission() { + this.permissions.toggleAddPermissions(); + } + render() { const { dashboardId, folderTitle, folderSlug, folderId, backendSrv } = this.props; return (
-

Permissions

- - - +
+

Permissions

+ + + +
+ +
+ + + { -// let wrapper; - -// beforeAll(() => { -// backendSrv.get.mockReturnValue( -// Promise.resolve([ -// { id: 2, dashboardId: 1, role: 'Viewer', permission: 1, permissionName: 'View' }, -// { id: 3, dashboardId: 1, role: 'Editor', permission: 1, permissionName: 'Edit' }, -// { -// id: 4, -// dashboardId: 1, -// userId: 2, -// userLogin: 'danlimerick', -// userEmail: 'dan.limerick@gmail.com', -// permission: 4, -// permissionName: 'Admin', -// }, -// ]) -// ); - -// backendSrv.post = jest.fn(); - -// const store = RootStore.create( -// {}, -// { -// backendSrv: backendSrv, -// } -// ); - -// wrapper = shallow(); -// return wrapper.instance().loadStore(1, true); -// }); - -// describe('when permission for a user is added', () => { -// it('should save permission to db', () => { -// const userItem = { -// id: 2, -// login: 'user2', -// }; - -// wrapper -// .instance() -// .userPicked(userItem) -// .then(() => { -// expect(backendSrv.post.mock.calls.length).toBe(1); -// expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/acl'); -// }); -// }); -// }); - -// describe('when permission for team is added', () => { -// it('should save permission to db', () => { -// const teamItem = { -// id: 2, -// name: 'ug1', -// }; - -// wrapper -// .instance() -// .teamPicked(teamItem) -// .then(() => { -// expect(backendSrv.post.mock.calls.length).toBe(1); -// expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/acl'); -// }); -// }); -// }); -// }); diff --git a/public/sass/components/_gf-form.scss b/public/sass/components/_gf-form.scss index a1e208ee1c2..6603cfa072b 100644 --- a/public/sass/components/_gf-form.scss +++ b/public/sass/components/_gf-form.scss @@ -400,7 +400,7 @@ select.gf-form-input ~ .gf-form-help-icon { .cta-form { position: relative; padding: 1rem; - background-color: $dark-4; + background-color: $empty-list-cta-bg; margin-bottom: 1rem; border-top: 3px solid $green; } From a77c6560331451a6e2a66ae902c55e00165d925c Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Thu, 1 Feb 2018 14:48:11 +0100 Subject: [PATCH 15/18] dashfolders: adds test for permission store --- .../PermissionsStore/PermissionsStore.jest.ts | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/public/app/stores/PermissionsStore/PermissionsStore.jest.ts b/public/app/stores/PermissionsStore/PermissionsStore.jest.ts index dcbb1e2a8b5..97a9906d0e5 100644 --- a/public/app/stores/PermissionsStore/PermissionsStore.jest.ts +++ b/public/app/stores/PermissionsStore/PermissionsStore.jest.ts @@ -57,30 +57,30 @@ describe('PermissionsStore', () => { expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/acl'); }); - // describe('when duplicate team permissions are added', () => { - // beforeEach(() => { - // const newItem = { - // teamId: 10, - // team: 'tester-team', - // permission: 1, - // dashboardId: 1, - // }; - // store.resetNewType(); - // store.newItem.setTeam(newItem.teamId, newItem.team); - // store.newItem.setPermission(newItem.permission); - // store.addStoreItem(); + describe('when duplicate team permissions are added', () => { + beforeEach(() => { + const newItem = { + teamId: 10, + team: 'tester-team', + permission: 1, + dashboardId: 1, + }; + store.resetNewType(); + store.newItem.setTeam(newItem.teamId, newItem.team); + store.newItem.setPermission(newItem.permission); + store.addStoreItem(); - // store.newItem.setTeam(newItem.teamId, newItem.team); - // store.newItem.setPermission(newItem.permission); - // store.addStoreItem(); - // }); + store.newItem.setTeam(newItem.teamId, newItem.team); + store.newItem.setPermission(newItem.permission); + store.addStoreItem(); + }); - // it('should return a validation error', () => { - // expect(store.items.length).toBe(4); - // expect(store.error).toBe('This permission exists already.'); - // expect(backendSrv.post.mock.calls.length).toBe(1); - // }); - // }); + it('should return a validation error', () => { + expect(store.items.length).toBe(4); + expect(store.error).toBe('This permission exists already.'); + expect(backendSrv.post.mock.calls.length).toBe(1); + }); + }); describe('when duplicate user permissions are added', () => { beforeEach(() => { From 734a2e59aab3544a2af91d53f8c9d50d901811cf Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 1 Feb 2018 15:23:00 +0100 Subject: [PATCH 16/18] add gofmt as precommit hook --- package.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/package.json b/package.json index d7fb5d8e6f8..acb992a0936 100644 --- a/package.json +++ b/package.json @@ -115,6 +115,10 @@ "*.scss": [ "prettier --write", "git add" + ], + "*.go": [ + "gofmt -w -s", + "git add" ] }, "prettier": { From c3181552f809d18b62fd6277a787037d932d79a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 1 Feb 2018 15:45:15 +0100 Subject: [PATCH 17/18] ux: added max width to dashboard settings views --- public/sass/components/_dashboard_settings.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/public/sass/components/_dashboard_settings.scss b/public/sass/components/_dashboard_settings.scss index 8f2fd2e0fbb..11d943eb13c 100644 --- a/public/sass/components/_dashboard_settings.scss +++ b/public/sass/components/_dashboard_settings.scss @@ -23,6 +23,7 @@ min-width: 0; height: 100%; padding: 30px; + max-width: 1100px; } .dashboard-settings__aside { From 744f402a964f272c0e560e046a0f3aaa94bb9883 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 1 Feb 2018 17:27:29 +0100 Subject: [PATCH 18/18] db: fix failing integration tests for mysql and postgresql --- pkg/services/sqlstore/dashboard.go | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index 81dab375188..356cd1ad6c9 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -316,16 +316,19 @@ func GetFoldersForSignedInUser(query *m.GetFoldersForSignedInUserQuery) error { params = append(params, query.SignedInUser.UserId) params = append(params, query.OrgId) - sql += `WHERE + sql += ` WHERE d.org_id = ? AND - d.is_folder = 1 AND + d.is_folder = ? AND ( - (d.has_acl = 1 AND da.permission > 1 AND (da.user_id = ? OR ugm.user_id = ? OR ou.id IS NOT NULL)) - OR (d.has_acl = 0 AND ouRole.id IS NOT NULL) + (d.has_acl = ? AND da.permission > 1 AND (da.user_id = ? OR ugm.user_id = ? OR ou.id IS NOT NULL)) + OR (d.has_acl = ? AND ouRole.id IS NOT NULL) )` params = append(params, query.OrgId) + params = append(params, dialect.BooleanStr(true)) + params = append(params, dialect.BooleanStr(true)) params = append(params, query.SignedInUser.UserId) params = append(params, query.SignedInUser.UserId) + params = append(params, dialect.BooleanStr(false)) if len(query.Title) > 0 { sql += " AND d.title " + dialect.LikeStr() + " ?" @@ -333,7 +336,6 @@ func GetFoldersForSignedInUser(query *m.GetFoldersForSignedInUserQuery) error { } sql += ` ORDER BY d.title ASC` - err = x.Sql(sql, params...).Find(&query.Result) } @@ -430,9 +432,9 @@ func GetDashboardPermissionsForUser(query *m.GetDashboardPermissionsForUserQuery params = append(params, query.OrgId) sql += ` - LEFT JOIN (SELECT 1 AS permission, 'Viewer' AS 'role' - UNION SELECT 2 AS permission, 'Editor' AS 'role' - UNION SELECT 4 AS permission, 'Admin' AS 'role') pt ON ouRole.role = pt.role + LEFT JOIN (SELECT 1 AS permission, 'Viewer' AS role + UNION SELECT 2 AS permission, 'Editor' AS role + UNION SELECT 4 AS permission, 'Admin' AS role) pt ON ouRole.role = pt.role WHERE d.Id IN (?` + strings.Repeat(",?", len(query.DashboardIds)-1) + `) ` for _, id := range query.DashboardIds { @@ -447,13 +449,15 @@ func GetDashboardPermissionsForUser(query *m.GetDashboardPermissionsForUserQuery ) group by d.id order by d.id asc` - params = append(params, dialect.BooleanStr(true)) params = append(params, query.OrgId) + params = append(params, dialect.BooleanStr(true)) params = append(params, query.UserId) params = append(params, query.UserId) params = append(params, dialect.BooleanStr(false)) + x.ShowSQL(true) err := x.Sql(sql, params...).Find(&query.Result) + x.ShowSQL(false) for _, p := range query.Result { p.PermissionName = p.Permission.String()