Merge branch 'data-source-instance-to-react' into datasource-dashboards-to-react

This commit is contained in:
Peter Holmberg 2018-10-17 11:12:13 +02:00
commit 314fffeae1
20 changed files with 434 additions and 35 deletions

View File

@ -234,13 +234,13 @@ func (hs *HTTPServer) registerRoutes() {
datasourceRoute.Get("/", Wrap(GetDataSources)) datasourceRoute.Get("/", Wrap(GetDataSources))
datasourceRoute.Post("/", quota("data_source"), bind(m.AddDataSourceCommand{}), Wrap(AddDataSource)) datasourceRoute.Post("/", quota("data_source"), bind(m.AddDataSourceCommand{}), Wrap(AddDataSource))
datasourceRoute.Put("/:id", bind(m.UpdateDataSourceCommand{}), Wrap(UpdateDataSource)) 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.Delete("/name/:name", Wrap(DeleteDataSourceByName))
datasourceRoute.Get("/:id", Wrap(GetDataSourceByID)) datasourceRoute.Get("/:id", Wrap(GetDataSourceById))
datasourceRoute.Get("/name/:name", Wrap(GetDataSourceByName)) datasourceRoute.Get("/name/:name", Wrap(GetDataSourceByName))
}, reqOrgAdmin) }, 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", Wrap(GetPluginList))
apiRoute.Get("/plugins/:pluginId/settings", Wrap(GetPluginSettingByID)) apiRoute.Get("/plugins/:pluginId/settings", Wrap(GetPluginSettingByID))

View File

@ -2,6 +2,7 @@ package api
import ( import (
"fmt" "fmt"
"github.com/pkg/errors"
"time" "time"
"github.com/grafana/grafana/pkg/api/pluginproxy" "github.com/grafana/grafana/pkg/api/pluginproxy"
@ -14,6 +15,20 @@ import (
const HeaderNameNoBackendCache = "X-Grafana-NoCache" const HeaderNameNoBackendCache = "X-Grafana-NoCache"
func (hs *HTTPServer) getDatasourceFromCache(id int64, c *m.ReqContext) (*m.DataSource, error) { 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" nocache := c.Req.Header.Get(HeaderNameNoBackendCache) == "true"
cacheKey := fmt.Sprintf("ds-%d", id) cacheKey := fmt.Sprintf("ds-%d", id)
@ -38,7 +53,9 @@ func (hs *HTTPServer) getDatasourceFromCache(id int64, c *m.ReqContext) (*m.Data
func (hs *HTTPServer) ProxyDataSourceRequest(c *m.ReqContext) { func (hs *HTTPServer) ProxyDataSourceRequest(c *m.ReqContext) {
c.TimeRequest(metrics.M_DataSource_ProxyReq_Timer) 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)
if err != nil { if err != nil {
c.JsonApiErr(500, "Unable to load datasource meta data", err) c.JsonApiErr(500, "Unable to load datasource meta data", err)
return return

View File

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

View File

@ -30,6 +30,7 @@ var (
ErrDataSourceNameExists = errors.New("Data source with same name already exists") ErrDataSourceNameExists = errors.New("Data source with same name already exists")
ErrDataSourceUpdatingOldVersion = errors.New("Trying to update old version of datasource") ErrDataSourceUpdatingOldVersion = errors.New("Trying to update old version of datasource")
ErrDatasourceIsReadOnly = errors.New("Data source is readonly. Can only be updated from configuration.") ErrDatasourceIsReadOnly = errors.New("Data source is readonly. Can only be updated from configuration.")
ErrDataSourceAccessDenied = errors.New("Data source access denied")
) )
type DsAccess string type DsAccess string
@ -167,6 +168,7 @@ type DeleteDataSourceByNameCommand struct {
type GetDataSourcesQuery struct { type GetDataSourcesQuery struct {
OrgId int64 OrgId int64
User *SignedInUser
Result []*DataSource Result []*DataSource
} }
@ -187,6 +189,37 @@ type GetDataSourceByNameQuery struct {
} }
// --------------------- // ---------------------
// EVENTS // Permissions
type DataSourceCreatedEvent struct { // ---------------------
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} datasource := m.DataSource{OrgId: query.OrgId, Id: query.Id}
has, err := x.Get(&datasource) has, err := x.Get(&datasource)
if err != nil { if err != nil {
return err return err
} }

View File

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

View File

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

View File

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

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

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

View File

@ -2,12 +2,15 @@ import { ThunkAction } from 'redux-thunk';
import { DataSource, Plugin, StoreState } from 'app/types'; import { DataSource, Plugin, StoreState } from 'app/types';
import { getBackendSrv } from '../../../core/services/backend_srv'; import { getBackendSrv } from '../../../core/services/backend_srv';
import { LayoutMode } from '../../../core/components/LayoutSelector/LayoutSelector'; 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 { UpdateLocationAction } from '../../../core/actions/location';
import { buildNavModel } from './navModel';
export enum ActionTypes { export enum ActionTypes {
LoadDataSources = 'LOAD_DATA_SOURCES', LoadDataSources = 'LOAD_DATA_SOURCES',
LoadDataSourceTypes = 'LOAD_DATA_SOURCE_TYPES', LoadDataSourceTypes = 'LOAD_DATA_SOURCE_TYPES',
LoadDataSource = 'LOAD_DATA_SOURCE',
LoadDataSourceMeta = 'LOAD_DATA_SOURCE_META',
SetDataSourcesSearchQuery = 'SET_DATA_SOURCES_SEARCH_QUERY', SetDataSourcesSearchQuery = 'SET_DATA_SOURCES_SEARCH_QUERY',
SetDataSourcesLayoutMode = 'SET_DATA_SOURCES_LAYOUT_MODE', SetDataSourcesLayoutMode = 'SET_DATA_SOURCES_LAYOUT_MODE',
SetDataSourceTypeSearchQuery = 'SET_DATA_SOURCE_TYPE_SEARCH_QUERY', SetDataSourceTypeSearchQuery = 'SET_DATA_SOURCE_TYPE_SEARCH_QUERY',
@ -38,11 +41,31 @@ export interface SetDataSourceTypeSearchQueryAction {
payload: string; payload: string;
} }
export interface LoadDataSourceAction {
type: ActionTypes.LoadDataSource;
payload: DataSource;
}
export interface LoadDataSourceMetaAction {
type: ActionTypes.LoadDataSourceMeta;
payload: Plugin;
}
const dataSourcesLoaded = (dataSources: DataSource[]): LoadDataSourcesAction => ({ const dataSourcesLoaded = (dataSources: DataSource[]): LoadDataSourcesAction => ({
type: ActionTypes.LoadDataSources, type: ActionTypes.LoadDataSources,
payload: dataSources, 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 => ({ const dataSourceTypesLoaded = (dataSourceTypes: Plugin[]): LoadDataSourceTypesAction => ({
type: ActionTypes.LoadDataSourceTypes, type: ActionTypes.LoadDataSourceTypes,
payload: dataSourceTypes, payload: dataSourceTypes,
@ -69,7 +92,10 @@ export type Action =
| SetDataSourcesLayoutModeAction | SetDataSourcesLayoutModeAction
| UpdateLocationAction | UpdateLocationAction
| LoadDataSourceTypesAction | LoadDataSourceTypesAction
| SetDataSourceTypeSearchQueryAction; | SetDataSourceTypeSearchQueryAction
| LoadDataSourceAction
| UpdateNavIndexAction
| LoadDataSourceMetaAction;
type ThunkResult<R> = ThunkAction<R, StoreState, undefined, Action>; type ThunkResult<R> = ThunkAction<R, StoreState, undefined, Action>;
@ -80,6 +106,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> { export function addDataSource(plugin: Plugin): ThunkResult<void> {
return async (dispatch, getStore) => { return async (dispatch, getStore) => {
await dispatch(loadDataSources()); await dispatch(loadDataSources());

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

@ -4,11 +4,13 @@ import { LayoutModes } from '../../../core/components/LayoutSelector/LayoutSelec
const initialState: DataSourcesState = { const initialState: DataSourcesState = {
dataSources: [] as DataSource[], dataSources: [] as DataSource[],
dataSource: {} as DataSource,
layoutMode: LayoutModes.Grid, layoutMode: LayoutModes.Grid,
searchQuery: '', searchQuery: '',
dataSourcesCount: 0, dataSourcesCount: 0,
dataSourceTypes: [] as Plugin[], dataSourceTypes: [] as Plugin[],
dataSourceTypeSearchQuery: '', dataSourceTypeSearchQuery: '',
dataSourceMeta: {} as Plugin,
hasFetched: false, hasFetched: false,
}; };
@ -17,6 +19,9 @@ export const dataSourcesReducer = (state = initialState, action: Action): DataSo
case ActionTypes.LoadDataSources: case ActionTypes.LoadDataSources:
return { ...state, hasFetched: true, dataSources: action.payload, dataSourcesCount: action.payload.length }; return { ...state, hasFetched: true, dataSources: action.payload, dataSourcesCount: action.payload.length };
case ActionTypes.LoadDataSource:
return { ...state, dataSource: action.payload };
case ActionTypes.SetDataSourcesSearchQuery: case ActionTypes.SetDataSourcesSearchQuery:
return { ...state, searchQuery: action.payload }; return { ...state, searchQuery: action.payload };
@ -28,6 +33,9 @@ export const dataSourcesReducer = (state = initialState, action: Action): DataSo
case ActionTypes.SetDataSourceTypeSearchQuery: case ActionTypes.SetDataSourceTypeSearchQuery:
return { ...state, dataSourceTypeSearchQuery: action.payload }; return { ...state, dataSourceTypeSearchQuery: action.payload };
case ActionTypes.LoadDataSourceMeta:
return { ...state, dataSourceMeta: action.payload };
} }
return state; return state;

View File

@ -1,3 +1,5 @@
import { DataSource } from '../../../types';
export const getDataSources = state => { export const getDataSources = state => {
const regex = new RegExp(state.searchQuery, 'i'); 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 getDataSourcesSearchQuery = state => state.searchQuery;
export const getDataSourcesLayoutMode = state => state.layoutMode; export const getDataSourcesLayoutMode = state => state.layoutMode;
export const getDataSourcesCount = state => state.dataSourcesCount; export const getDataSourcesCount = state => state.dataSourcesCount;

View File

@ -571,10 +571,10 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
<button className="btn navbar-button navbar-button--tight">Log labels</button> <button className="btn navbar-button navbar-button--tight">Log labels</button>
</Cascader> </Cascader>
) : ( ) : (
<Cascader options={metricsOptions} onChange={this.onChangeMetrics}> <Cascader options={metricsOptions} onChange={this.onChangeMetrics}>
<button className="btn navbar-button navbar-button--tight">Metrics</button> <button className="btn navbar-button navbar-button--tight">Metrics</button>
</Cascader> </Cascader>
)} )}
</div> </div>
<div className="prom-query-field-wrapper"> <div className="prom-query-field-wrapper">
<div className="slate-query-field-wrapper"> <div className="slate-query-field-wrapper">

View File

@ -1,5 +1,6 @@
import _ from 'lodash'; import _ from 'lodash';
import { DataSource, PluginMeta, NavModel } from 'app/types'; import { DataSource, PluginMeta, NavModel } from 'app/types';
import config from 'app/core/config';
export function buildNavModel(ds: DataSource, plugin: PluginMeta, currentPage: string): NavModel { export function buildNavModel(ds: DataSource, plugin: PluginMeta, currentPage: string): NavModel {
let title = 'New'; 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 { return {
main: main, main: main,
node: _.find(main.children, { active: true }), node: _.find(main.children, { active: true }),

View File

@ -11,7 +11,7 @@ import pluginReducers from 'app/features/plugins/state/reducers';
import dataSourcesReducers from 'app/features/datasources/state/reducers'; import dataSourcesReducers from 'app/features/datasources/state/reducers';
import usersReducers from 'app/features/users/state/reducers'; import usersReducers from 'app/features/users/state/reducers';
const rootReducer = combineReducers({ const rootReducers = {
...sharedReducers, ...sharedReducers,
...alertingReducers, ...alertingReducers,
...teamsReducers, ...teamsReducers,
@ -21,13 +21,19 @@ const rootReducer = combineReducers({
...pluginReducers, ...pluginReducers,
...dataSourcesReducers, ...dataSourcesReducers,
...usersReducers, ...usersReducers,
}); };
export let store; export let store;
export function addRootReducer(reducers) {
Object.assign(rootReducers, ...reducers);
}
export function configureStore() { export function configureStore() {
const composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; const composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const rootReducer = combineReducers(rootReducers);
if (process.env.NODE_ENV !== 'production') { if (process.env.NODE_ENV !== 'production') {
// DEV builds we had the logger middleware // DEV builds we had the logger middleware
store = createStore(rootReducer, {}, composeEnhancers(applyMiddleware(thunk, createLogger()))); store = createStore(rootReducer, {}, composeEnhancers(applyMiddleware(thunk, createLogger())));

View File

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

View File

@ -12,10 +12,10 @@ export interface DataSource {
password: string; password: string;
user: string; user: string;
database: string; database: string;
basicAuth: false; basicAuth: boolean;
isDefault: false; isDefault: boolean;
jsonData: { authType: string; defaultRegion: string }; jsonData: { authType: string; defaultRegion: string };
readOnly: false; readOnly: boolean;
} }
export interface DataSourcesState { export interface DataSourcesState {
@ -25,5 +25,7 @@ export interface DataSourcesState {
layoutMode: LayoutMode; layoutMode: LayoutMode;
dataSourcesCount: number; dataSourcesCount: number;
dataSourceTypes: Plugin[]; dataSourceTypes: Plugin[];
dataSource: DataSource;
dataSourceMeta: Plugin;
hasFetched: boolean; hasFetched: boolean;
} }