mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'data-source-instance-to-react' into data-source-settings-to-react
This commit is contained in:
commit
c5946ebd27
@ -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"
|
||||
},
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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{})
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
|
@ -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
public/app/features/datasources/AddDataSourcePermissions.tsx
Normal file
123
public/app/features/datasources/AddDataSourcePermissions.tsx
Normal file
@ -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;
|
@ -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
public/app/features/datasources/DataSourcePermissions.tsx
Normal file
155
public/app/features/datasources/DataSourcePermissions.tsx
Normal file
@ -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);
|
@ -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
public/app/features/datasources/DataSourcePermissionsList.tsx
Normal file
109
public/app/features/datasources/DataSourcePermissionsList.tsx
Normal file
@ -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
public/app/features/datasources/DataSourceSettings.tsx
Normal file
125
public/app/features/datasources/DataSourceSettings.tsx
Normal file
@ -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 & 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
public/app/features/datasources/EditDataSourcePage.tsx
Normal file
84
public/app/features/datasources/EditDataSourcePage.tsx
Normal file
@ -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));
|
@ -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,
|
||||
};
|
||||
|
||||
|
@ -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',
|
||||
};
|
||||
};
|
||||
|
@ -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>
|
||||
`;
|
@ -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>
|
||||
`;
|
@ -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>
|
||||
`;
|
@ -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
public/app/features/datasources/state/navModel.ts
Normal file
109
public/app/features/datasources/state/navModel.ts
Normal file
@ -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
|
||||
);
|
||||
}
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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 }),
|
||||
|
@ -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: {
|
||||
|
@ -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' },
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user