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

This commit is contained in:
Peter Holmberg 2018-10-12 09:20:43 +02:00
commit c5946ebd27
33 changed files with 3683 additions and 34 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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;

View File

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

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

View File

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

View 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;

View 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 &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);

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

View File

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

View File

@ -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',
};
};

View File

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

View File

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

View File

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

View File

@ -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 => {

View 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
);
}

View File

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

View File

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

View File

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

View File

@ -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: {

View File

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

View File

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

View File

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

1935
yarn.lock

File diff suppressed because it is too large Load Diff