Commit 0fd59455 by Carl Bergquist Committed by GitHub

Fixes linting errors in datasource provisioning. (#23515)

parent 6f1a25a8
......@@ -732,6 +732,7 @@ jobs:
-E varcheck -E goconst -E errcheck -E staticcheck ./pkg/...
./scripts/go/bin/revive -formatter stylish -config ./scripts/go/configs/revive.toml ./pkg/...
./scripts/go/bin/revive -formatter stylish ./pkg/services/alerting/...
./scripts/go/bin/revive -formatter stylish ./pkg/services/provisioning/datasources/...
./scripts/go/bin/gosec -quiet -exclude=G104,G107,G108,G201,G202,G204,G301,G304,G401,G402,G501 \
-conf=./scripts/go/configs/gosec.json ./pkg/...
......
......@@ -84,7 +84,8 @@ revive-alerting: scripts/go/bin/revive
@echo "lint alerting via revive"
@scripts/go/bin/revive \
-formatter stylish \
./pkg/services/alerting/...
./pkg/services/alerting/... \
./pkg/services/provisioning/datasources/...
scripts/go/bin/golangci-lint: scripts/go/go.mod
@cd scripts/go; \
......
......@@ -133,10 +133,6 @@ github.com/gosimple/slug v1.4.2 h1:jDmprx3q/9Lfk4FkGZtvzDQ9Cj9eAmsjzeQGp24PeiQ=
github.com/gosimple/slug v1.4.2/go.mod h1:ER78kgg1Mv0NQGlXiDe57DpCyfbNywXXZ9mIorhxAf0=
github.com/grafana/grafana-plugin-model v0.0.0-20190930120109-1fc953a61fb4 h1:SPdxCL9BChFTlyi0Khv64vdCW4TMna8+sxL7+Chx+Ag=
github.com/grafana/grafana-plugin-model v0.0.0-20190930120109-1fc953a61fb4/go.mod h1:nc0XxBzjeGcrMltCDw269LoWF9S8ibhgxolCdA1R8To=
github.com/grafana/grafana-plugin-sdk-go v0.39.0 h1:tPP83HeY9gN4q8O3tYka1vd82OQ/3CFdwx4QeEhJ0Qc=
github.com/grafana/grafana-plugin-sdk-go v0.39.0/go.mod h1:xRhfTHl+Dkqf2Py6Lr4pcHBC5pm8/N+IwPJ0R/iAHMM=
github.com/grafana/grafana-plugin-sdk-go v0.40.1-0.20200409163705-fd66aee09a52 h1:WEfl8G9uHk31r3pnAmsK+NRcHGpXnXauWmbhic3KuVU=
github.com/grafana/grafana-plugin-sdk-go v0.40.1-0.20200409163705-fd66aee09a52/go.mod h1:xRhfTHl+Dkqf2Py6Lr4pcHBC5pm8/N+IwPJ0R/iAHMM=
github.com/grafana/grafana-plugin-sdk-go v0.42.0 h1:8oiAQa/uABBFT70GDAv3BnqHfdMOxy/P8SzYVURJH6Y=
github.com/grafana/grafana-plugin-sdk-go v0.42.0/go.mod h1:xRhfTHl+Dkqf2Py6Lr4pcHBC5pm8/N+IwPJ0R/iAHMM=
github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd h1:rNuUHR+CvK1IS89MMtcF0EpcVMZtjKfPRp4MEmt/aTs=
......
......@@ -14,8 +14,8 @@ type configReader struct {
log log.Logger
}
func (cr *configReader) readConfig(path string) ([]*DatasourcesAsConfig, error) {
var datasources []*DatasourcesAsConfig
func (cr *configReader) readConfig(path string) ([]*configs, error) {
var datasources []*configs
files, err := ioutil.ReadDir(path)
if err != nil {
......@@ -44,34 +44,34 @@ func (cr *configReader) readConfig(path string) ([]*DatasourcesAsConfig, error)
return datasources, nil
}
func (cr *configReader) parseDatasourceConfig(path string, file os.FileInfo) (*DatasourcesAsConfig, error) {
func (cr *configReader) parseDatasourceConfig(path string, file os.FileInfo) (*configs, error) {
filename, _ := filepath.Abs(filepath.Join(path, file.Name()))
yamlFile, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}
var apiVersion *ConfigVersion
var apiVersion *configVersion
err = yaml.Unmarshal(yamlFile, &apiVersion)
if err != nil {
return nil, err
}
if apiVersion == nil {
apiVersion = &ConfigVersion{ApiVersion: 0}
apiVersion = &configVersion{APIVersion: 0}
}
if apiVersion.ApiVersion > 0 {
v1 := &DatasourcesAsConfigV1{log: cr.log}
if apiVersion.APIVersion > 0 {
v1 := &configsV1{log: cr.log}
err = yaml.Unmarshal(yamlFile, v1)
if err != nil {
return nil, err
}
return v1.mapToDatasourceFromConfig(apiVersion.ApiVersion), nil
return v1.mapToDatasourceFromConfig(apiVersion.APIVersion), nil
}
var v0 *DatasourcesAsConfigV0
var v0 *configsV0
err = yaml.Unmarshal(yamlFile, &v0)
if err != nil {
return nil, err
......@@ -79,10 +79,10 @@ func (cr *configReader) parseDatasourceConfig(path string, file os.FileInfo) (*D
cr.log.Warn("[Deprecated] the datasource provisioning config is outdated. please upgrade", "filename", filename)
return v0.mapToDatasourceFromConfig(apiVersion.ApiVersion), nil
return v0.mapToDatasourceFromConfig(apiVersion.APIVersion), nil
}
func validateDefaultUniqueness(datasources []*DatasourcesAsConfig) error {
func validateDefaultUniqueness(datasources []*configs) error {
defaultCount := map[int64]int{}
for i := range datasources {
if datasources[i].Datasources == nil {
......@@ -90,21 +90,21 @@ func validateDefaultUniqueness(datasources []*DatasourcesAsConfig) error {
}
for _, ds := range datasources[i].Datasources {
if ds.OrgId == 0 {
ds.OrgId = 1
if ds.OrgID == 0 {
ds.OrgID = 1
}
if ds.IsDefault {
defaultCount[ds.OrgId] = defaultCount[ds.OrgId] + 1
if defaultCount[ds.OrgId] > 1 {
defaultCount[ds.OrgID] = defaultCount[ds.OrgID] + 1
if defaultCount[ds.OrgID] > 1 {
return ErrInvalidConfigToManyDefault
}
}
}
for _, ds := range datasources[i].DeleteDatasources {
if ds.OrgId == 0 {
ds.OrgId = 1
if ds.OrgID == 0 {
ds.OrgID = 1
}
}
}
......
......@@ -159,7 +159,7 @@ func TestDatasourceAsConfig(t *testing.T) {
dsCfg := cfg[0]
So(dsCfg.ApiVersion, ShouldEqual, 1)
So(dsCfg.APIVersion, ShouldEqual, 1)
validateDatasource(dsCfg)
validateDeleteDatasources(dsCfg)
......@@ -187,26 +187,28 @@ func TestDatasourceAsConfig(t *testing.T) {
dsCfg := cfg[0]
So(dsCfg.ApiVersion, ShouldEqual, 0)
So(dsCfg.APIVersion, ShouldEqual, 0)
validateDatasource(dsCfg)
validateDeleteDatasources(dsCfg)
})
})
}
func validateDeleteDatasources(dsCfg *DatasourcesAsConfig) {
func validateDeleteDatasources(dsCfg *configs) {
So(len(dsCfg.DeleteDatasources), ShouldEqual, 1)
deleteDs := dsCfg.DeleteDatasources[0]
So(deleteDs.Name, ShouldEqual, "old-graphite3")
So(deleteDs.OrgId, ShouldEqual, 2)
So(deleteDs.OrgID, ShouldEqual, 2)
}
func validateDatasource(dsCfg *DatasourcesAsConfig) {
func validateDatasource(dsCfg *configs) {
ds := dsCfg.Datasources[0]
So(ds.Name, ShouldEqual, "name")
So(ds.Type, ShouldEqual, "type")
So(ds.Access, ShouldEqual, models.DS_ACCESS_PROXY)
So(ds.OrgId, ShouldEqual, 2)
So(ds.Url, ShouldEqual, "url")
So(ds.OrgID, ShouldEqual, 2)
So(ds.URL, ShouldEqual, "url")
So(ds.User, ShouldEqual, "user")
So(ds.Password, ShouldEqual, "password")
So(ds.Database, ShouldEqual, "database")
......@@ -218,15 +220,15 @@ func validateDatasource(dsCfg *DatasourcesAsConfig) {
So(ds.Editable, ShouldBeTrue)
So(ds.Version, ShouldEqual, 10)
So(len(ds.JsonData), ShouldBeGreaterThan, 2)
So(ds.JsonData["graphiteVersion"], ShouldEqual, "1.1")
So(ds.JsonData["tlsAuth"], ShouldEqual, true)
So(ds.JsonData["tlsAuthWithCACert"], ShouldEqual, true)
So(len(ds.JSONData), ShouldBeGreaterThan, 2)
So(ds.JSONData["graphiteVersion"], ShouldEqual, "1.1")
So(ds.JSONData["tlsAuth"], ShouldEqual, true)
So(ds.JSONData["tlsAuthWithCACert"], ShouldEqual, true)
So(len(ds.SecureJsonData), ShouldBeGreaterThan, 2)
So(ds.SecureJsonData["tlsCACert"], ShouldEqual, "MjNOcW9RdkbUDHZmpco2HCYzVq9dE+i6Yi+gmUJotq5CDA==")
So(ds.SecureJsonData["tlsClientCert"], ShouldEqual, "ckN0dGlyMXN503YNfjTcf9CV+GGQneN+xmAclQ==")
So(ds.SecureJsonData["tlsClientKey"], ShouldEqual, "ZkN4aG1aNkja/gKAB1wlnKFIsy2SRDq4slrM0A==")
So(len(ds.SecureJSONData), ShouldBeGreaterThan, 2)
So(ds.SecureJSONData["tlsCACert"], ShouldEqual, "MjNOcW9RdkbUDHZmpco2HCYzVq9dE+i6Yi+gmUJotq5CDA==")
So(ds.SecureJSONData["tlsClientCert"], ShouldEqual, "ckN0dGlyMXN503YNfjTcf9CV+GGQneN+xmAclQ==")
So(ds.SecureJSONData["tlsClientKey"], ShouldEqual, "ZkN4aG1aNkja/gKAB1wlnKFIsy2SRDq4slrM0A==")
}
type fakeRepository struct {
......
......@@ -11,14 +11,20 @@ import (
)
var (
// ErrInvalidConfigToManyDefault indicates that multiple datasource in the provisioning files
// contains more than one datasource marked as default.
ErrInvalidConfigToManyDefault = errors.New("datasource.yaml config is invalid. Only one datasource per organization can be marked as default")
)
// Provision scans a directory for provisioning config files
// and provisions the datasource in those files.
func Provision(configDirectory string) error {
dc := newDatasourceProvisioner(log.New("provisioning.datasources"))
return dc.applyChanges(configDirectory)
}
// DatasourceProvisioner is responsible for provisioning datasources based on
// configuration read by the `configReader`
type DatasourceProvisioner struct {
log log.Logger
cfgProvider *configReader
......@@ -31,13 +37,13 @@ func newDatasourceProvisioner(log log.Logger) DatasourceProvisioner {
}
}
func (dc *DatasourceProvisioner) apply(cfg *DatasourcesAsConfig) error {
func (dc *DatasourceProvisioner) apply(cfg *configs) error {
if err := dc.deleteDatasources(cfg.DeleteDatasources); err != nil {
return err
}
for _, ds := range cfg.Datasources {
cmd := &models.GetDataSourceByNameQuery{OrgId: ds.OrgId, Name: ds.Name}
cmd := &models.GetDataSourceByNameQuery{OrgId: ds.OrgID, Name: ds.Name}
err := bus.Dispatch(cmd)
if err != nil && err != models.ErrDataSourceNotFound {
return err
......@@ -76,9 +82,9 @@ func (dc *DatasourceProvisioner) applyChanges(configPath string) error {
return nil
}
func (dc *DatasourceProvisioner) deleteDatasources(dsToDelete []*DeleteDatasourceConfig) error {
func (dc *DatasourceProvisioner) deleteDatasources(dsToDelete []*deleteDatasourceConfig) error {
for _, ds := range dsToDelete {
cmd := &models.DeleteDataSourceByNameCommand{OrgId: ds.OrgId, Name: ds.Name}
cmd := &models.DeleteDataSourceByNameCommand{OrgId: ds.OrgID, Name: ds.Name}
if err := bus.Dispatch(cmd); err != nil {
return err
}
......
......@@ -7,30 +7,31 @@ import (
"github.com/grafana/grafana/pkg/services/provisioning/values"
)
type ConfigVersion struct {
ApiVersion int64 `json:"apiVersion" yaml:"apiVersion"`
// ConfigVersion is used to figure out which API version a config uses.
type configVersion struct {
APIVersion int64 `json:"apiVersion" yaml:"apiVersion"`
}
type DatasourcesAsConfig struct {
ApiVersion int64
type configs struct {
APIVersion int64
Datasources []*DataSourceFromConfig
DeleteDatasources []*DeleteDatasourceConfig
Datasources []*upsertDataSourceFromConfig
DeleteDatasources []*deleteDatasourceConfig
}
type DeleteDatasourceConfig struct {
OrgId int64
type deleteDatasourceConfig struct {
OrgID int64
Name string
}
type DataSourceFromConfig struct {
OrgId int64
type upsertDataSourceFromConfig struct {
OrgID int64
Version int
Name string
Type string
Access string
Url string
URL string
Password string
User string
Database string
......@@ -39,43 +40,43 @@ type DataSourceFromConfig struct {
BasicAuthPassword string
WithCredentials bool
IsDefault bool
JsonData map[string]interface{}
SecureJsonData map[string]string
JSONData map[string]interface{}
SecureJSONData map[string]string
Editable bool
}
type DatasourcesAsConfigV0 struct {
ConfigVersion
type configsV0 struct {
configVersion
Datasources []*DataSourceFromConfigV0 `json:"datasources" yaml:"datasources"`
DeleteDatasources []*DeleteDatasourceConfigV0 `json:"delete_datasources" yaml:"delete_datasources"`
Datasources []*upsertDataSourceFromConfigV0 `json:"datasources" yaml:"datasources"`
DeleteDatasources []*deleteDatasourceConfigV0 `json:"delete_datasources" yaml:"delete_datasources"`
}
type DatasourcesAsConfigV1 struct {
ConfigVersion
type configsV1 struct {
configVersion
log log.Logger
Datasources []*DataSourceFromConfigV1 `json:"datasources" yaml:"datasources"`
DeleteDatasources []*DeleteDatasourceConfigV1 `json:"deleteDatasources" yaml:"deleteDatasources"`
Datasources []*upsertDataSourceFromConfigV1 `json:"datasources" yaml:"datasources"`
DeleteDatasources []*deleteDatasourceConfigV1 `json:"deleteDatasources" yaml:"deleteDatasources"`
}
type DeleteDatasourceConfigV0 struct {
OrgId int64 `json:"org_id" yaml:"org_id"`
type deleteDatasourceConfigV0 struct {
OrgID int64 `json:"org_id" yaml:"org_id"`
Name string `json:"name" yaml:"name"`
}
type DeleteDatasourceConfigV1 struct {
OrgId values.Int64Value `json:"orgId" yaml:"orgId"`
type deleteDatasourceConfigV1 struct {
OrgID values.Int64Value `json:"orgId" yaml:"orgId"`
Name values.StringValue `json:"name" yaml:"name"`
}
type DataSourceFromConfigV0 struct {
OrgId int64 `json:"org_id" yaml:"org_id"`
type upsertDataSourceFromConfigV0 struct {
OrgID int64 `json:"org_id" yaml:"org_id"`
Version int `json:"version" yaml:"version"`
Name string `json:"name" yaml:"name"`
Type string `json:"type" yaml:"type"`
Access string `json:"access" yaml:"access"`
Url string `json:"url" yaml:"url"`
URL string `json:"url" yaml:"url"`
Password string `json:"password" yaml:"password"`
User string `json:"user" yaml:"user"`
Database string `json:"database" yaml:"database"`
......@@ -84,18 +85,18 @@ type DataSourceFromConfigV0 struct {
BasicAuthPassword string `json:"basic_auth_password" yaml:"basic_auth_password"`
WithCredentials bool `json:"with_credentials" yaml:"with_credentials"`
IsDefault bool `json:"is_default" yaml:"is_default"`
JsonData map[string]interface{} `json:"json_data" yaml:"json_data"`
SecureJsonData map[string]string `json:"secure_json_data" yaml:"secure_json_data"`
JSONData map[string]interface{} `json:"json_data" yaml:"json_data"`
SecureJSONData map[string]string `json:"secure_json_data" yaml:"secure_json_data"`
Editable bool `json:"editable" yaml:"editable"`
}
type DataSourceFromConfigV1 struct {
OrgId values.Int64Value `json:"orgId" yaml:"orgId"`
type upsertDataSourceFromConfigV1 struct {
OrgID values.Int64Value `json:"orgId" yaml:"orgId"`
Version values.IntValue `json:"version" yaml:"version"`
Name values.StringValue `json:"name" yaml:"name"`
Type values.StringValue `json:"type" yaml:"type"`
Access values.StringValue `json:"access" yaml:"access"`
Url values.StringValue `json:"url" yaml:"url"`
URL values.StringValue `json:"url" yaml:"url"`
Password values.StringValue `json:"password" yaml:"password"`
User values.StringValue `json:"user" yaml:"user"`
Database values.StringValue `json:"database" yaml:"database"`
......@@ -104,27 +105,27 @@ type DataSourceFromConfigV1 struct {
BasicAuthPassword values.StringValue `json:"basicAuthPassword" yaml:"basicAuthPassword"`
WithCredentials values.BoolValue `json:"withCredentials" yaml:"withCredentials"`
IsDefault values.BoolValue `json:"isDefault" yaml:"isDefault"`
JsonData values.JSONValue `json:"jsonData" yaml:"jsonData"`
SecureJsonData values.StringMapValue `json:"secureJsonData" yaml:"secureJsonData"`
JSONData values.JSONValue `json:"jsonData" yaml:"jsonData"`
SecureJSONData values.StringMapValue `json:"secureJsonData" yaml:"secureJsonData"`
Editable values.BoolValue `json:"editable" yaml:"editable"`
}
func (cfg *DatasourcesAsConfigV1) mapToDatasourceFromConfig(apiVersion int64) *DatasourcesAsConfig {
r := &DatasourcesAsConfig{}
func (cfg *configsV1) mapToDatasourceFromConfig(apiVersion int64) *configs {
r := &configs{}
r.ApiVersion = apiVersion
r.APIVersion = apiVersion
if cfg == nil {
return r
}
for _, ds := range cfg.Datasources {
r.Datasources = append(r.Datasources, &DataSourceFromConfig{
OrgId: ds.OrgId.Value(),
r.Datasources = append(r.Datasources, &upsertDataSourceFromConfig{
OrgID: ds.OrgID.Value(),
Name: ds.Name.Value(),
Type: ds.Type.Value(),
Access: ds.Access.Value(),
Url: ds.Url.Value(),
URL: ds.URL.Value(),
Password: ds.Password.Value(),
User: ds.User.Value(),
Database: ds.Database.Value(),
......@@ -133,8 +134,8 @@ func (cfg *DatasourcesAsConfigV1) mapToDatasourceFromConfig(apiVersion int64) *D
BasicAuthPassword: ds.BasicAuthPassword.Value(),
WithCredentials: ds.WithCredentials.Value(),
IsDefault: ds.IsDefault.Value(),
JsonData: ds.JsonData.Value(),
SecureJsonData: ds.SecureJsonData.Value(),
JSONData: ds.JSONData.Value(),
SecureJSONData: ds.SecureJSONData.Value(),
Editable: ds.Editable.Value(),
Version: ds.Version.Value(),
})
......@@ -158,8 +159,8 @@ func (cfg *DatasourcesAsConfigV1) mapToDatasourceFromConfig(apiVersion int64) *D
}
for _, ds := range cfg.DeleteDatasources {
r.DeleteDatasources = append(r.DeleteDatasources, &DeleteDatasourceConfig{
OrgId: ds.OrgId.Value(),
r.DeleteDatasources = append(r.DeleteDatasources, &deleteDatasourceConfig{
OrgID: ds.OrgID.Value(),
Name: ds.Name.Value(),
})
}
......@@ -167,22 +168,22 @@ func (cfg *DatasourcesAsConfigV1) mapToDatasourceFromConfig(apiVersion int64) *D
return r
}
func (cfg *DatasourcesAsConfigV0) mapToDatasourceFromConfig(apiVersion int64) *DatasourcesAsConfig {
r := &DatasourcesAsConfig{}
func (cfg *configsV0) mapToDatasourceFromConfig(apiVersion int64) *configs {
r := &configs{}
r.ApiVersion = apiVersion
r.APIVersion = apiVersion
if cfg == nil {
return r
}
for _, ds := range cfg.Datasources {
r.Datasources = append(r.Datasources, &DataSourceFromConfig{
OrgId: ds.OrgId,
r.Datasources = append(r.Datasources, &upsertDataSourceFromConfig{
OrgID: ds.OrgID,
Name: ds.Name,
Type: ds.Type,
Access: ds.Access,
Url: ds.Url,
URL: ds.URL,
Password: ds.Password,
User: ds.User,
Database: ds.Database,
......@@ -191,16 +192,16 @@ func (cfg *DatasourcesAsConfigV0) mapToDatasourceFromConfig(apiVersion int64) *D
BasicAuthPassword: ds.BasicAuthPassword,
WithCredentials: ds.WithCredentials,
IsDefault: ds.IsDefault,
JsonData: ds.JsonData,
SecureJsonData: ds.SecureJsonData,
JSONData: ds.JSONData,
SecureJSONData: ds.SecureJSONData,
Editable: ds.Editable,
Version: ds.Version,
})
}
for _, ds := range cfg.DeleteDatasources {
r.DeleteDatasources = append(r.DeleteDatasources, &DeleteDatasourceConfig{
OrgId: ds.OrgId,
r.DeleteDatasources = append(r.DeleteDatasources, &deleteDatasourceConfig{
OrgID: ds.OrgID,
Name: ds.Name,
})
}
......@@ -208,20 +209,20 @@ func (cfg *DatasourcesAsConfigV0) mapToDatasourceFromConfig(apiVersion int64) *D
return r
}
func createInsertCommand(ds *DataSourceFromConfig) *models.AddDataSourceCommand {
func createInsertCommand(ds *upsertDataSourceFromConfig) *models.AddDataSourceCommand {
jsonData := simplejson.New()
if len(ds.JsonData) > 0 {
for k, v := range ds.JsonData {
if len(ds.JSONData) > 0 {
for k, v := range ds.JSONData {
jsonData.Set(k, v)
}
}
return &models.AddDataSourceCommand{
OrgId: ds.OrgId,
OrgId: ds.OrgID,
Name: ds.Name,
Type: ds.Type,
Access: models.DsAccess(ds.Access),
Url: ds.Url,
Url: ds.URL,
Password: ds.Password,
User: ds.User,
Database: ds.Database,
......@@ -231,26 +232,26 @@ func createInsertCommand(ds *DataSourceFromConfig) *models.AddDataSourceCommand
WithCredentials: ds.WithCredentials,
IsDefault: ds.IsDefault,
JsonData: jsonData,
SecureJsonData: ds.SecureJsonData,
SecureJsonData: ds.SecureJSONData,
ReadOnly: !ds.Editable,
}
}
func createUpdateCommand(ds *DataSourceFromConfig, id int64) *models.UpdateDataSourceCommand {
func createUpdateCommand(ds *upsertDataSourceFromConfig, id int64) *models.UpdateDataSourceCommand {
jsonData := simplejson.New()
if len(ds.JsonData) > 0 {
for k, v := range ds.JsonData {
if len(ds.JSONData) > 0 {
for k, v := range ds.JSONData {
jsonData.Set(k, v)
}
}
return &models.UpdateDataSourceCommand{
Id: id,
OrgId: ds.OrgId,
OrgId: ds.OrgID,
Name: ds.Name,
Type: ds.Type,
Access: models.DsAccess(ds.Access),
Url: ds.Url,
Url: ds.URL,
Password: ds.Password,
User: ds.User,
Database: ds.Database,
......@@ -260,7 +261,7 @@ func createUpdateCommand(ds *DataSourceFromConfig, id int64) *models.UpdateDataS
WithCredentials: ds.WithCredentials,
IsDefault: ds.IsDefault,
JsonData: jsonData,
SecureJsonData: ds.SecureJsonData,
SecureJsonData: ds.SecureJSONData,
ReadOnly: !ds.Editable,
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment