adding tests for dashboard settings
fixing database persistence for settings. using reflection instead of abusing AppConfig for parsing UserSettingsEntries to UserSettings struct.
This commit is contained in:
parent
cb6cb1d2c5
commit
9a4dcf9852
|
@ -10,8 +10,6 @@ import (
|
|||
"strings"
|
||||
)
|
||||
|
||||
const DB_USER_SETTINGS_SUBKEY = "user"
|
||||
|
||||
// When initializing this class the following methods must be called:
|
||||
// Config.New
|
||||
// Config.Init
|
||||
|
|
|
@ -146,6 +146,20 @@ func (mr *MockInterfaceMockRecorder) IsSet(key interface{}) *gomock.Call {
|
|||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsSet", reflect.TypeOf((*MockInterface)(nil).IsSet), key)
|
||||
}
|
||||
|
||||
// MergeConfigMap mocks base method.
|
||||
func (m *MockInterface) MergeConfigMap(cfg map[string]interface{}) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "MergeConfigMap", cfg)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// MergeConfigMap indicates an expected call of MergeConfigMap.
|
||||
func (mr *MockInterfaceMockRecorder) MergeConfigMap(cfg interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MergeConfigMap", reflect.TypeOf((*MockInterface)(nil).MergeConfigMap), cfg)
|
||||
}
|
||||
|
||||
// ReadConfig mocks base method.
|
||||
func (m *MockInterface) ReadConfig(configFilePath string) error {
|
||||
m.ctrl.T.Helper()
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
pkg "github.com/fastenhealth/fastenhealth-onprem/backend/pkg"
|
||||
models0 "github.com/fastenhealth/fastenhealth-onprem/backend/pkg/models"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
uuid "github.com/google/uuid"
|
||||
)
|
||||
|
||||
// MockDatabaseRepository is a mock of DatabaseRepository interface.
|
||||
|
@ -316,6 +317,21 @@ func (mr *MockDatabaseRepositoryMockRecorder) ListResources(arg0, arg1 interface
|
|||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListResources", reflect.TypeOf((*MockDatabaseRepository)(nil).ListResources), arg0, arg1)
|
||||
}
|
||||
|
||||
// LoadUserSettings mocks base method.
|
||||
func (m *MockDatabaseRepository) LoadUserSettings(ctx context.Context) (*models0.UserSettings, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "LoadUserSettings", ctx)
|
||||
ret0, _ := ret[0].(*models0.UserSettings)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// LoadUserSettings indicates an expected call of LoadUserSettings.
|
||||
func (mr *MockDatabaseRepositoryMockRecorder) LoadUserSettings(ctx interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadUserSettings", reflect.TypeOf((*MockDatabaseRepository)(nil).LoadUserSettings), ctx)
|
||||
}
|
||||
|
||||
// Migrate mocks base method.
|
||||
func (m *MockDatabaseRepository) Migrate() error {
|
||||
m.ctrl.T.Helper()
|
||||
|
@ -330,11 +346,25 @@ func (mr *MockDatabaseRepositoryMockRecorder) Migrate() *gomock.Call {
|
|||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Migrate", reflect.TypeOf((*MockDatabaseRepository)(nil).Migrate))
|
||||
}
|
||||
|
||||
// PopulateDefaultUserSettings mocks base method.
|
||||
func (m *MockDatabaseRepository) PopulateDefaultUserSettings(ctx context.Context, userId uuid.UUID) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "PopulateDefaultUserSettings", ctx, userId)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// PopulateDefaultUserSettings indicates an expected call of PopulateDefaultUserSettings.
|
||||
func (mr *MockDatabaseRepositoryMockRecorder) PopulateDefaultUserSettings(ctx, userId interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PopulateDefaultUserSettings", reflect.TypeOf((*MockDatabaseRepository)(nil).PopulateDefaultUserSettings), ctx, userId)
|
||||
}
|
||||
|
||||
// QueryResources mocks base method.
|
||||
func (m *MockDatabaseRepository) QueryResources(ctx context.Context, query models0.QueryResource) ([]models0.ResourceBase, error) {
|
||||
func (m *MockDatabaseRepository) QueryResources(ctx context.Context, query models0.QueryResource) (interface{}, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "QueryResources", ctx, query)
|
||||
ret0, _ := ret[0].([]models0.ResourceBase)
|
||||
ret0, _ := ret[0].(interface{})
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
@ -359,6 +389,20 @@ func (mr *MockDatabaseRepositoryMockRecorder) RemoveResourceAssociation(ctx, sou
|
|||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveResourceAssociation", reflect.TypeOf((*MockDatabaseRepository)(nil).RemoveResourceAssociation), ctx, source, resourceType, resourceId, relatedSource, relatedResourceType, relatedResourceId)
|
||||
}
|
||||
|
||||
// SaveUserSettings mocks base method.
|
||||
func (m *MockDatabaseRepository) SaveUserSettings(arg0 context.Context, arg1 *models0.UserSettings) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "SaveUserSettings", arg0, arg1)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// SaveUserSettings indicates an expected call of SaveUserSettings.
|
||||
func (mr *MockDatabaseRepositoryMockRecorder) SaveUserSettings(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveUserSettings", reflect.TypeOf((*MockDatabaseRepository)(nil).SaveUserSettings), arg0, arg1)
|
||||
}
|
||||
|
||||
// UpdateSource mocks base method.
|
||||
func (m *MockDatabaseRepository) UpdateSource(ctx context.Context, sourceCreds *models0.SourceCredential) error {
|
||||
m.ctrl.T.Helper()
|
||||
|
|
|
@ -3,11 +3,8 @@ package database
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/fastenhealth/fastenhealth-onprem/backend/pkg/config"
|
||||
"github.com/fastenhealth/fastenhealth-onprem/backend/pkg/models"
|
||||
"github.com/google/uuid"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// LoadSettings will retrieve settings from the database, store them in the AppConfig object, and return a Settings struct
|
||||
|
@ -27,79 +24,46 @@ func (sr *SqliteRepository) LoadUserSettings(ctx context.Context) (*models.UserS
|
|||
return nil, fmt.Errorf("Could not get settings from DB: %v", err)
|
||||
}
|
||||
|
||||
// store retrieved settings in the AppConfig obj
|
||||
settings := models.UserSettings{}
|
||||
for _, settingsEntry := range settingsEntries {
|
||||
configKey := fmt.Sprintf("%s.%s", config.DB_USER_SETTINGS_SUBKEY, settingsEntry.SettingKeyName)
|
||||
|
||||
if settingsEntry.SettingDataType == "numeric" {
|
||||
sr.AppConfig.SetDefault(configKey, settingsEntry.SettingValueNumeric)
|
||||
} else if settingsEntry.SettingDataType == "string" {
|
||||
sr.AppConfig.SetDefault(configKey, settingsEntry.SettingValueString)
|
||||
} else if settingsEntry.SettingDataType == "bool" {
|
||||
sr.AppConfig.SetDefault(configKey, settingsEntry.SettingValueBool)
|
||||
} else if settingsEntry.SettingDataType == "array" {
|
||||
sr.AppConfig.SetDefault(configKey, settingsEntry.SettingValueArray)
|
||||
}
|
||||
}
|
||||
|
||||
// unmarshal the dbsetting object data to a settings object.
|
||||
var settings models.UserSettings
|
||||
err := sr.AppConfig.UnmarshalKey(config.DB_USER_SETTINGS_SUBKEY, &settings)
|
||||
err := settings.FromUserSettingsEntry(&settingsEntry)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("Could not get settings from DB: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return &settings, nil
|
||||
}
|
||||
|
||||
// testing
|
||||
// curl -d '{"metrics": { "notify_level": 5, "status_filter_attributes": 5, "status_threshold": 5 }}' -H "Content-Type: application/json" -X POST http://localhost:9090/api/settings
|
||||
// SaveSettings will update settings in AppConfig object, then save the settings to the database.
|
||||
func (sr *SqliteRepository) SaveUserSettings(ctx context.Context, settings *models.UserSettings) error {
|
||||
func (sr *SqliteRepository) SaveUserSettings(ctx context.Context, newSettings *models.UserSettings) error {
|
||||
currentUser, currentUserErr := sr.GetCurrentUser(ctx)
|
||||
if currentUserErr != nil {
|
||||
return currentUserErr
|
||||
}
|
||||
|
||||
//save the entries to the appconfig
|
||||
settingsMap := &map[string]interface{}{}
|
||||
err := mapstructure.Decode(settings, &settingsMap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
settingsWrapperMap := map[string]interface{}{}
|
||||
settingsWrapperMap[config.DB_USER_SETTINGS_SUBKEY] = *settingsMap
|
||||
err = sr.AppConfig.MergeConfigMap(settingsWrapperMap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sr.Logger.Debugf("after merge settings: %v", sr.AppConfig.AllSettings())
|
||||
//retrieve current settings from the database
|
||||
settingsEntries := []models.UserSettingEntry{}
|
||||
currentSettingsEntries := []models.UserSettingEntry{}
|
||||
|
||||
if err := sr.GormClient.
|
||||
WithContext(ctx).
|
||||
Where(models.UserSettingEntry{
|
||||
UserID: currentUser.ID,
|
||||
}).
|
||||
Find(&settingsEntries).Error; err != nil {
|
||||
Find(¤tSettingsEntries).Error; err != nil {
|
||||
return fmt.Errorf("Could not get settings from DB: %v", err)
|
||||
}
|
||||
|
||||
//update settingsEntries
|
||||
for ndx, settingsEntry := range settingsEntries {
|
||||
configKey := fmt.Sprintf("%s.%s", config.DB_USER_SETTINGS_SUBKEY, strings.ToLower(settingsEntry.SettingKeyName))
|
||||
if !sr.AppConfig.IsSet(configKey) {
|
||||
continue //skip any settings that don't exist in the appconfig
|
||||
|
||||
newSettingsEntries, err := newSettings.ToUserSettingsEntry(currentSettingsEntries)
|
||||
if err != nil {
|
||||
return fmt.Errorf("merge new settings with DB: %v", err)
|
||||
}
|
||||
|
||||
if settingsEntry.SettingDataType == "numeric" {
|
||||
settingsEntries[ndx].SettingValueNumeric = sr.AppConfig.GetInt(configKey)
|
||||
} else if settingsEntry.SettingDataType == "string" {
|
||||
settingsEntries[ndx].SettingValueString = sr.AppConfig.GetString(configKey)
|
||||
} else if settingsEntry.SettingDataType == "bool" {
|
||||
settingsEntries[ndx].SettingValueBool = sr.AppConfig.GetBool(configKey)
|
||||
} else if settingsEntry.SettingDataType == "array" {
|
||||
settingsEntries[ndx].SettingValueArray = sr.AppConfig.GetStringSlice(configKey)
|
||||
}
|
||||
for ndx, settingsEntry := range newSettingsEntries {
|
||||
|
||||
// store in database.
|
||||
//TODO: this should be `sr.gormClient.Updates(&settingsEntries).Error`
|
||||
|
@ -107,8 +71,8 @@ func (sr *SqliteRepository) SaveUserSettings(ctx context.Context, settings *mode
|
|||
WithContext(ctx).
|
||||
Model(&models.UserSettingEntry{}).
|
||||
Where([]uuid.UUID{settingsEntry.ID}).
|
||||
Select("setting_value_numeric", "setting_value_string", "setting_value_bool").
|
||||
Updates(settingsEntries[ndx]).Error
|
||||
Select("setting_value_numeric", "setting_value_string", "setting_value_bool", "setting_value_array").
|
||||
Updates(newSettingsEntries[ndx]).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -123,7 +87,7 @@ func (sr *SqliteRepository) PopulateDefaultUserSettings(ctx context.Context, use
|
|||
settingsEntries = append(settingsEntries, models.UserSettingEntry{
|
||||
UserID: userId,
|
||||
SettingKeyName: "dashboard_locations",
|
||||
SettingKeyDescription: "customized dashboard json locations",
|
||||
SettingKeyDescription: "remote dashboard locations (github gists)",
|
||||
SettingDataType: "array",
|
||||
SettingValueArray: []string{},
|
||||
})
|
||||
|
|
|
@ -0,0 +1,103 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/fastenhealth/fastenhealth-onprem/backend/pkg"
|
||||
"github.com/fastenhealth/fastenhealth-onprem/backend/pkg/config"
|
||||
"github.com/fastenhealth/fastenhealth-onprem/backend/pkg/models"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Define the suite, and absorb the built-in basic suite
|
||||
// functionality from testify - including a T() method which
|
||||
// returns the current testing context
|
||||
type RepositorySettingsTestSuite struct {
|
||||
suite.Suite
|
||||
TestDatabase *os.File
|
||||
TestConfig config.Interface
|
||||
|
||||
TestRepository DatabaseRepository
|
||||
TestUser *models.User
|
||||
}
|
||||
|
||||
// BeforeTest has a function to be executed right before the test starts and receives the suite and test names as input
|
||||
func (suite *RepositorySettingsTestSuite) BeforeTest(suiteName, testName string) {
|
||||
|
||||
dbFile, err := ioutil.TempFile("", fmt.Sprintf("%s.*.db", testName))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
suite.TestDatabase = dbFile
|
||||
|
||||
testConfig, err := config.Create()
|
||||
require.NoError(suite.T(), err)
|
||||
testConfig.SetDefault("database.location", suite.TestDatabase.Name())
|
||||
testConfig.SetDefault("log.level", "INFO")
|
||||
suite.TestConfig = testConfig
|
||||
|
||||
dbRepo, err := NewRepository(testConfig, logrus.WithField("test", suite.T().Name()))
|
||||
require.NoError(suite.T(), err)
|
||||
suite.TestRepository = dbRepo
|
||||
userModel := &models.User{
|
||||
Username: "test_username",
|
||||
Password: "testpassword",
|
||||
Email: "test@test.com",
|
||||
}
|
||||
err = suite.TestRepository.CreateUser(context.Background(), userModel)
|
||||
suite.TestUser = userModel
|
||||
require.NoError(suite.T(), err)
|
||||
|
||||
}
|
||||
|
||||
// AfterTest has a function to be executed right after the test finishes and receives the suite and test names as input
|
||||
func (suite *RepositorySettingsTestSuite) AfterTest(suiteName, testName string) {
|
||||
os.Remove(suite.TestDatabase.Name())
|
||||
}
|
||||
|
||||
// In order for 'go test' to run this suite, we need to create
|
||||
// a normal test function and pass our suite to suite.Run
|
||||
func TestRepositorySettingsTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(RepositorySettingsTestSuite))
|
||||
|
||||
}
|
||||
|
||||
func (suite *RepositorySettingsTestSuite) TestLoadUserSettings() {
|
||||
//setup
|
||||
authContext := context.WithValue(context.Background(), pkg.ContextKeyTypeAuthUsername, "test_username")
|
||||
|
||||
//test
|
||||
userSettings, err := suite.TestRepository.LoadUserSettings(authContext)
|
||||
require.NoError(suite.T(), err)
|
||||
|
||||
//assert
|
||||
require.Equal(suite.T(), userSettings, &models.UserSettings{
|
||||
DashboardLocations: []string{},
|
||||
})
|
||||
}
|
||||
|
||||
func (suite *RepositorySettingsTestSuite) TestSaveUserSettings() {
|
||||
//setup
|
||||
authContext := context.WithValue(context.Background(), pkg.ContextKeyTypeAuthUsername, "test_username")
|
||||
|
||||
//test
|
||||
err := suite.TestRepository.SaveUserSettings(authContext, &models.UserSettings{
|
||||
DashboardLocations: []string{"https://gist.github.com/AnalogJ/a56ded05cc6766b377268f14719cb84d"},
|
||||
})
|
||||
require.NoError(suite.T(), err)
|
||||
userSettings, err := suite.TestRepository.LoadUserSettings(authContext)
|
||||
require.NoError(suite.T(), err)
|
||||
|
||||
//assert
|
||||
require.Equal(suite.T(), userSettings, &models.UserSettings{
|
||||
DashboardLocations: []string{
|
||||
"https://gist.github.com/AnalogJ/a56ded05cc6766b377268f14719cb84d",
|
||||
},
|
||||
})
|
||||
}
|
|
@ -1,5 +1,69 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
)
|
||||
|
||||
type UserSettings struct {
|
||||
DashboardLocations []string `json:"dashboard_locations" mapstructure:"dashboard_locations"`
|
||||
DashboardLocations []string `json:"dashboard_locations"`
|
||||
}
|
||||
|
||||
// see https://gist.github.com/lelandbatey/a5c957b537bed39d1d6fb202c3b8de06
|
||||
func (s *UserSettings) FromUserSettingsEntry(entry *UserSettingEntry) error {
|
||||
|
||||
structType := reflect.ValueOf(s).Elem()
|
||||
|
||||
for i := 0; i < structType.NumField(); i++ {
|
||||
typeField := structType.Type().Field(i)
|
||||
|
||||
if jsonTagValue := typeField.Tag.Get("json"); jsonTagValue == entry.SettingKeyName {
|
||||
//fmt.Println("found field", field.Name)
|
||||
if entry.SettingDataType == "numeric" {
|
||||
structType.Field(i).SetInt(int64(entry.SettingValueNumeric))
|
||||
} else if entry.SettingDataType == "string" {
|
||||
structType.Elem().Field(i).SetString(entry.SettingValueString)
|
||||
} else if entry.SettingDataType == "bool" {
|
||||
structType.Field(i).SetBool(entry.SettingValueBool)
|
||||
} else if entry.SettingDataType == "array" {
|
||||
structType.Field(i).Set(reflect.ValueOf(entry.SettingValueArray))
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
//if entry.SettingKeyName == "dashboard_locations" {
|
||||
// s.DashboardLocations = entry.SettingValueArray
|
||||
//}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *UserSettings) ToUserSettingsEntry(entries []UserSettingEntry) ([]UserSettingEntry, error) {
|
||||
|
||||
structType := reflect.ValueOf(s).Elem()
|
||||
|
||||
fieldNameNdxLookup := map[string]int{}
|
||||
|
||||
for i := 0; i < structType.NumField(); i++ {
|
||||
typeField := structType.Type().Field(i)
|
||||
jsonTagValue := typeField.Tag.Get("json")
|
||||
fieldNameNdxLookup[jsonTagValue] = i
|
||||
}
|
||||
|
||||
for ndx, entry := range entries {
|
||||
fieldId := fieldNameNdxLookup[entry.SettingKeyName]
|
||||
|
||||
if entry.SettingDataType == "numeric" {
|
||||
entries[ndx].SettingValueNumeric = int(structType.Field(fieldId).Int())
|
||||
} else if entry.SettingDataType == "string" {
|
||||
entries[ndx].SettingValueString = structType.Elem().Field(fieldId).String()
|
||||
} else if entry.SettingDataType == "bool" {
|
||||
entries[ndx].SettingValueBool = structType.Field(fieldId).Bool()
|
||||
} else if entry.SettingDataType == "array" {
|
||||
sliceVal := structType.Field(fieldId).Slice(0, structType.Field(fieldId).Len())
|
||||
|
||||
entries[ndx].SettingValueArray = sliceVal.Interface().([]string)
|
||||
}
|
||||
}
|
||||
|
||||
return entries, nil
|
||||
}
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/require"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFromUserSettingsEntry(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
//setup
|
||||
userSettings := new(UserSettings)
|
||||
userSettingsEntry := UserSettingEntry{
|
||||
SettingKeyName: "dashboard_locations",
|
||||
SettingDataType: "array",
|
||||
SettingValueArray: []string{"a", "b", "c"},
|
||||
}
|
||||
|
||||
//test
|
||||
err := userSettings.FromUserSettingsEntry(&userSettingsEntry)
|
||||
|
||||
//assert
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []string{"a", "b", "c"}, userSettings.DashboardLocations)
|
||||
}
|
||||
|
||||
func TestToUserSettingsEntry(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
//setup
|
||||
userSettings := new(UserSettings)
|
||||
previousUserSettingsEntries := []UserSettingEntry{{
|
||||
ModelBase: ModelBase{
|
||||
ID: uuid.MustParse("73057947-af24-4739-a4af-ca3496f85b76"),
|
||||
},
|
||||
SettingKeyName: "dashboard_locations",
|
||||
SettingDataType: "array",
|
||||
SettingValueArray: []string{"a", "b", "c"},
|
||||
}}
|
||||
|
||||
//test
|
||||
userSettings.DashboardLocations = []string{"d", "e", "f"}
|
||||
updatedUserSettingsEntries, err := userSettings.ToUserSettingsEntry(previousUserSettingsEntries)
|
||||
|
||||
//assert
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []UserSettingEntry{{
|
||||
ModelBase: ModelBase{
|
||||
ID: uuid.MustParse("73057947-af24-4739-a4af-ca3496f85b76"),
|
||||
},
|
||||
SettingKeyName: "dashboard_locations",
|
||||
SettingDataType: "array",
|
||||
SettingValueArray: []string{"d", "e", "f"},
|
||||
}}, updatedUserSettingsEntries)
|
||||
}
|
|
@ -49,7 +49,7 @@
|
|||
<strong>Description:</strong> {{dashboardConfigs?.[selectedDashboardConfigNdx]?.description}}
|
||||
<br/>
|
||||
<span *ngIf="dashboardConfigs?.[selectedDashboardConfigNdx]?.source">
|
||||
<strong>Source:</strong> <a [href]="dashboardConfigs?.[selectedDashboardConfigNdx]?.source" target="_blank">{{dashboardConfigs?.[selectedDashboardConfigNdx]?.source}}</a>
|
||||
<strong>Source:</strong> <a [href]="dashboardConfigs?.[selectedDashboardConfigNdx]?.source" target="_blank"> {{dashboardConfigs?.[selectedDashboardConfigNdx]?.source}}</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
Loading…
Reference in New Issue