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 datasource-dashboards-to-react
This commit is contained in:
commit
314fffeae1
@ -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))
|
||||||
|
@ -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
|
||||||
|
@ -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 {
|
||||||
|
@ -22,7 +22,20 @@ func getFrontendSettingsMap(c *m.ReqContext) (map[string]interface{}, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
orgDataSources = query.Result
|
||||||
|
} else {
|
||||||
|
orgDataSources = dsFilterQuery.Result
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
datasources := make(map[string]interface{})
|
datasources := make(map[string]interface{})
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
@ -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);
|
||||||
|
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);
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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());
|
||||||
|
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
|
||||||
|
);
|
||||||
|
}
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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 }),
|
||||||
|
@ -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())));
|
||||||
|
@ -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' },
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user