Bläddra i källkod

Merge branch 'data-source-instance-to-react' into data-source-settings-to-react

Peter Holmberg 7 år sedan
förälder
incheckning
c5946ebd27
33 ändrade filer med 1924 tillägg och 34 borttagningar
  1. 1 1
      package.json
  2. 3 3
      pkg/api/api.go
  3. 19 1
      pkg/api/dataproxy.go
  4. 24 8
      pkg/api/datasources.go
  5. 14 1
      pkg/api/frontendsettings.go
  6. 35 2
      pkg/models/datasource.go
  7. 1 0
      pkg/services/sqlstore/datasource.go
  8. 9 2
      pkg/services/sqlstore/sqlstore.go
  9. 4 0
      public/app/core/components/PermissionList/AddPermission.tsx
  10. 0 4
      public/app/core/components/Picker/DescriptionPicker.tsx
  11. 29 0
      public/app/features/datasources/AddDataSourcePermissions.test.tsx
  12. 123 0
      public/app/features/datasources/AddDataSourcePermissions.tsx
  13. 77 0
      public/app/features/datasources/DataSourcePermissions.test.tsx
  14. 155 0
      public/app/features/datasources/DataSourcePermissions.tsx
  15. 32 0
      public/app/features/datasources/DataSourcePermissionsList.test.tsx
  16. 109 0
      public/app/features/datasources/DataSourcePermissionsList.tsx
  17. 125 0
      public/app/features/datasources/DataSourceSettings.tsx
  18. 84 0
      public/app/features/datasources/EditDataSourcePage.tsx
  19. 0 3
      public/app/features/datasources/NewDataSourcePage.tsx
  20. 30 1
      public/app/features/datasources/__mocks__/dataSourcesMocks.ts
  21. 177 0
      public/app/features/datasources/__snapshots__/AddDataSourcePermissions.test.tsx.snap
  22. 92 0
      public/app/features/datasources/__snapshots__/DataSourcePermissions.test.tsx.snap
  23. 327 0
      public/app/features/datasources/__snapshots__/DataSourcePermissionsList.test.tsx.snap
  24. 89 3
      public/app/features/datasources/state/actions.ts
  25. 109 0
      public/app/features/datasources/state/navModel.ts
  26. 13 1
      public/app/features/datasources/state/reducers.ts
  27. 9 0
      public/app/features/datasources/state/selectors.ts
  28. 11 0
      public/app/features/plugins/state/navModel.ts
  29. 7 0
      public/app/routes/routes.ts
  30. 9 0
      public/app/types/acl.ts
  31. 28 3
      public/app/types/datasources.ts
  32. 3 1
      public/app/types/index.ts
  33. 176 0
      yarn.lock

+ 1 - 1
package.json

@@ -102,7 +102,7 @@
     "build": "grunt build",
     "test": "grunt test",
     "lint": "tslint -c tslint.json --project tsconfig.json",
-    "jest": "jest --notify --watch",
+    "jest": "jest --config jest.config.json --notify --watch",
     "api-tests": "jest --notify --watch --config=tests/api/jest.js",
     "precommit": "lint-staged && grunt precommit"
   },

+ 3 - 3
pkg/api/api.go

@@ -234,13 +234,13 @@ func (hs *HTTPServer) registerRoutes() {
 			datasourceRoute.Get("/", Wrap(GetDataSources))
 			datasourceRoute.Post("/", quota("data_source"), bind(m.AddDataSourceCommand{}), Wrap(AddDataSource))
 			datasourceRoute.Put("/:id", bind(m.UpdateDataSourceCommand{}), Wrap(UpdateDataSource))
-			datasourceRoute.Delete("/:id", Wrap(DeleteDataSourceByID))
+			datasourceRoute.Delete("/:id", Wrap(DeleteDataSourceById))
 			datasourceRoute.Delete("/name/:name", Wrap(DeleteDataSourceByName))
-			datasourceRoute.Get("/:id", Wrap(GetDataSourceByID))
+			datasourceRoute.Get("/:id", Wrap(GetDataSourceById))
 			datasourceRoute.Get("/name/:name", Wrap(GetDataSourceByName))
 		}, reqOrgAdmin)
 
-		apiRoute.Get("/datasources/id/:name", Wrap(GetDataSourceIDByName), reqSignedIn)
+		apiRoute.Get("/datasources/id/:name", Wrap(GetDataSourceIdByName), reqSignedIn)
 
 		apiRoute.Get("/plugins", Wrap(GetPluginList))
 		apiRoute.Get("/plugins/:pluginId/settings", Wrap(GetPluginSettingByID))

+ 19 - 1
pkg/api/dataproxy.go

@@ -2,6 +2,7 @@ package api
 
 import (
 	"fmt"
+	"github.com/pkg/errors"
 	"time"
 
 	"github.com/grafana/grafana/pkg/api/pluginproxy"
@@ -14,6 +15,20 @@ import (
 const HeaderNameNoBackendCache = "X-Grafana-NoCache"
 
 func (hs *HTTPServer) getDatasourceFromCache(id int64, c *m.ReqContext) (*m.DataSource, error) {
+	userPermissionsQuery := m.GetDataSourcePermissionsForUserQuery{
+		User: c.SignedInUser,
+	}
+	if err := bus.Dispatch(&userPermissionsQuery); err != nil {
+		if err != bus.ErrHandlerNotFound {
+			return nil, err
+		}
+	} else {
+		permissionType, exists := userPermissionsQuery.Result[id]
+		if exists && permissionType != m.DsPermissionQuery {
+			return nil, errors.New("User not allowed to access datasource")
+		}
+	}
+
 	nocache := c.Req.Header.Get(HeaderNameNoBackendCache) == "true"
 	cacheKey := fmt.Sprintf("ds-%d", id)
 
@@ -38,7 +53,10 @@ func (hs *HTTPServer) getDatasourceFromCache(id int64, c *m.ReqContext) (*m.Data
 func (hs *HTTPServer) ProxyDataSourceRequest(c *m.ReqContext) {
 	c.TimeRequest(metrics.M_DataSource_ProxyReq_Timer)
 
-	ds, err := hs.getDatasourceFromCache(c.ParamsInt64(":id"), c)
+	dsId := c.ParamsInt64(":id")
+	ds, err := hs.getDatasourceFromCache(dsId, c)
+	hs.log.Debug("We are in the ds proxy", "dsId", dsId)
+
 	if err != nil {
 		c.JsonApiErr(500, "Unable to load datasource meta data", err)
 		return

+ 24 - 8
pkg/api/datasources.go

@@ -17,11 +17,27 @@ func GetDataSources(c *m.ReqContext) Response {
 		return Error(500, "Failed to query datasources", err)
 	}
 
+	dsFilterQuery := m.DatasourcesPermissionFilterQuery{
+		User:        c.SignedInUser,
+		Datasources: query.Result,
+	}
+
+	datasources := []*m.DataSource{}
+	if err := bus.Dispatch(&dsFilterQuery); err != nil {
+		if err != bus.ErrHandlerNotFound {
+			return Error(500, "Could not get datasources", err)
+		}
+
+		datasources = query.Result
+	} else {
+		datasources = dsFilterQuery.Result
+	}
+
 	result := make(dtos.DataSourceList, 0)
-	for _, ds := range query.Result {
+	for _, ds := range datasources {
 		dsItem := dtos.DataSourceListItemDTO{
-			Id:        ds.Id,
 			OrgId:     ds.OrgId,
+			Id:        ds.Id,
 			Name:      ds.Name,
 			Url:       ds.Url,
 			Type:      ds.Type,
@@ -49,7 +65,7 @@ func GetDataSources(c *m.ReqContext) Response {
 	return JSON(200, &result)
 }
 
-func GetDataSourceByID(c *m.ReqContext) Response {
+func GetDataSourceById(c *m.ReqContext) Response {
 	query := m.GetDataSourceByIdQuery{
 		Id:    c.ParamsInt64(":id"),
 		OrgId: c.OrgId,
@@ -68,14 +84,14 @@ func GetDataSourceByID(c *m.ReqContext) Response {
 	return JSON(200, &dtos)
 }
 
-func DeleteDataSourceByID(c *m.ReqContext) Response {
+func DeleteDataSourceById(c *m.ReqContext) Response {
 	id := c.ParamsInt64(":id")
 
 	if id <= 0 {
 		return Error(400, "Missing valid datasource id", nil)
 	}
 
-	ds, err := getRawDataSourceByID(id, c.OrgId)
+	ds, err := getRawDataSourceById(id, c.OrgId)
 	if err != nil {
 		return Error(400, "Failed to delete datasource", nil)
 	}
@@ -186,7 +202,7 @@ func fillWithSecureJSONData(cmd *m.UpdateDataSourceCommand) error {
 		return nil
 	}
 
-	ds, err := getRawDataSourceByID(cmd.Id, cmd.OrgId)
+	ds, err := getRawDataSourceById(cmd.Id, cmd.OrgId)
 	if err != nil {
 		return err
 	}
@@ -206,7 +222,7 @@ func fillWithSecureJSONData(cmd *m.UpdateDataSourceCommand) error {
 	return nil
 }
 
-func getRawDataSourceByID(id int64, orgID int64) (*m.DataSource, error) {
+func getRawDataSourceById(id int64, orgID int64) (*m.DataSource, error) {
 	query := m.GetDataSourceByIdQuery{
 		Id:    id,
 		OrgId: orgID,
@@ -236,7 +252,7 @@ func GetDataSourceByName(c *m.ReqContext) Response {
 }
 
 // Get /api/datasources/id/:name
-func GetDataSourceIDByName(c *m.ReqContext) Response {
+func GetDataSourceIdByName(c *m.ReqContext) Response {
 	query := m.GetDataSourceByNameQuery{Name: c.Params(":name"), OrgId: c.OrgId}
 
 	if err := bus.Dispatch(&query); err != nil {

+ 14 - 1
pkg/api/frontendsettings.go

@@ -22,7 +22,20 @@ func getFrontendSettingsMap(c *m.ReqContext) (map[string]interface{}, error) {
 			return nil, err
 		}
 
-		orgDataSources = query.Result
+		dsFilterQuery := m.DatasourcesPermissionFilterQuery{
+			User:        c.SignedInUser,
+			Datasources: query.Result,
+		}
+
+		if err := bus.Dispatch(&dsFilterQuery); err != nil {
+			if err != bus.ErrHandlerNotFound {
+				return nil, err
+			}
+
+			orgDataSources = query.Result
+		} else {
+			orgDataSources = dsFilterQuery.Result
+		}
 	}
 
 	datasources := make(map[string]interface{})

+ 35 - 2
pkg/models/datasource.go

@@ -30,6 +30,7 @@ var (
 	ErrDataSourceNameExists         = errors.New("Data source with same name already exists")
 	ErrDataSourceUpdatingOldVersion = errors.New("Trying to update old version of datasource")
 	ErrDatasourceIsReadOnly         = errors.New("Data source is readonly. Can only be updated from configuration.")
+	ErrDataSourceAccessDenied       = errors.New("Data source access denied")
 )
 
 type DsAccess string
@@ -167,6 +168,7 @@ type DeleteDataSourceByNameCommand struct {
 
 type GetDataSourcesQuery struct {
 	OrgId  int64
+	User   *SignedInUser
 	Result []*DataSource
 }
 
@@ -187,6 +189,37 @@ type GetDataSourceByNameQuery struct {
 }
 
 // ---------------------
-// EVENTS
-type DataSourceCreatedEvent struct {
+//  Permissions
+// ---------------------
+
+type DsPermissionType int
+
+const (
+	DsPermissionQuery DsPermissionType = 1 << iota
+	DsPermissionNoAccess
+)
+
+func (p DsPermissionType) String() string {
+	names := map[int]string{
+		int(DsPermissionQuery):    "Query",
+		int(DsPermissionNoAccess): "No Access",
+	}
+	return names[int(p)]
+}
+
+type HasRequiredDataSourcePermissionQuery struct {
+	Id                 int64
+	User               *SignedInUser
+	RequiredPermission DsPermissionType
+}
+
+type GetDataSourcePermissionsForUserQuery struct {
+	User   *SignedInUser
+	Result map[int64]DsPermissionType
+}
+
+type DatasourcesPermissionFilterQuery struct {
+	User        *SignedInUser
+	Datasources []*DataSource
+	Result      []*DataSource
 }

+ 1 - 0
pkg/services/sqlstore/datasource.go

@@ -27,6 +27,7 @@ func GetDataSourceById(query *m.GetDataSourceByIdQuery) error {
 
 	datasource := m.DataSource{OrgId: query.OrgId, Id: query.Id}
 	has, err := x.Get(&datasource)
+
 	if err != nil {
 		return err
 	}

+ 9 - 2
pkg/services/sqlstore/sqlstore.go

@@ -53,6 +53,7 @@ type SqlStore struct {
 	dbCfg           DatabaseConfig
 	engine          *xorm.Engine
 	log             log.Logger
+	Dialect         migrator.Dialect
 	skipEnsureAdmin bool
 }
 
@@ -125,10 +126,12 @@ func (ss *SqlStore) Init() error {
 	}
 
 	ss.engine = engine
+	ss.Dialect = migrator.NewDialect(ss.engine)
 
 	// temporarily still set global var
 	x = engine
-	dialect = migrator.NewDialect(x)
+	dialect = ss.Dialect
+
 	migrator := migrator.NewMigrator(x)
 	migrations.AddMigrations(migrator)
 
@@ -347,7 +350,11 @@ func InitTestDB(t *testing.T) *SqlStore {
 		t.Fatalf("Failed to init test database: %v", err)
 	}
 
-	dialect = migrator.NewDialect(engine)
+	sqlstore.Dialect = migrator.NewDialect(engine)
+
+	// temp global var until we get rid of global vars
+	dialect = sqlstore.Dialect
+
 	if err := dialect.CleanDB(); err != nil {
 		t.Fatalf("Failed to clean test db %v", err)
 	}

+ 4 - 0
public/app/core/components/PermissionList/AddPermission.tsx

@@ -18,6 +18,10 @@ export interface Props {
 }
 
 class AddPermissions extends Component<Props, NewDashboardAclItem> {
+  static defaultProps = {
+    showPermissionLevels: true,
+  };
+
   constructor(props) {
     super(props);
     this.state = this.getCleanState();

+ 0 - 4
public/app/core/components/Picker/DescriptionPicker.tsx

@@ -22,10 +22,6 @@ export interface Props {
 const getSelectedOption = (optionsWithDesc, value) => optionsWithDesc.find(option => option.value === value);
 
 class DescriptionPicker extends Component<Props, any> {
-  constructor(props) {
-    super(props);
-  }
-
   render() {
     const { optionsWithDesc, onSelected, disabled, className, value } = this.props;
     const selectedOption = getSelectedOption(optionsWithDesc, value);

+ 29 - 0
public/app/features/datasources/AddDataSourcePermissions.test.tsx

@@ -0,0 +1,29 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import { AddDataSourcePermissions, Props } from './AddDataSourcePermissions';
+import { AclTarget } from '../../types/acl';
+
+const setup = () => {
+  const props: Props = {
+    onAddPermission: jest.fn(),
+    onCancel: jest.fn(),
+  };
+
+  return shallow(<AddDataSourcePermissions {...props} />);
+};
+
+describe('Render', () => {
+  it('should render component', () => {
+    const wrapper = setup();
+
+    expect(wrapper).toMatchSnapshot();
+  });
+
+  it('should render user picker', () => {
+    const wrapper = setup();
+
+    wrapper.instance().setState({ type: AclTarget.User });
+
+    expect(wrapper).toMatchSnapshot();
+  });
+});

+ 123 - 0
public/app/features/datasources/AddDataSourcePermissions.tsx

@@ -0,0 +1,123 @@
+import React, { PureComponent } from 'react';
+import { UserPicker } from 'app/core/components/Picker/UserPicker';
+import { Team, TeamPicker } from 'app/core/components/Picker/TeamPicker';
+import DescriptionPicker, { OptionWithDescription } from 'app/core/components/Picker/DescriptionPicker';
+import { dataSourceAclLevels, AclTarget, DataSourcePermissionLevel } from 'app/types/acl';
+import { User } from 'app/types';
+
+export interface Props {
+  onAddPermission: (state) => void;
+  onCancel: () => void;
+}
+
+interface State {
+  userId: number;
+  teamId: number;
+  type: AclTarget;
+  permission: DataSourcePermissionLevel;
+}
+
+export class AddDataSourcePermissions extends PureComponent<Props, State> {
+  cleanState = () => ({
+    userId: 0,
+    teamId: 0,
+    type: AclTarget.Team,
+    permission: DataSourcePermissionLevel.Query,
+  });
+
+  state = this.cleanState();
+
+  isValid() {
+    switch (this.state.type) {
+      case AclTarget.Team:
+        return this.state.teamId > 0;
+      case AclTarget.User:
+        return this.state.userId > 0;
+    }
+    return true;
+  }
+
+  onTeamSelected = (team: Team) => {
+    this.setState({ teamId: team ? team.id : 0 });
+  };
+
+  onUserSelected = (user: User) => {
+    this.setState({ userId: user ? user.id : 0 });
+  };
+
+  onPermissionChanged = (permission: OptionWithDescription) => {
+    this.setState({ permission: permission.value });
+  };
+
+  onTypeChanged = event => {
+    const type = event.target.value as AclTarget;
+
+    this.setState({ type: type, userId: 0, teamId: 0 });
+  };
+
+  onSubmit = async event => {
+    event.preventDefault();
+
+    await this.props.onAddPermission(this.state);
+    this.setState(this.cleanState());
+  };
+
+  render() {
+    const { onCancel } = this.props;
+    const { type, permission } = this.state;
+
+    const pickerClassName = 'width-20';
+    const aclTargets = [{ value: AclTarget.Team, text: 'Team' }, { value: AclTarget.User, text: 'User' }];
+
+    return (
+      <div className="gf-form-inline cta-form">
+        <button className="cta-form__close btn btn-transparent" onClick={onCancel}>
+          <i className="fa fa-close" />
+        </button>
+        <form name="addPermission" onSubmit={this.onSubmit}>
+          <h5>Add Permission For</h5>
+          <div className="gf-form-inline">
+            <div className="gf-form">
+              <select className="gf-form-input gf-size-auto" value={type} onChange={this.onTypeChanged}>
+                {aclTargets.map((option, idx) => {
+                  return (
+                    <option key={idx} value={option.value}>
+                      {option.text}
+                    </option>
+                  );
+                })}
+              </select>
+            </div>
+            {type === AclTarget.User && (
+              <div className="gf-form">
+                <UserPicker onSelected={this.onUserSelected} className={pickerClassName} />
+              </div>
+            )}
+
+            {type === AclTarget.Team && (
+              <div className="gf-form">
+                <TeamPicker onSelected={this.onTeamSelected} className={pickerClassName} />
+              </div>
+            )}
+            <div className="gf-form">
+              <DescriptionPicker
+                optionsWithDesc={dataSourceAclLevels}
+                onSelected={this.onPermissionChanged}
+                value={permission}
+                disabled={false}
+                className={'gf-form-input--form-dropdown-right'}
+              />
+            </div>
+            <div className="gf-form">
+              <button data-save-permission className="btn btn-success" type="submit" disabled={!this.isValid()}>
+                Save
+              </button>
+            </div>
+          </div>
+        </form>
+      </div>
+    );
+  }
+}
+
+export default AddDataSourcePermissions;

+ 77 - 0
public/app/features/datasources/DataSourcePermissions.test.tsx

@@ -0,0 +1,77 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import { DataSourcePermissions, Props } from './DataSourcePermissions';
+import { DataSourcePermission, DataSourcePermissionDTO } from 'app/types';
+import { AclTarget, dashboardPermissionLevels } from '../../types/acl';
+
+const setup = (propOverrides?: object) => {
+  const props: Props = {
+    dataSourcePermission: {} as DataSourcePermissionDTO,
+    pageId: 1,
+    addDataSourcePermission: jest.fn(),
+    enableDataSourcePermissions: jest.fn(),
+    disableDataSourcePermissions: jest.fn(),
+    loadDataSourcePermissions: jest.fn(),
+    removeDataSourcePermission: jest.fn(),
+  };
+
+  Object.assign(props, propOverrides);
+
+  const wrapper = shallow(<DataSourcePermissions {...props} />);
+  const instance = wrapper.instance() as DataSourcePermissions;
+
+  return {
+    wrapper,
+    instance,
+  };
+};
+
+describe('Render', () => {
+  it('should render component', () => {
+    const { wrapper } = setup();
+
+    expect(wrapper).toMatchSnapshot();
+  });
+
+  it('should render permissions enabled', () => {
+    const { wrapper } = setup({
+      dataSourcePermission: {
+        enabled: true,
+        datasourceId: 1,
+        permissions: [] as DataSourcePermission[],
+      },
+    });
+
+    expect(wrapper).toMatchSnapshot();
+  });
+});
+
+describe('Functions', () => {
+  describe('on add permissions', () => {
+    const { instance } = setup();
+
+    it('should add permissions for team', () => {
+      const mockState = {
+        permission: dashboardPermissionLevels[0].value,
+        teamId: 1,
+        type: AclTarget.Team,
+      };
+
+      instance.onAddPermission(mockState);
+
+      expect(instance.props.addDataSourcePermission).toHaveBeenCalledWith(1, { teamId: 1, permission: 1 });
+    });
+
+    it('should add permissions for user', () => {
+      const mockState = {
+        permission: dashboardPermissionLevels[0].value,
+        userId: 1,
+        type: AclTarget.User,
+      };
+
+      instance.onAddPermission(mockState);
+
+      expect(instance.props.addDataSourcePermission).toHaveBeenCalledWith(1, { userId: 1, permission: 1 });
+    });
+  });
+});

+ 155 - 0
public/app/features/datasources/DataSourcePermissions.tsx

@@ -0,0 +1,155 @@
+import React, { PureComponent } from 'react';
+import { connect } from 'react-redux';
+import SlideDown from '../../core/components/Animations/SlideDown';
+import AddDataSourcePermissions from './AddDataSourcePermissions';
+import DataSourcePermissionsList from './DataSourcePermissionsList';
+import { AclTarget } from 'app/types/acl';
+import {
+  addDataSourcePermission,
+  disableDataSourcePermissions,
+  enableDataSourcePermissions,
+  loadDataSourcePermissions,
+  removeDataSourcePermission,
+} from './state/actions';
+import { DataSourcePermissionDTO } from 'app/types';
+import { getRouteParamsId } from '../../core/selectors/location';
+
+export interface Props {
+  dataSourcePermission: DataSourcePermissionDTO;
+  pageId: number;
+  addDataSourcePermission: typeof addDataSourcePermission;
+  enableDataSourcePermissions: typeof enableDataSourcePermissions;
+  disableDataSourcePermissions: typeof disableDataSourcePermissions;
+  loadDataSourcePermissions: typeof loadDataSourcePermissions;
+  removeDataSourcePermission: typeof removeDataSourcePermission;
+}
+
+interface State {
+  isAdding: boolean;
+}
+
+export class DataSourcePermissions extends PureComponent<Props, State> {
+  state = {
+    isAdding: false,
+  };
+
+  componentDidMount() {
+    this.fetchDataSourcePermissions();
+  }
+
+  async fetchDataSourcePermissions() {
+    const { pageId, loadDataSourcePermissions } = this.props;
+
+    return await loadDataSourcePermissions(pageId);
+  }
+
+  onOpenAddPermissions = () => {
+    this.setState({
+      isAdding: true,
+    });
+  };
+
+  onEnablePermissions = () => {
+    const { pageId, enableDataSourcePermissions } = this.props;
+    enableDataSourcePermissions(pageId);
+  };
+
+  onDisablePermissions = () => {
+    const { pageId, disableDataSourcePermissions } = this.props;
+
+    disableDataSourcePermissions(pageId);
+  };
+
+  onAddPermission = state => {
+    const { pageId, addDataSourcePermission } = this.props;
+    const data = {
+      permission: state.permission,
+    };
+
+    if (state.type === AclTarget.Team) {
+      addDataSourcePermission(pageId, Object.assign(data, { teamId: state.teamId }));
+    } else if (state.type === AclTarget.User) {
+      addDataSourcePermission(pageId, Object.assign(data, { userId: state.userId }));
+    }
+  };
+
+  onRemovePermission = item => {
+    this.props.removeDataSourcePermission(item.datasourceId, item.id);
+  };
+
+  onCancelAddPermission = () => {
+    this.setState({
+      isAdding: false,
+    });
+  };
+
+  render() {
+    const { dataSourcePermission } = this.props;
+    const { isAdding } = this.state;
+    const isPermissionsEnabled = dataSourcePermission.enabled;
+
+    return (
+      <div>
+        <div className="page-action-bar">
+          <h3 className="page-sub-heading">Permissions</h3>
+          <div className="page-action-bar__spacer" />
+          {isPermissionsEnabled && [
+            <button
+              key="add-permission"
+              className="btn btn-success pull-right"
+              onClick={this.onOpenAddPermissions}
+              disabled={isAdding}
+            >
+              <i className="fa fa-plus" /> Add Permission
+            </button>,
+            <button key="disable-permissions" className="btn btn-danger pull-right" onClick={this.onDisablePermissions}>
+              Disable Permissions
+            </button>,
+          ]}
+        </div>
+        {!isPermissionsEnabled ? (
+          <div className="empty-list-cta">
+            <div className="empty-list-cta__title">{'Permissions not enabled for this data source.'}</div>
+            <button onClick={this.onEnablePermissions} className="empty-list-cta__button btn btn-xlarge btn-success">
+              {'Enable'}
+            </button>
+            <div className="empty-list-cta__pro-tip">
+              <i className="fa fa-rocket" /> ProTip:{' '}
+              {'Only admins will be able to query the data source after you enable permissions.'}
+            </div>
+          </div>
+        ) : (
+          <div>
+            <SlideDown in={isAdding}>
+              <AddDataSourcePermissions
+                onAddPermission={state => this.onAddPermission(state)}
+                onCancel={this.onCancelAddPermission}
+              />
+            </SlideDown>
+            <DataSourcePermissionsList
+              items={dataSourcePermission.permissions}
+              onRemoveItem={item => this.onRemovePermission(item)}
+            />
+          </div>
+        )}
+      </div>
+    );
+  }
+}
+
+function mapStateToProps(state) {
+  return {
+    pageId: getRouteParamsId(state.location),
+    dataSourcePermission: state.dataSources.dataSourcePermission,
+  };
+}
+
+const mapDispatchToProps = {
+  addDataSourcePermission,
+  enableDataSourcePermissions,
+  disableDataSourcePermissions,
+  loadDataSourcePermissions,
+  removeDataSourcePermission,
+};
+
+export default connect(mapStateToProps, mapDispatchToProps)(DataSourcePermissions);

+ 32 - 0
public/app/features/datasources/DataSourcePermissionsList.test.tsx

@@ -0,0 +1,32 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import { DataSourcePermissionsList, Props } from './DataSourcePermissionsList';
+import { DataSourcePermission } from '../../types';
+import { getMockDataSourcePermissionsTeam, getMockDataSourcePermissionsUser } from './__mocks__/dataSourcesMocks';
+
+const setup = (propOverrides?: object) => {
+  const props: Props = {
+    items: [] as DataSourcePermission[],
+    onRemoveItem: jest.fn(),
+  };
+
+  Object.assign(props, propOverrides);
+
+  return shallow(<DataSourcePermissionsList {...props} />);
+};
+
+describe('Render', () => {
+  it('should render component', () => {
+    const wrapper = setup();
+
+    expect(wrapper).toMatchSnapshot();
+  });
+
+  it('should render items', () => {
+    const wrapper = setup({
+      items: [getMockDataSourcePermissionsUser(), getMockDataSourcePermissionsTeam()],
+    });
+
+    expect(wrapper).toMatchSnapshot();
+  });
+});

+ 109 - 0
public/app/features/datasources/DataSourcePermissionsList.tsx

@@ -0,0 +1,109 @@
+import React, { PureComponent } from 'react';
+import { DataSourcePermission } from 'app/types';
+import { dataSourceAclLevels, DataSourcePermissionLevel } from 'app/types/acl';
+import DescriptionPicker from '../../core/components/Picker/DescriptionPicker';
+
+export interface Props {
+  items: DataSourcePermission[];
+  onRemoveItem: (item) => void;
+}
+
+export class DataSourcePermissionsList extends PureComponent<Props> {
+  renderAvatar(item) {
+    if (item.teamId) {
+      return <img className="filter-table__avatar" src={item.teamAvatarUrl} />;
+    } else if (item.userId) {
+      return <img className="filter-table__avatar" src={item.userAvatarUrl} />;
+    }
+
+    return <i style={{ width: '25px', height: '25px' }} className="gicon gicon-viewer" />;
+  }
+
+  renderDescription(item) {
+    if (item.userId) {
+      return [
+        <span key="name">{item.userLogin} </span>,
+        <span key="description" className="filter-table__weak-italic">
+          (User)
+        </span>,
+      ];
+    }
+    if (item.teamId) {
+      return [
+        <span key="name">{item.team} </span>,
+        <span key="description" className="filter-table__weak-italic">
+          (Team)
+        </span>,
+      ];
+    }
+    return <span className="filter-table__weak-italic">(Role)</span>;
+  }
+
+  render() {
+    const { items } = this.props;
+    const permissionLevels = [...dataSourceAclLevels];
+    permissionLevels.push({ value: DataSourcePermissionLevel.Admin, label: 'Admin', description: '' });
+
+    return (
+      <table className="filter-table gf-form-group">
+        <tbody>
+          <tr className="gf-form-disabled">
+            <td style={{ width: '1%' }}>
+              <i style={{ width: '25px', height: '25px' }} className="gicon gicon-shield" />
+            </td>
+            <td style={{ width: '90%' }}>
+              Admin
+              <span className="filter-table__weak-italic"> (Role)</span>
+            </td>
+            <td />
+            <td className="query-keyword">Can</td>
+            <td>
+              <div className="gf-form">
+                <DescriptionPicker
+                  optionsWithDesc={permissionLevels}
+                  onSelected={() => {}}
+                  value={2}
+                  disabled={true}
+                  className={'gf-form-input--form-dropdown-right'}
+                />
+              </div>
+            </td>
+            <td>
+              <button className="btn btn-inverse btn-small">
+                <i className="fa fa-lock" />
+              </button>
+            </td>
+          </tr>
+          {items.map((item, index) => {
+            return (
+              <tr key={`${item.id}-${index}`}>
+                <td style={{ width: '1%' }}>{this.renderAvatar(item)}</td>
+                <td style={{ width: '90%' }}>{this.renderDescription(item)}</td>
+                <td />
+                <td className="query-keyword">Can</td>
+                <td>
+                  <div className="gf-form">
+                    <DescriptionPicker
+                      optionsWithDesc={permissionLevels}
+                      onSelected={() => {}}
+                      value={1}
+                      disabled={true}
+                      className={'gf-form-input--form-dropdown-right'}
+                    />
+                  </div>
+                </td>
+                <td>
+                  <button className="btn btn-danger btn-small" onClick={() => this.props.onRemoveItem(item)}>
+                    <i className="fa fa-remove" />
+                  </button>
+                </td>
+              </tr>
+            );
+          })}
+        </tbody>
+      </table>
+    );
+  }
+}
+
+export default DataSourcePermissionsList;

+ 125 - 0
public/app/features/datasources/DataSourceSettings.tsx

@@ -0,0 +1,125 @@
+import React, { PureComponent } from 'react';
+import { connect } from 'react-redux';
+import { DataSource, Plugin } from 'app/types';
+
+export interface Props {
+  dataSource: DataSource;
+  dataSourceMeta: Plugin;
+}
+interface State {
+  name: string;
+}
+
+enum DataSourceStates {
+  Alpha = 'alpha',
+  Beta = 'beta',
+}
+
+export class DataSourceSettings extends PureComponent<Props, State> {
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      name: props.dataSource.name,
+    };
+  }
+
+  onNameChange = event => {
+    this.setState({
+      name: event.target.value,
+    });
+  };
+
+  onSubmit = event => {
+    event.preventDefault();
+    console.log(event);
+  };
+
+  onDelete = event => {
+    console.log(event);
+  };
+
+  isReadyOnly() {
+    return this.props.dataSource.readOnly === true;
+  }
+
+  shouldRenderInfoBox() {
+    const { state } = this.props.dataSourceMeta;
+
+    return state === DataSourceStates.Alpha || state === DataSourceStates.Beta;
+  }
+
+  getInfoText() {
+    const { dataSourceMeta } = this.props;
+
+    switch (dataSourceMeta.state) {
+      case DataSourceStates.Alpha:
+        return (
+          'This plugin is marked as being in alpha state, which means it is in early development phase and updates' +
+          ' will include breaking changes.'
+        );
+
+      case DataSourceStates.Beta:
+        return (
+          'This plugin is marked as being in a beta development state. This means it is in currently in active' +
+          ' development and could be missing important features.'
+        );
+    }
+
+    return null;
+  }
+
+  render() {
+    const { name } = this.state;
+
+    return (
+      <div>
+        <h3 className="page-sub-heading">Settings</h3>
+        <form onSubmit={this.onSubmit}>
+          <div className="gf-form-group">
+            <div className="gf-form-inline">
+              <div className="gf-form max-width-30">
+                <span className="gf-form-label width-10">Name</span>
+                <input
+                  className="gf-form-input max-width-23"
+                  type="text"
+                  value={name}
+                  placeholder="name"
+                  onChange={this.onNameChange}
+                  required
+                />
+              </div>
+            </div>
+          </div>
+          {this.shouldRenderInfoBox() && <div className="grafana-info-box">{this.getInfoText()}</div>}
+          {this.isReadyOnly() && (
+            <div className="grafana-info-box span8">
+              This datasource was added by config and cannot be modified using the UI. Please contact your server admin
+              to update this datasource.
+            </div>
+          )}
+          <div className="gf-form-button-row">
+            <button type="submit" className="btn btn-success" disabled={this.isReadyOnly()} onClick={this.onSubmit}>
+              Save &amp; Test
+            </button>
+            <button type="submit" className="btn btn-danger" disabled={this.isReadyOnly()} onClick={this.onDelete}>
+              Delete
+            </button>
+            <a className="btn btn-inverse" href="datasources">
+              Back
+            </a>
+          </div>
+        </form>
+      </div>
+    );
+  }
+}
+
+function mapStateToProps(state) {
+  return {
+    dataSource: state.dataSources.dataSource,
+    dataSourceMeta: state.dataSources.dataSourceMeta,
+  };
+}
+
+export default connect(mapStateToProps)(DataSourceSettings);

+ 84 - 0
public/app/features/datasources/EditDataSourcePage.tsx

@@ -0,0 +1,84 @@
+import React, { PureComponent } from 'react';
+import { hot } from 'react-hot-loader';
+import { connect } from 'react-redux';
+import PageHeader from '../../core/components/PageHeader/PageHeader';
+import DataSourcePermissions from './DataSourcePermissions';
+import { DataSource, NavModel } from 'app/types';
+import { loadDataSource } from './state/actions';
+import { getNavModel } from '../../core/selectors/navModel';
+import { getRouteParamsId, getRouteParamsPage } from '../../core/selectors/location';
+import { getDataSourceLoadingNav } from './state/navModel';
+import { getDataSource } from './state/selectors';
+
+export interface Props {
+  navModel: NavModel;
+  dataSource: DataSource;
+  dataSourceId: number;
+  pageName: string;
+  loadDataSource: typeof loadDataSource;
+}
+
+enum PageTypes {
+  Settings = 'settings',
+  Permissions = 'permissions',
+  Dashboards = 'dashboards',
+}
+
+export class EditDataSourcePage extends PureComponent<Props> {
+  componentDidMount() {
+    this.fetchDataSource();
+  }
+
+  async fetchDataSource() {
+    await this.props.loadDataSource(this.props.dataSourceId);
+  }
+
+  isValidPage(currentPage) {
+    return (Object as any).values(PageTypes).includes(currentPage);
+  }
+
+  getCurrentPage() {
+    const currentPage = this.props.pageName;
+
+    return this.isValidPage(currentPage) ? currentPage : PageTypes.Permissions;
+  }
+
+  renderPage() {
+    switch (this.getCurrentPage()) {
+      case PageTypes.Permissions:
+        return <DataSourcePermissions />;
+    }
+
+    return null;
+  }
+
+  render() {
+    const { navModel } = this.props;
+
+    return (
+      <div>
+        <PageHeader model={navModel} />
+        <div className="page-container page-body">{this.renderPage()}</div>
+      </div>
+    );
+  }
+}
+
+function mapStateToProps(state) {
+  const pageName = getRouteParamsPage(state.location) || PageTypes.Permissions;
+  const dataSourceId = getRouteParamsId(state.location);
+  const dataSourceLoadingNav = getDataSourceLoadingNav(pageName);
+
+  return {
+    navModel: getNavModel(state.navIndex, `datasource-${pageName}-${dataSourceId}`, dataSourceLoadingNav),
+    dataSourceId: dataSourceId,
+    dataSource: getDataSource(state.dataSources, dataSourceId),
+    pageName: pageName,
+  };
+}
+
+const mapDispatchToProps = {
+  loadDataSource,
+};
+
+export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(EditDataSourcePage));

+ 0 - 3
public/app/features/datasources/NewDataSourcePage.tsx

@@ -4,7 +4,6 @@ import { hot } from 'react-hot-loader';
 import PageHeader from 'app/core/components/PageHeader/PageHeader';
 import { NavModel, Plugin } from 'app/types';
 import { addDataSource, loadDataSourceTypes, setDataSourceTypeSearchQuery } from './state/actions';
-import { updateLocation } from '../../core/actions';
 import { getNavModel } from 'app/core/selectors/navModel';
 import { getDataSourceTypes } from './state/selectors';
 
@@ -13,7 +12,6 @@ export interface Props {
   dataSourceTypes: Plugin[];
   addDataSource: typeof addDataSource;
   loadDataSourceTypes: typeof loadDataSourceTypes;
-  updateLocation: typeof updateLocation;
   dataSourceTypeSearchQuery: string;
   setDataSourceTypeSearchQuery: typeof setDataSourceTypeSearchQuery;
 }
@@ -81,7 +79,6 @@ function mapStateToProps(state) {
 const mapDispatchToProps = {
   addDataSource,
   loadDataSourceTypes,
-  updateLocation,
   setDataSourceTypeSearchQuery,
 };
 

+ 30 - 1
public/app/features/datasources/__mocks__/dataSourcesMocks.ts

@@ -1,4 +1,4 @@
-import { DataSource } from 'app/types';
+import { DataSource, DataSourcePermission } from 'app/types';
 
 export const getMockDataSources = (amount: number): DataSource[] => {
   const dataSources = [];
@@ -43,3 +43,32 @@ export const getMockDataSource = (): DataSource => {
     user: '',
   };
 };
+
+export const getMockDataSourcePermissionsUser = (): DataSourcePermission => {
+  return {
+    created: '2018-10-10T16:50:45+02:00',
+    datasourceId: 1,
+    id: 2,
+    permission: 1,
+    permissionName: 'Query',
+    updated: '2018-10-10T16:50:45+02:00',
+    userAvatarUrl: '/avatar/926aa85c6bcefa0b4deca3223f337ae1',
+    userEmail: 'test@test.com',
+    userId: 3,
+    userLogin: 'testUser',
+  };
+};
+
+export const getMockDataSourcePermissionsTeam = (): DataSourcePermission => {
+  return {
+    created: '2018-10-10T16:57:09+02:00',
+    datasourceId: 1,
+    id: 6,
+    permission: 1,
+    permissionName: 'Query',
+    team: 'A-team',
+    teamAvatarUrl: '/avatar/93c0801b955cbd443a8cfa91a401d7bc',
+    teamId: 1,
+    updated: '2018-10-10T16:57:09+02:00',
+  };
+};

+ 177 - 0
public/app/features/datasources/__snapshots__/AddDataSourcePermissions.test.tsx.snap

@@ -0,0 +1,177 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Render should render component 1`] = `
+<div
+  className="gf-form-inline cta-form"
+>
+  <button
+    className="cta-form__close btn btn-transparent"
+    onClick={[MockFunction]}
+  >
+    <i
+      className="fa fa-close"
+    />
+  </button>
+  <form
+    name="addPermission"
+    onSubmit={[Function]}
+  >
+    <h5>
+      Add Permission For
+    </h5>
+    <div
+      className="gf-form-inline"
+    >
+      <div
+        className="gf-form"
+      >
+        <select
+          className="gf-form-input gf-size-auto"
+          onChange={[Function]}
+          value="Team"
+        >
+          <option
+            key="0"
+            value="Team"
+          >
+            Team
+          </option>
+          <option
+            key="1"
+            value="User"
+          >
+            User
+          </option>
+        </select>
+      </div>
+      <div
+        className="gf-form"
+      >
+        <TeamPicker
+          className="width-20"
+          onSelected={[Function]}
+        />
+      </div>
+      <div
+        className="gf-form"
+      >
+        <DescriptionPicker
+          className="gf-form-input--form-dropdown-right"
+          disabled={false}
+          onSelected={[Function]}
+          optionsWithDesc={
+            Array [
+              Object {
+                "description": "Can query data source.",
+                "label": "Query",
+                "value": 1,
+              },
+            ]
+          }
+          value={1}
+        />
+      </div>
+      <div
+        className="gf-form"
+      >
+        <button
+          className="btn btn-success"
+          data-save-permission={true}
+          disabled={true}
+          type="submit"
+        >
+          Save
+        </button>
+      </div>
+    </div>
+  </form>
+</div>
+`;
+
+exports[`Render should render user picker 1`] = `
+<div
+  className="gf-form-inline cta-form"
+>
+  <button
+    className="cta-form__close btn btn-transparent"
+    onClick={[MockFunction]}
+  >
+    <i
+      className="fa fa-close"
+    />
+  </button>
+  <form
+    name="addPermission"
+    onSubmit={[Function]}
+  >
+    <h5>
+      Add Permission For
+    </h5>
+    <div
+      className="gf-form-inline"
+    >
+      <div
+        className="gf-form"
+      >
+        <select
+          className="gf-form-input gf-size-auto"
+          onChange={[Function]}
+          value="User"
+        >
+          <option
+            key="0"
+            value="Team"
+          >
+            Team
+          </option>
+          <option
+            key="1"
+            value="User"
+          >
+            User
+          </option>
+        </select>
+      </div>
+      <div
+        className="gf-form"
+      >
+        <UserPicker
+          className="width-20"
+          onSelected={[Function]}
+        />
+      </div>
+      <div
+        className="gf-form"
+      >
+        <DescriptionPicker
+          className="gf-form-input--form-dropdown-right"
+          disabled={false}
+          onSelected={[Function]}
+          optionsWithDesc={
+            Array [
+              Object {
+                "description": "Can query data source.",
+                "label": "Query",
+                "value": 1,
+              },
+            ]
+          }
+          value={1}
+        />
+      </div>
+      <div
+        className="gf-form"
+      >
+        <button
+          className="btn btn-success"
+          data-save-permission={true}
+          disabled={true}
+          type="submit"
+        >
+          Save
+        </button>
+      </div>
+    </div>
+  </form>
+</div>
+`;

+ 92 - 0
public/app/features/datasources/__snapshots__/DataSourcePermissions.test.tsx.snap

@@ -0,0 +1,92 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Render should render component 1`] = `
+<div>
+  <div
+    className="page-action-bar"
+  >
+    <h3
+      className="page-sub-heading"
+    >
+      Permissions
+    </h3>
+    <div
+      className="page-action-bar__spacer"
+    />
+  </div>
+  <div
+    className="empty-list-cta"
+  >
+    <div
+      className="empty-list-cta__title"
+    >
+      Permissions not enabled for this data source.
+    </div>
+    <button
+      className="empty-list-cta__button btn btn-xlarge btn-success"
+      onClick={[Function]}
+    >
+      Enable
+    </button>
+    <div
+      className="empty-list-cta__pro-tip"
+    >
+      <i
+        className="fa fa-rocket"
+      />
+       ProTip:
+       
+      Only admins will be able to query the data source after you enable permissions.
+    </div>
+  </div>
+</div>
+`;
+
+exports[`Render should render permissions enabled 1`] = `
+<div>
+  <div
+    className="page-action-bar"
+  >
+    <h3
+      className="page-sub-heading"
+    >
+      Permissions
+    </h3>
+    <div
+      className="page-action-bar__spacer"
+    />
+    <button
+      className="btn btn-success pull-right"
+      disabled={false}
+      key="add-permission"
+      onClick={[Function]}
+    >
+      <i
+        className="fa fa-plus"
+      />
+       Add Permission
+    </button>
+    <button
+      className="btn btn-danger pull-right"
+      key="disable-permissions"
+      onClick={[Function]}
+    >
+      Disable Permissions
+    </button>
+  </div>
+  <div>
+    <Component
+      in={false}
+    >
+      <AddDataSourcePermissions
+        onAddPermission={[Function]}
+        onCancel={[Function]}
+      />
+    </Component>
+    <DataSourcePermissionsList
+      items={Array []}
+      onRemoveItem={[Function]}
+    />
+  </div>
+</div>
+`;

+ 327 - 0
public/app/features/datasources/__snapshots__/DataSourcePermissionsList.test.tsx.snap

@@ -0,0 +1,327 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Render should render component 1`] = `
+<table
+  className="filter-table gf-form-group"
+>
+  <tbody>
+    <tr
+      className="gf-form-disabled"
+    >
+      <td
+        style={
+          Object {
+            "width": "1%",
+          }
+        }
+      >
+        <i
+          className="gicon gicon-shield"
+          style={
+            Object {
+              "height": "25px",
+              "width": "25px",
+            }
+          }
+        />
+      </td>
+      <td
+        style={
+          Object {
+            "width": "90%",
+          }
+        }
+      >
+        Admin
+        <span
+          className="filter-table__weak-italic"
+        >
+           (Role)
+        </span>
+      </td>
+      <td />
+      <td
+        className="query-keyword"
+      >
+        Can
+      </td>
+      <td>
+        <div
+          className="gf-form"
+        >
+          <DescriptionPicker
+            className="gf-form-input--form-dropdown-right"
+            disabled={true}
+            onSelected={[Function]}
+            optionsWithDesc={
+              Array [
+                Object {
+                  "description": "Can query data source.",
+                  "label": "Query",
+                  "value": 1,
+                },
+                Object {
+                  "description": "",
+                  "label": "Admin",
+                  "value": 2,
+                },
+              ]
+            }
+            value={2}
+          />
+        </div>
+      </td>
+      <td>
+        <button
+          className="btn btn-inverse btn-small"
+        >
+          <i
+            className="fa fa-lock"
+          />
+        </button>
+      </td>
+    </tr>
+  </tbody>
+</table>
+`;
+
+exports[`Render should render items 1`] = `
+<table
+  className="filter-table gf-form-group"
+>
+  <tbody>
+    <tr
+      className="gf-form-disabled"
+    >
+      <td
+        style={
+          Object {
+            "width": "1%",
+          }
+        }
+      >
+        <i
+          className="gicon gicon-shield"
+          style={
+            Object {
+              "height": "25px",
+              "width": "25px",
+            }
+          }
+        />
+      </td>
+      <td
+        style={
+          Object {
+            "width": "90%",
+          }
+        }
+      >
+        Admin
+        <span
+          className="filter-table__weak-italic"
+        >
+           (Role)
+        </span>
+      </td>
+      <td />
+      <td
+        className="query-keyword"
+      >
+        Can
+      </td>
+      <td>
+        <div
+          className="gf-form"
+        >
+          <DescriptionPicker
+            className="gf-form-input--form-dropdown-right"
+            disabled={true}
+            onSelected={[Function]}
+            optionsWithDesc={
+              Array [
+                Object {
+                  "description": "Can query data source.",
+                  "label": "Query",
+                  "value": 1,
+                },
+                Object {
+                  "description": "",
+                  "label": "Admin",
+                  "value": 2,
+                },
+              ]
+            }
+            value={2}
+          />
+        </div>
+      </td>
+      <td>
+        <button
+          className="btn btn-inverse btn-small"
+        >
+          <i
+            className="fa fa-lock"
+          />
+        </button>
+      </td>
+    </tr>
+    <tr
+      key="2-0"
+    >
+      <td
+        style={
+          Object {
+            "width": "1%",
+          }
+        }
+      >
+        <img
+          className="filter-table__avatar"
+          src="/avatar/926aa85c6bcefa0b4deca3223f337ae1"
+        />
+      </td>
+      <td
+        style={
+          Object {
+            "width": "90%",
+          }
+        }
+      >
+        <span
+          key="name"
+        >
+          testUser
+           
+        </span>
+        <span
+          className="filter-table__weak-italic"
+          key="description"
+        >
+          (User)
+        </span>
+      </td>
+      <td />
+      <td
+        className="query-keyword"
+      >
+        Can
+      </td>
+      <td>
+        <div
+          className="gf-form"
+        >
+          <DescriptionPicker
+            className="gf-form-input--form-dropdown-right"
+            disabled={true}
+            onSelected={[Function]}
+            optionsWithDesc={
+              Array [
+                Object {
+                  "description": "Can query data source.",
+                  "label": "Query",
+                  "value": 1,
+                },
+                Object {
+                  "description": "",
+                  "label": "Admin",
+                  "value": 2,
+                },
+              ]
+            }
+            value={1}
+          />
+        </div>
+      </td>
+      <td>
+        <button
+          className="btn btn-danger btn-small"
+          onClick={[Function]}
+        >
+          <i
+            className="fa fa-remove"
+          />
+        </button>
+      </td>
+    </tr>
+    <tr
+      key="6-1"
+    >
+      <td
+        style={
+          Object {
+            "width": "1%",
+          }
+        }
+      >
+        <img
+          className="filter-table__avatar"
+          src="/avatar/93c0801b955cbd443a8cfa91a401d7bc"
+        />
+      </td>
+      <td
+        style={
+          Object {
+            "width": "90%",
+          }
+        }
+      >
+        <span
+          key="name"
+        >
+          A-team
+           
+        </span>
+        <span
+          className="filter-table__weak-italic"
+          key="description"
+        >
+          (Team)
+        </span>
+      </td>
+      <td />
+      <td
+        className="query-keyword"
+      >
+        Can
+      </td>
+      <td>
+        <div
+          className="gf-form"
+        >
+          <DescriptionPicker
+            className="gf-form-input--form-dropdown-right"
+            disabled={true}
+            onSelected={[Function]}
+            optionsWithDesc={
+              Array [
+                Object {
+                  "description": "Can query data source.",
+                  "label": "Query",
+                  "value": 1,
+                },
+                Object {
+                  "description": "",
+                  "label": "Admin",
+                  "value": 2,
+                },
+              ]
+            }
+            value={1}
+          />
+        </div>
+      </td>
+      <td>
+        <button
+          className="btn btn-danger btn-small"
+          onClick={[Function]}
+        >
+          <i
+            className="fa fa-remove"
+          />
+        </button>
+      </td>
+    </tr>
+  </tbody>
+</table>
+`;

+ 89 - 3
public/app/features/datasources/state/actions.ts

@@ -1,13 +1,17 @@
 import { ThunkAction } from 'redux-thunk';
-import { DataSource, Plugin, StoreState } from 'app/types';
+import { DataSource, DataSourcePermissionDTO, Plugin, StoreState } from 'app/types';
 import { getBackendSrv } from '../../../core/services/backend_srv';
 import { LayoutMode } from '../../../core/components/LayoutSelector/LayoutSelector';
-import { updateLocation } from '../../../core/actions';
+import { updateLocation, updateNavIndex, UpdateNavIndexAction } from '../../../core/actions';
 import { UpdateLocationAction } from '../../../core/actions/location';
+import { buildNavModel } from './navModel';
 
 export enum ActionTypes {
   LoadDataSources = 'LOAD_DATA_SOURCES',
   LoadDataSourceTypes = 'LOAD_DATA_SOURCE_TYPES',
+  LoadDataSource = 'LOAD_DATA_SOURCE',
+  LoadDataSourceMeta = 'LOAD_DATA_SOURCE_META',
+  LoadDataSourcePermissions = 'LOAD_DATA_SOURCE_PERMISSIONS',
   SetDataSourcesSearchQuery = 'SET_DATA_SOURCES_SEARCH_QUERY',
   SetDataSourcesLayoutMode = 'SET_DATA_SOURCES_LAYOUT_MODE',
   SetDataSourceTypeSearchQuery = 'SET_DATA_SOURCE_TYPE_SEARCH_QUERY',
@@ -38,16 +42,48 @@ export interface SetDataSourceTypeSearchQueryAction {
   payload: string;
 }
 
+export interface LoadDataSourceAction {
+  type: ActionTypes.LoadDataSource;
+  payload: DataSource;
+}
+
+export interface LoadDataSourceMetaAction {
+  type: ActionTypes.LoadDataSourceMeta;
+  payload: Plugin;
+}
+
+export interface LoadDataSourcePermissionsAction {
+  type: ActionTypes.LoadDataSourcePermissions;
+  payload: DataSourcePermissionDTO;
+}
+
 const dataSourcesLoaded = (dataSources: DataSource[]): LoadDataSourcesAction => ({
   type: ActionTypes.LoadDataSources,
   payload: dataSources,
 });
 
+const dataSourceLoaded = (dataSource: DataSource): LoadDataSourceAction => ({
+  type: ActionTypes.LoadDataSource,
+  payload: dataSource,
+});
+
+const dataSourceMetaLoaded = (dataSourceMeta: Plugin): LoadDataSourceMetaAction => ({
+  type: ActionTypes.LoadDataSourceMeta,
+  payload: dataSourceMeta,
+});
+
 const dataSourceTypesLoaded = (dataSourceTypes: Plugin[]): LoadDataSourceTypesAction => ({
   type: ActionTypes.LoadDataSourceTypes,
   payload: dataSourceTypes,
 });
 
+const dataSourcePermissionsLoaded = (
+  dataSourcePermission: DataSourcePermissionDTO
+): LoadDataSourcePermissionsAction => ({
+  type: ActionTypes.LoadDataSourcePermissions,
+  payload: dataSourcePermission,
+});
+
 export const setDataSourcesSearchQuery = (searchQuery: string): SetDataSourcesSearchQueryAction => ({
   type: ActionTypes.SetDataSourcesSearchQuery,
   payload: searchQuery,
@@ -69,7 +105,11 @@ export type Action =
   | SetDataSourcesLayoutModeAction
   | UpdateLocationAction
   | LoadDataSourceTypesAction
-  | SetDataSourceTypeSearchQueryAction;
+  | SetDataSourceTypeSearchQueryAction
+  | LoadDataSourceAction
+  | UpdateNavIndexAction
+  | LoadDataSourceMetaAction
+  | LoadDataSourcePermissionsAction;
 
 type ThunkResult<R> = ThunkAction<R, StoreState, undefined, Action>;
 
@@ -80,6 +120,16 @@ export function loadDataSources(): ThunkResult<void> {
   };
 }
 
+export function loadDataSource(id: number): ThunkResult<void> {
+  return async dispatch => {
+    const dataSource = await getBackendSrv().get(`/api/datasources/${id}`);
+    const pluginInfo = await getBackendSrv().get(`/api/plugins/${dataSource.type}/settings`);
+    dispatch(dataSourceLoaded(dataSource));
+    dispatch(dataSourceMetaLoaded(pluginInfo));
+    dispatch(updateNavIndex(buildNavModel(dataSource, pluginInfo)));
+  };
+}
+
 export function addDataSource(plugin: Plugin): ThunkResult<void> {
   return async (dispatch, getStore) => {
     await dispatch(loadDataSources());
@@ -109,6 +159,42 @@ export function loadDataSourceTypes(): ThunkResult<void> {
   };
 }
 
+export function loadDataSourcePermissions(id: number): ThunkResult<void> {
+  return async dispatch => {
+    const response = await getBackendSrv().get(`/api/datasources/${id}/permissions`);
+    dispatch(dataSourcePermissionsLoaded(response));
+  };
+}
+
+export function enableDataSourcePermissions(id: number): ThunkResult<void> {
+  return async dispatch => {
+    await getBackendSrv().post(`/api/datasources/${id}/enable-permissions`, {});
+    dispatch(loadDataSourcePermissions(id));
+  };
+}
+
+export function disableDataSourcePermissions(id: number): ThunkResult<void> {
+  return async dispatch => {
+    await getBackendSrv().post(`/api/datasources/${id}/disable-permissions`, {});
+    dispatch(loadDataSourcePermissions(id));
+  };
+}
+
+export function addDataSourcePermission(id: number, data: object): ThunkResult<void> {
+  return async dispatch => {
+    await getBackendSrv().post(`/api/datasources/${id}/permissions`, data);
+
+    dispatch(loadDataSourcePermissions(id));
+  };
+}
+
+export function removeDataSourcePermission(id: number, permissionId: number): ThunkResult<void> {
+  return async dispatch => {
+    await getBackendSrv().delete(`/api/datasources/${id}/permissions/${permissionId}`);
+    dispatch(loadDataSourcePermissions(id));
+  };
+}
+
 export function nameExits(dataSources, name) {
   return (
     dataSources.filter(dataSource => {

+ 109 - 0
public/app/features/datasources/state/navModel.ts

@@ -0,0 +1,109 @@
+import { DataSource, NavModel, NavModelItem, PluginMeta } from 'app/types';
+import config from 'app/core/config';
+
+export function buildNavModel(dataSource: DataSource, pluginMeta: PluginMeta): NavModelItem {
+  const navModel = {
+    img: pluginMeta.info.logos.large,
+    id: 'datasource-' + dataSource.id,
+    subTitle: `Type: ${pluginMeta.name}`,
+    url: '',
+    text: dataSource.name,
+    breadcrumbs: [{ title: 'Data Sources', url: 'datasources' }],
+    children: [
+      {
+        active: false,
+        icon: 'fa fa-fw fa-sliders',
+        id: `datasource-settings-${dataSource.id}`,
+        text: 'Settings',
+        url: `datasources/edit/${dataSource.id}`,
+      },
+    ],
+  };
+
+  if (pluginMeta.includes && hasDashboards(pluginMeta.includes)) {
+    navModel.children.push({
+      active: false,
+      icon: 'fa fa-fw fa-th-large',
+      id: `datasource-dashboards-${dataSource.id}`,
+      text: 'Dashboards',
+      url: `datasources/edit/${dataSource.id}/dashboards`,
+    });
+  }
+
+  if (config.buildInfo.isEnterprise) {
+    navModel.children.push({
+      active: false,
+      icon: 'fa fa-fw fa-lock',
+      id: `datasource-permissions-${dataSource.id}`,
+      text: 'Permissions',
+      url: `datasources/edit/${dataSource.id}/permissions`,
+    });
+  }
+
+  return navModel;
+}
+
+export function getDataSourceLoadingNav(pageName: string): NavModel {
+  const main = buildNavModel(
+    {
+      access: '',
+      basicAuth: false,
+      database: '',
+      id: 1,
+      isDefault: false,
+      jsonData: { authType: 'credentials', defaultRegion: 'eu-west-2' },
+      name: 'Loading',
+      orgId: 1,
+      password: '',
+      readOnly: false,
+      type: 'Loading',
+      typeLogoUrl: 'public/img/icn-datasource.svg',
+      url: '',
+      user: '',
+    },
+    {
+      id: '1',
+      name: '',
+      info: {
+        author: {
+          name: '',
+          url: '',
+        },
+        description: '',
+        links: [''],
+        logos: {
+          large: '',
+          small: '',
+        },
+        screenshots: '',
+        updated: '',
+        version: '',
+      },
+      includes: [{ type: '', name: '', path: '' }],
+    }
+  );
+
+  let node: NavModelItem;
+
+  // find active page
+  for (const child of main.children) {
+    if (child.id.indexOf(pageName) > 0) {
+      child.active = true;
+      node = child;
+      break;
+    }
+  }
+
+  return {
+    main: main,
+    node: node,
+  };
+}
+
+function hasDashboards(includes) {
+  return (
+    includes.filter(include => {
+      return include.type === 'dashboard';
+    }).length > 0
+  );
+}

+ 13 - 1
public/app/features/datasources/state/reducers.ts

@@ -1,15 +1,18 @@
-import { DataSource, DataSourcesState, Plugin } from 'app/types';
+import { DataSource, DataSourcePermissionDTO, DataSourcesState, Plugin } from 'app/types';
 import { Action, ActionTypes } from './actions';
 import { LayoutModes } from '../../../core/components/LayoutSelector/LayoutSelector';
 
 const initialState: DataSourcesState = {
   dataSources: [] as DataSource[],
+  dataSource: {} as DataSource,
   layoutMode: LayoutModes.Grid,
   searchQuery: '',
   dataSourcesCount: 0,
   dataSourceTypes: [] as Plugin[],
   dataSourceTypeSearchQuery: '',
   hasFetched: false,
+  dataSourceMeta: {} as Plugin,
+  dataSourcePermission: {} as DataSourcePermissionDTO,
 };
 
 export const dataSourcesReducer = (state = initialState, action: Action): DataSourcesState => {
@@ -17,6 +20,9 @@ export const dataSourcesReducer = (state = initialState, action: Action): DataSo
     case ActionTypes.LoadDataSources:
       return { ...state, hasFetched: true, dataSources: action.payload, dataSourcesCount: action.payload.length };
 
+    case ActionTypes.LoadDataSource:
+      return { ...state, dataSource: action.payload };
+
     case ActionTypes.SetDataSourcesSearchQuery:
       return { ...state, searchQuery: action.payload };
 
@@ -28,6 +34,12 @@ export const dataSourcesReducer = (state = initialState, action: Action): DataSo
 
     case ActionTypes.SetDataSourceTypeSearchQuery:
       return { ...state, dataSourceTypeSearchQuery: action.payload };
+
+    case ActionTypes.LoadDataSourceMeta:
+      return { ...state, dataSourceMeta: action.payload };
+
+    case ActionTypes.LoadDataSourcePermissions:
+      return { ...state, dataSourcePermission: action.payload };
   }
 
   return state;

+ 9 - 0
public/app/features/datasources/state/selectors.ts

@@ -1,3 +1,5 @@
+import { DataSource } from '../../../types';
+
 export const getDataSources = state => {
   const regex = new RegExp(state.searchQuery, 'i');
 
@@ -14,6 +16,13 @@ export const getDataSourceTypes = state => {
   });
 };
 
+export const getDataSource = (state, dataSourceId): DataSource | null => {
+  if (state.dataSource.id === parseInt(dataSourceId, 10)) {
+    return state.dataSource;
+  }
+  return null;
+};
+
 export const getDataSourcesSearchQuery = state => state.searchQuery;
 export const getDataSourcesLayoutMode = state => state.layoutMode;
 export const getDataSourcesCount = state => state.dataSourcesCount;

+ 11 - 0
public/app/features/plugins/state/navModel.ts

@@ -1,5 +1,6 @@
 import _ from 'lodash';
 import { DataSource, PluginMeta, NavModel } from 'app/types';
+import config from 'app/core/config';
 
 export function buildNavModel(ds: DataSource, plugin: PluginMeta, currentPage: string): NavModel {
   let title = 'New';
@@ -38,6 +39,16 @@ export function buildNavModel(ds: DataSource, plugin: PluginMeta, currentPage: s
     });
   }
 
+  if (config.buildInfo.isEnterprise) {
+    main.children.push({
+      active: currentPage === 'datasource-permissions',
+      icon: 'fa fa-fw fa-lock',
+      id: 'datasource-permissions',
+      text: 'Permissions',
+      url: `datasources/edit/${ds.id}/permissions`,
+    });
+  }
+
   return {
     main: main,
     node: _.find(main.children, { active: true }),

+ 7 - 0
public/app/routes/routes.ts

@@ -13,6 +13,7 @@ import FolderPermissions from 'app/features/folders/FolderPermissions';
 import DataSourcesListPage from 'app/features/datasources/DataSourcesListPage';
 import NewDataSourcePage from '../features/datasources/NewDataSourcePage';
 import UsersListPage from 'app/features/users/UsersListPage';
+import EditDataSourcePage from 'app/features/datasources/EditDataSourcePage';
 
 /** @ngInject */
 export function setupAngularRoutes($routeProvider, $locationProvider) {
@@ -82,6 +83,12 @@ export function setupAngularRoutes($routeProvider, $locationProvider) {
       controller: 'DataSourceDashboardsCtrl',
       controllerAs: 'ctrl',
     })
+    .when('/datasources/edit/:id/:page?', {
+      template: '<react-container />',
+      resolve: {
+        component: () => EditDataSourcePage,
+      },
+    })
     .when('/datasources/new', {
       template: '<react-container />',
       resolve: {

+ 9 - 0
public/app/types/acl.ts

@@ -61,6 +61,11 @@ export enum PermissionLevel {
   Admin = 4,
 }
 
+export enum DataSourcePermissionLevel {
+  Query = 1,
+  Admin = 2,
+}
+
 export enum AclTarget {
   Team = 'Team',
   User = 'User',
@@ -73,6 +78,10 @@ export interface AclTargetInfo {
   text: string;
 }
 
+export const dataSourceAclLevels = [
+  { value: DataSourcePermissionLevel.Query, label: 'Query', description: 'Can query data source.' },
+];
+
 export const dashboardAclTargets: AclTargetInfo[] = [
   { value: AclTarget.Team, text: 'Team' },
   { value: AclTarget.User, text: 'User' },

+ 28 - 3
public/app/types/datasources.ts

@@ -1,6 +1,28 @@
 import { LayoutMode } from '../core/components/LayoutSelector/LayoutSelector';
 import { Plugin } from './plugins';
 
+export interface DataSourcePermission {
+  id: number;
+  datasourceId: number;
+  permission: number;
+  permissionName: string;
+  created: string;
+  updated: string;
+  userId?: number;
+  userLogin?: string;
+  userEmail?: string;
+  userAvatarUrl?: string;
+  teamId?: number;
+  teamAvatarUrl?: string;
+  team?: string;
+}
+
+export interface DataSourcePermissionDTO {
+  datasourceId: number;
+  enabled: boolean;
+  permissions: DataSourcePermission[];
+}
+
 export interface DataSource {
   id: number;
   orgId: number;
@@ -12,10 +34,10 @@ export interface DataSource {
   password: string;
   user: string;
   database: string;
-  basicAuth: false;
-  isDefault: false;
+  basicAuth: boolean;
+  isDefault: boolean;
   jsonData: { authType: string; defaultRegion: string };
-  readOnly: false;
+  readOnly: boolean;
 }
 
 export interface DataSourcesState {
@@ -26,4 +48,7 @@ export interface DataSourcesState {
   dataSourcesCount: number;
   dataSourceTypes: Plugin[];
   hasFetched: boolean;
+  dataSource: DataSource;
+  dataSourceMeta: Plugin;
+  dataSourcePermission: DataSourcePermissionDTO;
 }

+ 3 - 1
public/app/types/index.ts

@@ -7,7 +7,7 @@ import { DashboardState } from './dashboard';
 import { DashboardAcl, OrgRole, PermissionLevel } from './acl';
 import { ApiKey, ApiKeysState, NewApiKey } from './apiKeys';
 import { Invitee, OrgUser, User, UsersState } from './user';
-import { DataSource, DataSourcesState } from './datasources';
+import { DataSource, DataSourcePermissionDTO, DataSourcePermission, DataSourcesState } from './datasources';
 import { PluginMeta, Plugin, PluginsState } from './plugins';
 
 export {
@@ -41,6 +41,8 @@ export {
   Plugin,
   PluginsState,
   DataSourcesState,
+  DataSourcePermissionDTO,
+  DataSourcePermission,
   Invitee,
   OrgUser,
   User,

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 176 - 0
yarn.lock


Vissa filer visades inte eftersom för många filer har ändrats