diff --git a/.goreleaser.yml b/.goreleaser.yml index 427cf08e..7fb64865 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -10,7 +10,7 @@ builds: - env: - CGO_ENABLED=0 binary: gdg - ldflags: -X github.com/esnet/gdg/version.GitCommit={{ .Commit }} -X github.com/esnet/gdg/version.BuildDate={{ .Date }} -X github.com/esnet/gdg/version.Version={{ .Tag }} + ldflags: -s -w -X github.com/esnet/gdg/version.GitCommit={{ .Commit }} -X github.com/esnet/gdg/version.BuildDate={{ .Date }} -X github.com/esnet/gdg/version.Version={{ .Tag }} goos: - linux - windows diff --git a/Makefile b/Makefile index a830fa24..e6a74314 100644 --- a/Makefile +++ b/Makefile @@ -27,7 +27,7 @@ authors: linux: clean - env GOOS='linux' GOARCH='amd64' go build -ldflags "-X github.com/esnet/gdg/version.GitCommit=${GIT_COMMIT}${GIT_DIRTY} -X github.com/esnet/gdg/version.BuildDate=${BUILD_DATE}" -o bin/${BIN_NAME}_linux + env GOOS='linux' GOARCH='amd64' go build -ldflags "-s -w -X github.com/esnet/gdg/version.GitCommit=${GIT_COMMIT}${GIT_DIRTY} -X github.com/esnet/gdg/version.BuildDate=${BUILD_DATE}" -o bin/${BIN_NAME}_linux help: @echo 'Management commands for gdg:' @@ -48,12 +48,12 @@ help: build: @echo "building ${BIN_NAME} ${VERSION}" @echo "GOPATH=${GOPATH}" - go build -ldflags "-X github.com/esnet/gdg/version.GitCommit=${GIT_COMMIT}${GIT_DIRTY} -X github.com/esnet/gdg/version.BuildDate=${BUILD_DATE}" -o bin/${BIN_NAME} + go build -ldflags "-s -w -X github.com/esnet/gdg/version.GitCommit=${GIT_COMMIT}${GIT_DIRTY} -X github.com/esnet/gdg/version.BuildDate=${BUILD_DATE}" -o bin/${BIN_NAME} install: @echo "installing ${BIN_NAME} ${VERSION}" @echo "GOPATH=${GOPATH}" - go install -ldflags "-X github.com/esnet/gdg/version.GitCommit=${GIT_COMMIT}${GIT_DIRTY} -X github.com/esnet/gdg/version.BuildDate=${BUILD_DATE}" + go install -ldflags "-s -w -X github.com/esnet/gdg/version.GitCommit=${GIT_COMMIT}${GIT_DIRTY} -X github.com/esnet/gdg/version.BuildDate=${BUILD_DATE}" mv ${GOPATH}/bin/gdg ${GOPATH}/bin/${BIN_NAME} get-deps: @@ -62,7 +62,7 @@ get-deps: build-alpine: @echo "building ${BIN_NAME} ${VERSION}" @echo "GOPATH=${GOPATH}" - go build -ldflags '-w -linkmode external -extldflags "-static" -X github.com/esnet/gdg/version.GitCommit=${GIT_COMMIT}${GIT_DIRTY} -X github.com/esnet/gdg/version.BuildDate=${BUILD_DATE}' -o bin/${BIN_NAME} + go build -ldflags '-s -w -w -linkmode external -extldflags "-static" -X github.com/esnet/gdg/version.GitCommit=${GIT_COMMIT}${GIT_DIRTY} -X github.com/esnet/gdg/version.BuildDate=${BUILD_DATE}' -o bin/${BIN_NAME} package: @echo "building image ${BIN_NAME} ${VERSION} $(GIT_COMMIT)" diff --git a/README.md b/README.md index e70ce4d9..646b1cdb 100644 --- a/README.md +++ b/README.md @@ -13,38 +13,7 @@ Please find the pretty documentation [here](https://software.es.net/gdg/docs/usa ## Breaking Changes -ChangeLog: 0.4.X - -This is a major change from the previous one, I'll likely cut the 1.x soon and start following the more typical Semver conventions. - -### API SDK Changes: - -I have been trying to find a proper library to use so I'm not re-writing and reinventing the wheel so to speak. - -For reference, here are all the current "active" (active can be a relative term for some of these project) development I'm aware of. - - - https://github.com/grafana-tools/sdk Initial version of GDG was based on this project. It mostly works but getting any PRS accepted can be tedious and it's needs some help. - - https://github.com/grafana/grafana-api-golang-client Owned by the Grafana Org which is nice, but it has a slightly different goal. It's primary goal is to support the terraform provider for Grafana. I also found some endpoints missing very early on. So decided not to go with it. - - Swagger Based: There's a branch that I've been keeping an eye on. https://github.com/grafana/grafana-api-golang-client/tree/papagian/generate-client-from-swagger which makes an effort to generate code based on the swagger manifest that's available from Grafana. It's a mostly automated code that pulls data from the [Schema](https://github.com/grafana/grafana/blob/main/public/api-merged.json) and generates the underlying code. It hasn't had much traction of late so I ended up forking the project currently available [here](https://github.com/esnet/grafana-swagger-api-golang) - -Final Choice: - -Although the Swagger/OpenAPI based version is not great, I've even ran into a few issues where the documented response -does not match the result, it's a lot more encompassing and allows further development without being as limited on upstream changes. - -## DataModel Changes - -I've tried to utilize mostly the same endpoints to recreate the same behavior for all the various entities, but there -is are some changes. For most use cases this shouldn't matter. But you have been officially warned. - -## Cloud Support - -The previous abstraction library used to provide S3, GS, SFTP has limited activity and introduced some security vulnerabilities. 0.4.X also -changes some of the cloud behavior. It relies on the system authentication rather than having the auth in the config file. - -Please see the related docs on how to configure your environment. - -As the Stow library was removed, SFTP has been dropped. The list of current supported cloud providers are: S3, GS, Azure. +Release 0.4 has some breaking changes, please read the release (notes)[https://software.es.net/gdg/docs/releases/gdg_0.4.0/] ## Quickstart diff --git a/api/alertnotifications.go b/api/alertnotifications.go index 8a6a7a26..f860ff7c 100644 --- a/api/alertnotifications.go +++ b/api/alertnotifications.go @@ -16,7 +16,6 @@ import ( // AlertNotificationsApi Contract definition // Deprecated: Marked as Deprecated as of Grafana 9.0, Moving to ContactPoints is recommended type AlertNotificationsApi interface { - //AlertNotifications ListAlertNotifications() []*models.AlertNotification ImportAlertNotifications() []string ExportAlertNotifications() []string diff --git a/api/common.go b/api/common.go index ad5e5865..54fb0f51 100644 --- a/api/common.go +++ b/api/common.go @@ -3,7 +3,6 @@ package api import ( "fmt" "github.com/esnet/gdg/config" - "github.com/esnet/grafana-swagger-api-golang/goclient/models" "os" "path/filepath" "strings" @@ -14,8 +13,10 @@ import ( ) var ( - DefaultFolderName = "General" - DefaultFolderId = int64(0) + DefaultFolderName = "General" + DefaultFolderId = int64(0) + searchTypeDashboard = "dash-db" + searchTypeFolder = "dash-folder" ) func GetSlug(title string) string { @@ -56,12 +57,3 @@ func buildResourcePath(folderName string, resourceType config.ResourceType) stri return v } - -// getFolderNameIDMap helper function to build a mapping for name to folderID -func getFolderNameIDMap(folders []*models.FolderSearchHit) map[string]int64 { - var folderMap = make(map[string]int64) - for _, folder := range folders { - folderMap[folder.Title] = folder.ID - } - return folderMap -} diff --git a/api/contract.go b/api/contract.go index 9715e5dc..815b14f5 100644 --- a/api/contract.go +++ b/api/contract.go @@ -20,6 +20,7 @@ type ApiService interface { AlertNotificationsApi UsersApi FoldersApi + LibraryElementsApi //MetaData GetServerInfo() map[string]interface{} diff --git a/api/dashboards.go b/api/dashboards.go index 7ca7658d..1f8723ca 100644 --- a/api/dashboards.go +++ b/api/dashboards.go @@ -3,6 +3,7 @@ package api import ( "encoding/json" "fmt" + "github.com/esnet/gdg/api/filters" "github.com/esnet/gdg/config" gapi "github.com/esnet/grafana-swagger-api-golang" "github.com/esnet/grafana-swagger-api-golang/goclient/client/dashboards" @@ -14,6 +15,7 @@ import ( "golang.org/x/exp/slices" "os" "path/filepath" + "regexp" "strings" "github.com/esnet/gdg/apphelpers" @@ -26,15 +28,88 @@ import ( // DashboardsApi Contract definition type DashboardsApi interface { - ListDashboards(filter Filter) []*models.Hit - ImportDashboards(filter Filter) []string - ExportDashboards(filter Filter) - DeleteAllDashboards(filter Filter) []string + ListDashboards(filter filters.Filter) []*models.Hit + ImportDashboards(filter filters.Filter) []string + ExportDashboards(filter filters.Filter) + DeleteAllDashboards(filter filters.Filter) []string +} + +func NewDashboardFilter(entries ...string) filters.Filter { + if len(entries) != 3 { + log.Fatal("Unable to create a valid Dashboard Filter, aborting.") + } + folderFilter := entries[0] + dashboardFilter := entries[1] + tagsFilter := entries[2] + + filterObj := filters.NewBaseFilter() + filterObj.AddFilter(filters.FolderFilter, folderFilter) + filterObj.AddFilter(filters.DashFilter, dashboardFilter) + filterObj.AddFilter(filters.TagsFilter, tagsFilter) + quoteRegex, _ := regexp.Compile("['\"]+") + filterObj.AddRegex(filters.TagsFilter, quoteRegex) + filterObj.AddRegex(filters.FolderFilter, quoteRegex) + //Add Folder Validation + filterObj.AddValidation(filters.FolderFilter, func(i interface{}) bool { + val, ok := i.(map[filters.FilterType]string) + if !ok { + return ok + } + //Check folder + if folderFilter, ok = val[filters.FolderFilter]; ok { + if filterObj.GetFilter(filters.FolderFilter) == "" { + return true + } else { + return folderFilter == filterObj.GetFilter(filters.FolderFilter) + } + } else { + return true + } + }) + + //Add Tag Validation + filterObj.AddValidation(filters.TagsFilter, func(i interface{}) bool { + val, ok := i.(map[filters.FilterType]string) + if !ok { + return ok + } + + //Check Tags + if tagsFilter, ok = val[filters.TagsFilter]; ok { + if filterObj.GetFilter(filters.TagsFilter) == "" { + return true + } + return tagsFilter == filterObj.GetFilter(filters.TagsFilter) + } else { + return true + } + //Check Dashboard + + }) + //Add DashValidation + filterObj.AddValidation(filters.DashFilter, func(i interface{}) bool { + val, ok := i.(map[filters.FilterType]string) + if !ok { + return ok + } + + if dashboardFilter, ok = val[filters.DashFilter]; ok { + if filterObj.GetFilter(filters.DashFilter) == "" { + return true + } + return dashboardFilter == filterObj.GetFilter(filters.DashFilter) + } else { + return true + } + + }) + + return filterObj } // ListDashboards List all dashboards optionally filtered by folder name. If folderFilters // is blank, defaults to the configured Monitored folders -func (s *DashNGoImpl) ListDashboards(filters Filter) []*models.Hit { +func (s *DashNGoImpl) ListDashboards(filterReq filters.Filter) []*models.Hit { var orgsPayload []*models.OrgDTO orgList, err := s.client.Orgs.SearchOrgs(orgs.NewSearchOrgsParams(), s.getAdminAuth()) @@ -63,8 +138,8 @@ func (s *DashNGoImpl) ListDashboards(filters Filter) []*models.Hit { } // Fallback on defaults - if filters == nil { - filters = &DashboardFilter{} + if filterReq == nil { + filterReq = NewDashboardFilter("", "", "") } var boardsList = make([]*models.Hit, 0) @@ -75,7 +150,7 @@ func (s *DashNGoImpl) ListDashboards(filters Filter) []*models.Hit { var tagsParams = make([]string, 0) if !apphelpers.GetCtxDefaultGrafanaConfig().GetFilterOverrides().IgnoreDashboardFilters { - tagsParams = append(tagsParams, filters.GetTags()...) + tagsParams = append(tagsParams, filterReq.GetEntity(filters.TagsFilter)...) } for { @@ -83,7 +158,7 @@ func (s *DashNGoImpl) ListDashboards(filters Filter) []*models.Hit { searchParams.Tag = tagsParams searchParams.Limit = gapi.ToPtr(int64(limit)) searchParams.Page = gapi.ToPtr(int64(page)) - searchParams.Type = gapi.ToPtr("dash-db") + searchParams.Type = gapi.ToPtr(searchTypeDashboard) pageBoardLinks, err := s.client.Search.Search(searchParams, s.getAuth()) if err != nil { @@ -96,7 +171,7 @@ func (s *DashNGoImpl) ListDashboards(filters Filter) []*models.Hit { page += 1 } - folderFilters := filters.GetFolders() + folderFilters := filterReq.GetEntity(filters.FolderFilter) var validFolder bool var validUid bool for _, link := range boardLinks { @@ -113,8 +188,9 @@ func (s *DashNGoImpl) ListDashboards(filters Filter) []*models.Hit { continue } link.Slug = updateSlug(link.URI) - validUid = filters.GetFilter("DashFilter") == "" || link.Slug == filters.GetFilter("DashFilter") + validUid = filterReq.GetFilter(filters.DashFilter) == "" || link.Slug == filterReq.GetFilter(filters.DashFilter) if link.FolderID == 0 { + link.FolderTitle = DefaultFolderName } @@ -128,7 +204,7 @@ func (s *DashNGoImpl) ListDashboards(filters Filter) []*models.Hit { } // ImportDashboards saves all dashboards matching query to configured location -func (s *DashNGoImpl) ImportDashboards(filter Filter) []string { +func (s *DashNGoImpl) ImportDashboards(filter filters.Filter) []string { var ( boardLinks []*models.Hit rawBoard []byte @@ -180,7 +256,7 @@ func (s *DashNGoImpl) createdFolder(folderName string) (int64, error) { // ExportDashboards finds all the dashboards in the configured location and exports them to grafana. // if the folder doesn't exist, it'll be created. -func (s *DashNGoImpl) ExportDashboards(filters Filter) { +func (s *DashNGoImpl) ExportDashboards(filterReq filters.Filter) { var ( rawBoard []byte @@ -193,23 +269,19 @@ func (s *DashNGoImpl) ExportDashboards(filters Filter) { log.WithError(err).Fatal("unable to find any files to export from storage engine") } //Delete all dashboards that match prior to import - s.DeleteAllDashboards(filters) + s.DeleteAllDashboards(filterReq) - folderMap := getFolderNameIDMap(s.ListFolder(nil)) + folderMap := getFolderNameIDMap(s.ListFolder(NewFolderFilter())) // Fallback on defaults - if filters == nil { - filters = &DashboardFilter{} + if filterReq == nil { + filterReq = NewDashboardFilter("", "", "") } - validFolders := filters.GetFolders() + validFolders := filterReq.GetEntity(filters.FolderFilter) for _, file := range filesInDir { baseFile := filepath.Base(file) baseFile = strings.ReplaceAll(baseFile, ".json", "") - validateMap := map[string]string{FolderFilter: folderName, DashFilter: baseFile} - //If folder OR slug is filtered, then skip if it doesn't match - if !filters.Validate(validateMap) { - continue - } + if !strings.HasSuffix(file, ".json") { log.Warnf("Only json files are supported, skipping %s", file) continue @@ -237,6 +309,11 @@ func (s *DashNGoImpl) ExportDashboards(filters Filter) { log.Debugf("Skipping file %s, doesn't match any valid folders", file) continue } + validateMap := map[filters.FilterType]string{filters.FolderFilter: folderName, filters.DashFilter: baseFile} + //If folder OR slug is filtered, then skip if it doesn't match + if !filterReq.ValidateAll(validateMap) { + continue + } if folderName == DefaultFolderName { folderId = DefaultFolderId @@ -244,7 +321,7 @@ func (s *DashNGoImpl) ExportDashboards(filters Filter) { if val, ok := folderMap[folderName]; ok { folderId = val } else { - if filters.Validate(validateMap) { + if filterReq.ValidateAll(validateMap) { id, folderErr := s.createdFolder(folderName) if folderErr != nil { log.Panic("Unable to create required folder") @@ -278,12 +355,12 @@ func (s *DashNGoImpl) ExportDashboards(filters Filter) { // DeleteAllDashboards clears all current dashboards being monitored. Any folder not white listed // will not be affected -func (s *DashNGoImpl) DeleteAllDashboards(filter Filter) []string { +func (s *DashNGoImpl) DeleteAllDashboards(filter filters.Filter) []string { var dashboardListing = make([]string, 0) items := s.ListDashboards(filter) for _, item := range items { - if filter.Validate(map[string]string{FolderFilter: item.FolderTitle, DashFilter: item.Slug}) { + if filter.ValidateAll(map[filters.FilterType]string{filters.FolderFilter: item.FolderTitle, filters.DashFilter: item.Slug}) { dp := dashboards.NewDeleteDashboardByUIDParams() dp.UID = item.UID _, err := s.client.Dashboards.DeleteDashboardByUID(dp, s.getAuth()) diff --git a/api/datasources.go b/api/datasources.go index 695c3e40..ad471895 100644 --- a/api/datasources.go +++ b/api/datasources.go @@ -3,6 +3,7 @@ package api import ( "encoding/json" "fmt" + "github.com/esnet/gdg/api/filters" "github.com/esnet/gdg/apphelpers" "github.com/esnet/grafana-swagger-api-golang/goclient/client/datasources" "github.com/esnet/grafana-swagger-api-golang/goclient/models" @@ -17,15 +18,34 @@ import ( // DataSourcesApi Contract definition type DataSourcesApi interface { - ListDataSources(filter Filter) []models.DataSourceListItemDTO - ImportDataSources(filter Filter) []string - ExportDataSources(filter Filter) []string - DeleteAllDataSources(filter Filter) []string + ListDataSources(filter filters.Filter) []models.DataSourceListItemDTO + ImportDataSources(filter filters.Filter) []string + ExportDataSources(filter filters.Filter) []string + DeleteAllDataSources(filter filters.Filter) []string +} + +//Filters + +func NewDataSourceFilter(name string) filters.Filter { + filterEntity := filters.NewBaseFilter() + filterEntity.AddFilter("Name", name) + filterEntity.AddValidation(filters.DefaultFilter, func(i interface{}) bool { + val, ok := i.(map[filters.FilterType]string) + if !ok { + return ok + } + if filterEntity.GetFilter(filters.Name) == "" { + return true + } + return val[filters.Name] == filterEntity.GetFilter(filters.Name) + }) + + return filterEntity } // ListDataSources: list all the currently configured datasources -func (s *DashNGoImpl) ListDataSources(filter Filter) []models.DataSourceListItemDTO { +func (s *DashNGoImpl) ListDataSources(filter filters.Filter) []models.DataSourceListItemDTO { ds, err := s.client.Datasources.GetDataSources(datasources.NewGetDataSourcesParams(), s.getAuth()) if err != nil { panic(err) @@ -38,7 +58,7 @@ func (s *DashNGoImpl) ListDataSources(filter Filter) []models.DataSourceListItem log.Debugf("Skipping data source: %s since it fails filter checks with dataType of: %s", item.Name, item.Type) continue } - if filter.Validate(map[string]string{Name: GetSlug(item.Name)}) { + if filter.ValidateAll(map[filters.FilterType]string{filters.Name: GetSlug(item.Name)}) { result = append(result, *item) } } @@ -48,7 +68,7 @@ func (s *DashNGoImpl) ListDataSources(filter Filter) []models.DataSourceListItem // ImportDataSources: will read in all the configured datasources. // NOTE: credentials cannot be retrieved and need to be set via configuration -func (s *DashNGoImpl) ImportDataSources(filter Filter) []string { +func (s *DashNGoImpl) ImportDataSources(filter filters.Filter) []string { var ( dsListing []models.DataSourceListItemDTO dsPacked []byte @@ -74,7 +94,7 @@ func (s *DashNGoImpl) ImportDataSources(filter Filter) []string { } // Removes all current datasources -func (s *DashNGoImpl) DeleteAllDataSources(filter Filter) []string { +func (s *DashNGoImpl) DeleteAllDataSources(filter filters.Filter) []string { var ds []string = make([]string, 0) items := s.ListDataSources(filter) for _, item := range items { @@ -92,7 +112,7 @@ func (s *DashNGoImpl) DeleteAllDataSources(filter Filter) []string { } // ExportDataSources: exports all datasources to grafana using the credentials configured in config file. -func (s *DashNGoImpl) ExportDataSources(filter Filter) []string { +func (s *DashNGoImpl) ExportDataSources(filter filters.Filter) []string { var dsListing []models.DataSourceListItemDTO var exported []string = make([]string, 0) @@ -122,7 +142,7 @@ func (s *DashNGoImpl) ExportDataSources(filter Filter) []string { continue } - if !filter.Validate(map[string]string{Name: GetSlug(newDS.Name)}) { + if !filter.ValidateAll(map[filters.FilterType]string{filters.Name: GetSlug(newDS.Name)}) { continue } dsConfig := s.grafanaConf diff --git a/api/filters.go b/api/filters.go deleted file mode 100644 index e1ea26c5..00000000 --- a/api/filters.go +++ /dev/null @@ -1,164 +0,0 @@ -package api - -import ( - "regexp" - "strings" - - "github.com/esnet/gdg/apphelpers" - "github.com/thoas/go-funk" -) - -// Currently supported filters -const ( - TagsFilter = "TagsFilter" - DashFilter = "DashFilter" - FolderFilter = "FolderFilter" - Name = "Name" -) - -type Filter interface { - GetTypes() []string //Return list of active Filter Types - GetFilter(key string) string //Get the Filter value - AddFilter(key, value string) //Add a filter query - Validate(items map[string]string) bool //Validate if Entry is valid - GetFolders() []string //List of supported folders if Any - GetTags() []string //List of tags -} - -type BaseFilter struct { - Filters map[string]string -} - -// GetTypes returns all the current keys for the configured Filter -func (s BaseFilter) GetTypes() []string { - keys := funk.Keys(s.Filters) - return keys.([]string) -} - -// GetFilter returns the value of the filter -func (s BaseFilter) GetFilter(key string) string { - if val, ok := s.Filters[key]; ok { - return val - } - return "" -} - -// AddFilter adds a filter and the corresponding value -func (s BaseFilter) AddFilter(key, value string) { - s.Filters[key] = value -} - -func (s *BaseFilter) Init() { - s.Filters = make(map[string]string) -} - -type DashboardFilter struct { - quoteRegex *regexp.Regexp - BaseFilter -} - -// NewDashboardFilter creates a new dashboard filter -func NewDashboardFilter() *DashboardFilter { - s := DashboardFilter{} - s.init() - return &s - -} - -func (s *DashboardFilter) init() { - s.BaseFilter.Init() - s.quoteRegex, _ = regexp.Compile("['\"]+") -} - -// GetFolders splits the comma delimited folder list and returns a slice -func (s *DashboardFilter) GetFolders() []string { - if s.GetFilter(FolderFilter) == "" { - return apphelpers.GetCtxDefaultGrafanaConfig().GetMonitoredFolders() - } - folderFilter := s.GetFilter(FolderFilter) - folderFilter = s.quoteRegex.ReplaceAllString(folderFilter, "") - s.AddFilter(FolderFilter, folderFilter) - - return strings.Split(folderFilter, ",") -} - -// GetTags returns a list of all tags to filter on -func (s *DashboardFilter) GetTags() []string { - if s.GetFilter(TagsFilter) == "" { - return []string{} - } - tagsFilter := s.GetFilter(TagsFilter) - tagsFilter = s.quoteRegex.ReplaceAllString(tagsFilter, "") - s.AddFilter(TagsFilter, tagsFilter) - - return strings.Split(tagsFilter, ",") -} - -func (s *DashboardFilter) validateDashboard(dashUid string) bool { - if s.GetFilter(DashFilter) == "" { - return true - } - return dashUid == s.GetFilter(DashFilter) -} - -func (s *DashboardFilter) Validate(items map[string]string) bool { - var folderCheck, tagsCheck, dashboardCheck bool - //Check folder - if folderFilter, ok := items[FolderFilter]; ok { - folderCheck = s.validateFolder(folderFilter) - } else { - folderCheck = true - } - - //check tags - if tagsFilter, ok := items[TagsFilter]; ok { - tagsCheck = s.validateTags(tagsFilter) - } else { - tagsCheck = true - } - - //check Dash - if dashFilter, ok := items[DashFilter]; ok { - dashboardCheck = s.validateDashboard(dashFilter) - } else { - dashboardCheck = true - } - return folderCheck && tagsCheck && dashboardCheck -} - -func (s *DashboardFilter) validateFolder(folder string) bool { - if s.GetFilter(FolderFilter) == "" { - return true - } - return folder == s.GetFilter(FolderFilter) -} - -func (s *DashboardFilter) validateTags(tags string) bool { - if s.GetFilter(TagsFilter) == "" { - return true - } - return tags == s.GetFilter(TagsFilter) -} - -type DatasourceFilter struct { - BaseFilter -} - -// GetFolders return empty list since Folders NA for datasources -func (s DatasourceFilter) GetFolders() []string { - return []string{} -} - -// GetTags return empty list since Tags NA for datasources -func (s DatasourceFilter) GetTags() []string { - return []string{} -} - -// Validate returns true if mapped values are valid -func (s DatasourceFilter) Validate(items map[string]string) bool { - if s.GetFilter(Name) == "" { - return true - } - return items[Name] == s.GetFilter(Name) - -} diff --git a/api/filters/filters.go b/api/filters/filters.go new file mode 100644 index 00000000..033924c4 --- /dev/null +++ b/api/filters/filters.go @@ -0,0 +1,154 @@ +package filters + +import ( + "github.com/esnet/gdg/apphelpers" + log "github.com/sirupsen/logrus" + "github.com/thoas/go-funk" + "regexp" + "strings" +) + +// FilterType Currently supported filters +type FilterType string + +const ( + TagsFilter FilterType = "TagsFilter" + DashFilter FilterType = "DashFilter" + FolderFilter FilterType = "FolderFilter" + DefaultFilter FilterType = "default" + Name FilterType = "Name" +) + +func (s FilterType) String() string { + return string(s) +} + +type Filter interface { + //Regex Tooling + AddRegex(FilterType, *regexp.Regexp) + //Entity filterMap + GetEntity(FilterType) []string // Returns slice of filter values or default value from Config + GetFilter(key FilterType) string //Get the Filter value + AddFilter(key FilterType, value string) //Add a filter to match against for a given type + + ValidateAll(interface{}) bool //ValidateAll if Entry is valid + InvokeValidation(FilterType, interface{}) bool + AddValidation(FilterType, func(interface{}) bool) +} + +// BaseFilter is designed to be fairly generic, there shouldn't be any reason to extend it, but if you have a specialized +// use case feel free to do so. +type BaseFilter struct { + filterMap map[FilterType]string // Matches given field against a given value + validationMethods map[FilterType]func(interface{}) bool // Invokes a function to validate a certain entity type + validationPatterns map[FilterType]*regexp.Regexp +} + +func NewBaseFilter() *BaseFilter { + b := &BaseFilter{} + b.Init() + return b +} + +// Returns the entity filter +func (s *BaseFilter) getRegex(name FilterType) *regexp.Regexp { + return s.validationPatterns[name] +} + +func (s *BaseFilter) AddRegex(name FilterType, pattern *regexp.Regexp) { + if name == "" { + name = DefaultFilter + } + if pattern == nil { + log.Warnf("invalid pattern received, cannot set filter pattern for entity name: %s", name) + return + } + s.validationPatterns[name] = pattern +} + +func (s *BaseFilter) getEntities(name FilterType, defaultVal []string) []string { + if s.GetFilter(name) == "" { + return defaultVal + } + entityFilter := s.GetFilter(name) + //regex + if s.getRegex(name) != nil { + entityFilter = s.getRegex(name).ReplaceAllString(entityFilter, "") + } + s.AddFilter(name, entityFilter) + + return strings.Split(entityFilter, ",") +} + +func (s *BaseFilter) GetEntity(name FilterType) []string { + var defaultResponse []string + if name == "" { + return defaultResponse + } + switch name { + case TagsFilter: + return s.getEntities(TagsFilter, []string{}) + case FolderFilter: + return s.getEntities(FolderFilter, apphelpers.GetCtxDefaultGrafanaConfig().GetMonitoredFolders()) + default: + return defaultResponse + } + +} + +func (s *BaseFilter) AddValidation(name FilterType, f func(interface{}) bool) { + if name == "" { + name = DefaultFilter + } + + s.validationMethods[name] = f + +} + +func (s *BaseFilter) InvokeValidation(name FilterType, i interface{}) bool { + if name == "" { + name = "default" + } + if s.validationMethods != nil && s.validationMethods[name] != nil { + return s.validationMethods[name](i) + } + + return false +} + +// Validate Iterates through all validation checks +func (s *BaseFilter) ValidateAll(items interface{}) bool { + for _, val := range s.validationMethods { + ok := val(items) + if !ok { + return ok + } + } + + return true +} + +// GetTypes returns all the current keys for the configured Filter +func (s *BaseFilter) GetTypes() []string { + keys := funk.Keys(s.filterMap) + return keys.([]string) +} + +// GetFilter returns the value of the filter +func (s *BaseFilter) GetFilter(key FilterType) string { + if val, ok := s.filterMap[key]; ok { + return val + } + return "" +} + +// AddFilter adds a filter and the corresponding value +func (s *BaseFilter) AddFilter(key FilterType, value string) { + s.filterMap[key] = value +} + +func (s *BaseFilter) Init() { + s.filterMap = make(map[FilterType]string) + s.validationMethods = make(map[FilterType]func(interface{}) bool, 0) + s.validationPatterns = make(map[FilterType]*regexp.Regexp) +} diff --git a/api/folders.go b/api/folders.go index 5ad3abf2..11669bb6 100644 --- a/api/folders.go +++ b/api/folders.go @@ -2,12 +2,15 @@ package api import ( "encoding/json" + "github.com/esnet/gdg/api/filters" "github.com/esnet/gdg/apphelpers" "github.com/esnet/gdg/config" "github.com/esnet/grafana-swagger-api-golang/goclient/client/folders" + "github.com/esnet/grafana-swagger-api-golang/goclient/client/search" "github.com/esnet/grafana-swagger-api-golang/goclient/models" "github.com/gosimple/slug" log "github.com/sirupsen/logrus" + "golang.org/x/exp/slices" "os" "path/filepath" "strings" @@ -15,22 +18,57 @@ import ( // FoldersApi Contract definition type FoldersApi interface { - ListFolder(filter Filter) []*models.FolderSearchHit - ImportFolder(filter Filter) []string - ExportFolder(filter Filter) []string - DeleteAllFolder(filter Filter) []string + ListFolder(filter filters.Filter) []*models.Hit + ImportFolder(filter filters.Filter) []string + ExportFolder(filter filters.Filter) []string + DeleteAllFolder(filter filters.Filter) []string } -func (s *DashNGoImpl) ListFolder(filter Filter) []*models.FolderSearchHit { - folderListing, err := s.client.Folders.GetFolders(folders.NewGetFoldersParams(), s.getAuth()) +func NewFolderFilter() filters.Filter { + filterObj := filters.NewBaseFilter() + + //filterObj.AddFilter(filters.FolderFilter, folders) + //Add Folder Validation + filterObj.AddValidation(filters.FolderFilter, func(i interface{}) bool { + val, ok := i.(map[filters.FilterType]string) + if !ok { + return ok + } + //Check folder + if folderFilter, ok := val[filters.FolderFilter]; ok { + return slices.Contains(apphelpers.GetCtxDefaultGrafanaConfig().GetMonitoredFolders(), folderFilter) + } else { + return true + } + }) + return filterObj + +} + +func (s *DashNGoImpl) ListFolder(filter filters.Filter) []*models.Hit { + var result = make([]*models.Hit, 0) + if apphelpers.GetCtxDefaultGrafanaConfig().GetFilterOverrides().IgnoreDashboardFilters { + filter = nil + } + p := search.NewSearchParams() + p.Type = &searchTypeFolder + folderListing, err := s.client.Search.Search(p, s.getAuth()) + folderListing.GetPayload() if err != nil { log.Fatal("unable to retrieve folder list.") } + for ndx, val := range folderListing.GetPayload() { + if filter == nil { + result = append(result, folderListing.GetPayload()[ndx]) + } else if filter.ValidateAll(map[filters.FilterType]string{filters.FolderFilter: val.Title}) { + result = append(result, folderListing.GetPayload()[ndx]) + } + } - return folderListing.Payload + return result } -func (s *DashNGoImpl) ImportFolder(filter Filter) []string { +func (s *DashNGoImpl) ImportFolder(filter filters.Filter) []string { var ( dsPacked []byte err error @@ -53,7 +91,7 @@ func (s *DashNGoImpl) ImportFolder(filter Filter) []string { return dataFiles } -func (s *DashNGoImpl) ExportFolder(filter Filter) []string { +func (s *DashNGoImpl) ExportFolder(filter filters.Filter) []string { var ( result []string rawFolder []byte @@ -101,7 +139,7 @@ func (s *DashNGoImpl) ExportFolder(filter Filter) []string { return result } -func (s *DashNGoImpl) DeleteAllFolder(filter Filter) []string { +func (s *DashNGoImpl) DeleteAllFolder(filter filters.Filter) []string { var result []string folderListing := s.ListFolder(filter) for _, folder := range folderListing { @@ -114,3 +152,21 @@ func (s *DashNGoImpl) DeleteAllFolder(filter Filter) []string { } return result } + +// getFolderNameIDMap helper function to build a mapping for name to folderID +func getFolderNameIDMap(folders []*models.Hit) map[string]int64 { + var folderMap = make(map[string]int64) + for _, folder := range folders { + folderMap[folder.Title] = folder.ID + } + return folderMap +} + +func reverseLookUp[T comparable, Y comparable](m map[T]Y) map[Y]T { + reverse := make(map[Y]T, 0) + for key, val := range m { + reverse[val] = key + } + + return reverse +} diff --git a/api/libraryelements.go b/api/libraryelements.go new file mode 100644 index 00000000..5ad19225 --- /dev/null +++ b/api/libraryelements.go @@ -0,0 +1,194 @@ +package api + +import ( + "encoding/json" + "fmt" + "github.com/esnet/gdg/api/filters" + "github.com/esnet/gdg/apphelpers" + "github.com/esnet/gdg/config" + "github.com/esnet/grafana-swagger-api-golang/goclient/client/library_elements" + "github.com/esnet/grafana-swagger-api-golang/goclient/models" + "github.com/gosimple/slug" + log "github.com/sirupsen/logrus" + "github.com/tidwall/gjson" + "golang.org/x/exp/maps" + "golang.org/x/exp/slices" + "os" + "strings" +) + +type LibraryElementsApi interface { + ListLibraryElements(filter filters.Filter) []*models.LibraryElementDTO + ImportLibraryElements(filter filters.Filter) []string + ExportLibraryElements(filter filters.Filter) []string + DeleteAllLibraryElements(filter filters.Filter) []string +} + +var ( + listLibraryPanels int64 = 1 + listLibraryVars int64 = 2 +) + +func (s *DashNGoImpl) ListLibraryElements(filter filters.Filter) []*models.LibraryElementDTO { + ignoreFilters := apphelpers.GetCtxDefaultGrafanaConfig().GetFilterOverrides().IgnoreDashboardFilters + folderFilter := NewFolderFilter() + if ignoreFilters { + folderFilter = nil + } + + folderNameMap := getFolderNameIDMap(s.ListFolder(folderFilter)) + values := maps.Values(folderNameMap) + var buf = strings.Builder{} + //Check to see if General should be included + //If Ignore Filters OR General is in monitored list, add 0 folder + if (!ignoreFilters && slices.Contains(apphelpers.GetCtxDefaultGrafanaConfig().GetMonitoredFolders(), DefaultFolderName)) || ignoreFilters { + buf.WriteString("0,") + } else { + buf.WriteString("") + } + for _, i := range values { + buf.WriteString(fmt.Sprintf("%d,", i)) + } + folderList := buf.String()[:len(buf.String())-1] + + params := library_elements.NewGetLibraryElementsParams() + params.FolderFilter = &folderList + params.Kind = &listLibraryPanels + libraryElements, err := s.client.LibraryElements.GetLibraryElements(params, s.getAuth()) + if err != nil { + log.WithError(err).Fatal("Unable to list Library Elements") + + } + return libraryElements.GetPayload().Result.Elements +} + +func (s *DashNGoImpl) ImportLibraryElements(filter filters.Filter) []string { + var ( + listing []*models.LibraryElementDTO + dsPacked []byte + err error + dataFiles []string + ) + + folderMap := reverseLookUp(getFolderNameIDMap(s.ListFolder(nil))) + listing = s.ListLibraryElements(filter) + for _, item := range listing { + if dsPacked, err = json.MarshalIndent(item, "", " "); err != nil { + log.Errorf("%s for %s\n", err, item.Name) + continue + } + folderName := DefaultFolderName + + if val, ok := folderMap[item.FolderID]; ok { + folderName = val + } + + libraryPath := fmt.Sprintf("%s/%s.json", buildResourceFolder(folderName, config.LibraryElementResource), slug.Make(item.Name)) + + if err = s.storage.WriteFile(libraryPath, dsPacked, os.FileMode(int(0666))); err != nil { + log.Errorf("%s for %s\n", err, slug.Make(item.Name)) + } else { + dataFiles = append(dataFiles, libraryPath) + } + } + return dataFiles +} + +func (s *DashNGoImpl) ExportLibraryElements(filter filters.Filter) []string { + var ( + exported []string = make([]string, 0) + rawLibraryElement []byte + folderName string + libraryUID string + ) + + log.Infof("Reading files from folder: %s", apphelpers.GetCtxDefaultGrafanaConfig().GetPath(config.LibraryElementResource)) + filesInDir, err := s.storage.FindAllFiles(apphelpers.GetCtxDefaultGrafanaConfig().GetPath(config.LibraryElementResource), true) + + currentLibElements := s.ListLibraryElements(filter) + libMapping := make(map[string]*models.LibraryElementDTO, 0) + //Build a mapping by UID + for ndx, item := range currentLibElements { + libMapping[item.UID] = currentLibElements[ndx] + } + + if err != nil { + log.WithError(err).Errorf("failed to list files in directory for library elements") + } + + for _, file := range filesInDir { + fileLocation := file + if strings.HasSuffix(file, ".json") { + if rawLibraryElement, err = s.storage.ReadFile(fileLocation); err != nil { + log.WithError(err).Errorf("failed to read file: %s", fileLocation) + continue + } + + Results := gjson.GetManyBytes(rawLibraryElement, "meta.folderName", "uid") + + if Results[0].Exists() { + folderName = Results[0].String() + } else { + log.Errorf("Unable to determine folder name of library component, skipping %s", file) + continue + } + //Get UID + if Results[1].Exists() { + libraryUID = Results[1].String() + } else { + log.Errorf("Unable to determine the library panel UID, %s, attempting to export anyways", file) + //continue + } + + if _, ok := libMapping[libraryUID]; ok { + log.Warnf("Library already exists, skipping %s", file) + continue + } + + if !slices.Contains(apphelpers.GetCtxDefaultGrafanaConfig().GetMonitoredFolders(), folderName) { + log.WithField("folder", folderName).WithField("file", file).Warn("Skipping since requested file is not in a folder gdg is configured to manage") + continue + } + var newLibraryRequest models.CreateLibraryElementCommand + + if err = json.Unmarshal(rawLibraryElement, &newLibraryRequest); err != nil { + log.WithError(err).Errorf("failed to unmarshall file: %s", fileLocation) + continue + } + + params := library_elements.NewCreateLibraryElementParams() + params.Body = &newLibraryRequest + entity, err := s.client.LibraryElements.CreateLibraryElement(params, s.getAuth()) + if err != nil { + log.WithError(err).Errorf("Failed to create library element") + } else { + exported = append(exported, entity.Payload.Result.Name) + } + } + } + return exported +} + +func (s *DashNGoImpl) DeleteAllLibraryElements(filter filters.Filter) []string { + var entries []string + libraryElements := s.ListLibraryElements(filter) + for _, element := range libraryElements { + + params := library_elements.NewDeleteLibraryElementByUIDParams() + params.SetLibraryElementUID(element.UID) + _, err := s.client.LibraryElements.DeleteLibraryElementByUID(params, s.getAuth()) + if err != nil { + var logEntry *log.Entry + if serr, ok := err.(*library_elements.DeleteLibraryElementByUIDForbidden); ok { + logEntry = log.WithField("ErrorMessage", *serr.GetPayload().Message) + } else { + log.WithError(err) + } + logEntry.Errorf("Failed to delete library panel titled: %s", element.Name) + continue + } + entries = append(entries, element.Name) + } + + return entries +} diff --git a/cmd/dashboard.go b/cmd/dashboard.go index a3fce29c..93cc4380 100644 --- a/cmd/dashboard.go +++ b/cmd/dashboard.go @@ -10,17 +10,12 @@ import ( "strings" ) -func getDashboardGlobalFlags(cmd *cobra.Command) api.Filter { +func parseDashboardGlobalFlags(cmd *cobra.Command) []string { folderFilter, _ := cmd.Flags().GetString("folder") dashboardFilter, _ := cmd.Flags().GetString("dashboard") tagsFilter, _ := cmd.Flags().GetStringSlice("tags") - filters := api.NewDashboardFilter() - filters.AddFilter(api.FolderFilter, folderFilter) - filters.AddFilter(api.DashFilter, dashboardFilter) - filters.AddFilter(api.TagsFilter, strings.Join(tagsFilter, ",")) - - return filters + return []string{folderFilter, dashboardFilter, strings.Join(tagsFilter, ",")} } var dashboard = &cobra.Command{ @@ -35,7 +30,7 @@ var clearDashboards = &cobra.Command{ Short: "delete all monitored dashboards", Long: `clear all monitored dashboards from grafana`, Run: func(cmd *cobra.Command, args []string) { - filter := getDashboardGlobalFlags(cmd) + filter := api.NewDashboardFilter(parseDashboardGlobalFlags(cmd)...) deletedDashboards := client.DeleteAllDashboards(filter) tableObj.AppendHeader(table.Row{"type", "filename"}) for _, file := range deletedDashboards { @@ -58,7 +53,7 @@ var exportDashboard = &cobra.Command{ Long: `export all dashboards`, Run: func(cmd *cobra.Command, args []string) { - filter := getDashboardGlobalFlags(cmd) + filter := api.NewDashboardFilter(parseDashboardGlobalFlags(cmd)...) client.ExportDashboards(filter) tableObj.AppendHeader(table.Row{"Title", "id", "folder", "UID"}) @@ -82,7 +77,7 @@ var importDashboard = &cobra.Command{ Short: "Import all dashboards", Long: `Import all dashboards from grafana to local file system`, Run: func(cmd *cobra.Command, args []string) { - filter := getDashboardGlobalFlags(cmd) + filter := api.NewDashboardFilter(parseDashboardGlobalFlags(cmd)...) savedFiles := client.ImportDashboards(filter) log.Infof("Importing dashboards for context: '%s'", apphelpers.GetContext()) tableObj.AppendHeader(table.Row{"type", "filename"}) @@ -98,16 +93,16 @@ var listDashboards = &cobra.Command{ Short: "List all dashboards", Long: `List all dashboards`, Run: func(cmd *cobra.Command, args []string) { - tableObj.AppendHeader(table.Row{"id", "Title", "Slug", "Folder", "UID", "URL"}) + tableObj.AppendHeader(table.Row{"id", "Title", "Slug", "Folder", "UID", "Tags", "URL"}) - filters := getDashboardGlobalFlags(cmd) + filters := api.NewDashboardFilter(parseDashboardGlobalFlags(cmd)...) boards := client.ListDashboards(filters) log.Infof("Listing dashboards for context: '%s'", apphelpers.GetContext()) for _, link := range boards { url := fmt.Sprintf("%s%s", apphelpers.GetCtxDefaultGrafanaConfig().URL, link.URL) tableObj.AppendRow(table.Row{link.ID, link.Title, link.Slug, link.FolderTitle, - link.UID, url}) + link.UID, strings.Join(link.Tags, ","), url}) } if len(boards) > 0 { diff --git a/cmd/datasources.go b/cmd/datasources.go index 6250a41e..ff2b294d 100644 --- a/cmd/datasources.go +++ b/cmd/datasources.go @@ -9,18 +9,6 @@ import ( "github.com/spf13/cobra" ) -func getDatasourcesGlobalFlags(cmd *cobra.Command) api.Filter { - dashboardFilter, _ := cmd.Flags().GetString("datasource") - - filters := api.DatasourceFilter{} - filters.Init() - filters.AddFilter("Name", dashboardFilter) - - return filters - -} - -// versionCmd represents the version command var datasources = &cobra.Command{ Use: "datasources", Aliases: []string{"ds", "datasource"}, @@ -34,7 +22,8 @@ var clearDataSources = &cobra.Command{ Long: `clear all datasources from grafana`, Run: func(cmd *cobra.Command, args []string) { log.Info("Delete datasources") - filters := getDatasourcesGlobalFlags(cmd) + dashboardFilter, _ := cmd.Flags().GetString("datasource") + filters := api.NewDataSourceFilter(dashboardFilter) savedFiles := client.DeleteAllDataSources(filters) tableObj.AppendHeader(table.Row{"type", "filename"}) for _, file := range savedFiles { @@ -51,7 +40,8 @@ var exportDataSources = &cobra.Command{ Long: `export all datasources`, Run: func(cmd *cobra.Command, args []string) { log.Info("Exporting datasources") - filters := getDatasourcesGlobalFlags(cmd) + dashboardFilter, _ := cmd.Flags().GetString("datasource") + filters := api.NewDataSourceFilter(dashboardFilter) exportedList := client.ExportDataSources(filters) tableObj.AppendHeader(table.Row{"type", "filename"}) for _, file := range exportedList { @@ -68,7 +58,8 @@ var ImportDataSources = &cobra.Command{ Long: `import all datasources from grafana to local filesystem`, Run: func(cmd *cobra.Command, args []string) { log.Infof("Importing datasources for context: '%s'", apphelpers.GetContext()) - filters := getDatasourcesGlobalFlags(cmd) + dashboardFilter, _ := cmd.Flags().GetString("datasource") + filters := api.NewDataSourceFilter(dashboardFilter) savedFiles := client.ImportDataSources(filters) tableObj.AppendHeader(table.Row{"type", "filename"}) for _, file := range savedFiles { @@ -85,13 +76,14 @@ var listDataSources = &cobra.Command{ Long: `List all dashboards`, Run: func(cmd *cobra.Command, args []string) { tableObj.AppendHeader(table.Row{"id", "uid", "name", "slug", "type", "default", "url"}) - filters := getDatasourcesGlobalFlags(cmd) - datasources := client.ListDataSources(filters) + dashboardFilter, _ := cmd.Flags().GetString("datasource") + filters := api.NewDataSourceFilter(dashboardFilter) + dsListing := client.ListDataSources(filters) log.Infof("Listing datasources for context: '%s'", apphelpers.GetContext()) - if len(datasources) == 0 { + if len(dsListing) == 0 { log.Info("No datasources found") } else { - for _, link := range datasources { + for _, link := range dsListing { url := fmt.Sprintf("%s/datasource/edit/%d", apphelpers.GetCtxDefaultGrafanaConfig().URL, link.ID) tableObj.AppendRow(table.Row{link.ID, link.UID, link.Name, api.GetSlug(link.Name), link.Type, link.IsDefault, url}) } diff --git a/cmd/folders.go b/cmd/folders.go index 57e4d6e9..4d8b5b6c 100644 --- a/cmd/folders.go +++ b/cmd/folders.go @@ -1,12 +1,16 @@ package cmd import ( + "github.com/esnet/gdg/api" + "github.com/esnet/gdg/api/filters" "github.com/esnet/gdg/apphelpers" "github.com/jedib0t/go-pretty/table" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) +var useFolderFilters bool + var foldersCmd = &cobra.Command{ Use: "folders", Aliases: []string{"fld", "folder"}, @@ -14,6 +18,14 @@ var foldersCmd = &cobra.Command{ Long: `Folders Tooling`, } +func getFolderFilter() filters.Filter { + if !useFolderFilters { + return nil + } + return api.NewFolderFilter() + +} + var foldersDeleteCmd = &cobra.Command{ Use: "delete", Short: "delete Folders", @@ -23,7 +35,7 @@ var foldersDeleteCmd = &cobra.Command{ log.Infof("Deleting all Folders for context: '%s'", apphelpers.GetContext()) tableObj.AppendHeader(table.Row{"title"}) - folders := client.DeleteAllFolder(nil) + folders := client.DeleteAllFolder(getFolderFilter()) if len(folders) == 0 { log.Info("No Folders found") } else { @@ -44,7 +56,7 @@ var foldersExportCmd = &cobra.Command{ log.Infof("Listing Folders for context: '%s'", apphelpers.GetContext()) tableObj.AppendHeader(table.Row{"file"}) - folders := client.ExportFolder(nil) + folders := client.ExportFolder(getFolderFilter()) if len(folders) == 0 { log.Info("No folders found") } else { @@ -65,7 +77,7 @@ var foldersImportCmd = &cobra.Command{ log.Infof("Listing Folders for context: '%s'", apphelpers.GetContext()) tableObj.AppendHeader(table.Row{"file"}) - folders := client.ImportFolder(nil) + folders := client.ImportFolder(getFolderFilter()) if len(folders) == 0 { log.Info("No folders found") } else { @@ -86,7 +98,8 @@ var foldersListCmd = &cobra.Command{ log.Infof("Listing Folders for context: '%s'", apphelpers.GetContext()) tableObj.AppendHeader(table.Row{"id", "uid", "title"}) - folders := client.ListFolder(nil) + folders := client.ListFolder(getFolderFilter()) + if len(folders) == 0 { log.Info("No folders found") } else { @@ -105,4 +118,6 @@ func init() { foldersCmd.AddCommand(foldersExportCmd) foldersCmd.AddCommand(foldersImportCmd) foldersCmd.AddCommand(foldersListCmd) + foldersCmd.PersistentFlags().BoolVar(&useFolderFilters, "use-filters", false, "Default to false, but if passed then will only operate on the list of folders listed in the configuration file") + } diff --git a/cmd/library.go b/cmd/library.go new file mode 100644 index 00000000..ece47ad2 --- /dev/null +++ b/cmd/library.go @@ -0,0 +1,108 @@ +package cmd + +import ( + "github.com/esnet/gdg/api/filters" + "github.com/esnet/gdg/apphelpers" + "github.com/jedib0t/go-pretty/table" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +var libraryElements = &cobra.Command{ + Use: "libraryelements", + Aliases: []string{"lib", "library"}, + Short: "Manage Library Elements", + Long: `Manage Library Elements.`, +} + +var clearLibrary = &cobra.Command{ + Use: "clear", + Short: "delete all Library elements", + Long: `clear all library elements`, + Run: func(cmd *cobra.Command, args []string) { + //filter := getLibraryGlobalFlags(cmd) + deletedLibrarys := client.DeleteAllLibraryElements(nil) + tableObj.AppendHeader(table.Row{"type", "filename"}) + for _, file := range deletedLibrarys { + tableObj.AppendRow(table.Row{"library", file}) + } + if len(deletedLibrarys) == 0 { + log.Info("No library were found. 0 librarys removed") + + } else { + log.Infof("%d library were deleted", len(deletedLibrarys)) + tableObj.Render() + } + + }, +} + +var exportLibrary = &cobra.Command{ + Use: "export", + Short: "export all library", + Long: `export all library`, + Run: func(cmd *cobra.Command, args []string) { + log.Info("exporting lib elements") + libraryFilter := filters.NewBaseFilter() + elements := client.ExportLibraryElements(libraryFilter) + tableObj.AppendHeader(table.Row{"Name"}) + if len(elements) > 0 { + for _, link := range elements { + tableObj.AppendRow(table.Row{link}) + } + tableObj.Render() + } else { + log.Info("No library found") + } + }, +} + +var importLibrary = &cobra.Command{ + Use: "import", + Short: "Import all library", + Long: `Import all library from grafana to local file system`, + Run: func(cmd *cobra.Command, args []string) { + log.Info("exporting lib elements") + //filter := parseDashboardGlobalFlags(cmd) + savedFiles := client.ImportLibraryElements(nil) + log.Infof("Importing library for context: '%s'", apphelpers.GetContext()) + tableObj.AppendHeader(table.Row{"type", "filename"}) + for _, file := range savedFiles { + tableObj.AppendRow(table.Row{"library", file}) + } + tableObj.Render() + + }, +} + +var listLibraries = &cobra.Command{ + Use: "list", + Short: "List all library", + Long: `List all library`, + Run: func(cmd *cobra.Command, args []string) { + tableObj.AppendHeader(table.Row{"id", "UID", "Folder", "Name", "Type"}) + + elements := client.ListLibraryElements(nil) + log.Infof("Number of elements is: %d", len(elements)) + + log.Infof("Listing library for context: '%s'", apphelpers.GetContext()) + for _, link := range elements { + tableObj.AppendRow(table.Row{link.ID, link.UID, link.Meta.FolderName, link.Name, link.Type}) + + } + if len(elements) > 0 { + tableObj.Render() + } else { + log.Info("No library found") + } + + }, +} + +func init() { + rootCmd.AddCommand(libraryElements) + libraryElements.AddCommand(importLibrary) + libraryElements.AddCommand(listLibraries) + libraryElements.AddCommand(clearLibrary) + libraryElements.AddCommand(exportLibrary) +} diff --git a/config/config_model.go b/config/config_model.go index fd93f4e0..ff648993 100644 --- a/config/config_model.go +++ b/config/config_model.go @@ -19,6 +19,7 @@ const ( DataSourceResource = "datasources" FolderResource = "folders" AlertNotificationResource = "alertnotifications" + LibraryElementResource = "libraryelements" ) func (s *ResourceType) String() string { diff --git a/docker-compose.yml b/docker-compose.yml index 794e91c3..60aaf49b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,4 +11,7 @@ services: grafana: image: grafana/grafana:9.4.3-ubuntu ports: - - 3000:3000 \ No newline at end of file + - 3000:3000 + environment: + - GF_INSTALL_PLUGINS=grafana-googlesheets-datasource + diff --git a/documentation/content/en/_index.md b/documentation/content/en/_index.md index 2e514e8b..b48b5c64 100644 --- a/documentation/content/en/_index.md +++ b/documentation/content/en/_index.md @@ -1,7 +1,7 @@ --- -title : "Modern Documentation Theme" -description: "Doks is a Hugo theme for building secure, fast, and SEO-ready documentation websites, which you can easily update and customize." -lead: "Doks is a Hugo theme for building secure, fast, and SEO-ready documentation websites, which you can easily update and customize." +title : "GDG - Grafana Dash-n-Grab" +description: "GDG is a tool used to manage dashboards, datasources, orgs and various entities of the Grafana application." +lead: "GDG is a tool used to manage dashboards, datasources, orgs and various entities of the Grafana application." date: 2020-10-06T08:47:36+00:00 lastmod: 2020-10-06T08:47:36+00:00 draft: false diff --git a/documentation/content/en/docs/gdg/usage_guide.md b/documentation/content/en/docs/gdg/usage_guide.md index 44332b0e..7f8d69da 100644 --- a/documentation/content/en/docs/gdg/usage_guide.md +++ b/documentation/content/en/docs/gdg/usage_guide.md @@ -5,7 +5,9 @@ weight: 16 Every namespace supporting CRUD operations has the functions: list, import, export, clear operating on only the monitored folders. -### Alert Notifications +### Alert Notifications (DEPRECATED) + +This will stop working soon both as a concept in grafana and something that GDG will support. Allows you to manage alertnotifications (an) if you have any setup @@ -32,7 +34,7 @@ ctx is shorthand for context and basic CRUD is supported which is mainly tooling ``` -#### Dashboards +### Dashboards Dashboards are imported or exported from _organization_ specified in configuration file otherwise current organization user is used. @@ -53,7 +55,7 @@ You can also use filtering options to list or import your daashboard by folder o ./bin/gdg dash import -t tagA -t tagB -t tagC ``` -#### DataSources +### DataSources DataSources credentials are keyed by the name of the DataSource. See see [config example](https://github.com/esnet/gdg/blob/master/conf/importer-example.yml). If the datasource JSON doesn't have auth enabled, the credentials are ignored. If Credentials are missing, we'll fall back on default credentials if any exist. The password is set as a value for basicAuthPassword in the API payload. Datasources are imported or exported from _organization_ specified in configuration file otherwise current organization user is used. @@ -68,7 +70,22 @@ All commands can use `datasources` or `ds` to manage datasources ./bin/gdg ds clear -- Deletes all datasources ``` -#### Devel +### Library Elements + +Library elements are components that can be shared among multiple dashboards. Folder matching will still be applied, so any folders not monitored will be ignored unless explicitly specified. If wildcard flag is enabled, all elements will be acted on irrelevant of folder location + +All commands can use `libraryelements` aliased to `library` and `lib` for laziness purposes. + +```sh +./bin/gdg lib list -- Lists all library components +./bin/gdg lib import -- Import all library components from grafana to local file system +./bin/gdg lib export -- Exports all library components from local filesystem (matching folder filter) to Grafana +./bin/gdg lib clear -- Deletes all library components +``` + + + +### Devel Some developer helper utilities @@ -77,7 +94,7 @@ Some developer helper utilities ./bin/gdg devel srvinfo -- print grafana server info ``` -#### Folders +### Folders Mostly optional as Dashboards will create/delete these are needed but if there is additional metadata you wish to persist you can use this to manage them. @@ -88,7 +105,7 @@ Mostly optional as Dashboards will create/delete these are needed but if there i ./bin/gdg folders clear -- Deletes all folders ``` -#### Organizations +### Organizations Command can use `organizations` or `org` to manage organizations. ```sh diff --git a/documentation/content/en/docs/releases/gdg_0.4.0.md b/documentation/content/en/docs/releases/gdg_0.4.0.md index 63ea9730..3b839d7a 100644 --- a/documentation/content/en/docs/releases/gdg_0.4.0.md +++ b/documentation/content/en/docs/releases/gdg_0.4.0.md @@ -2,36 +2,59 @@ title: "0.4.0" description: "Release Notes for v0.4.0" #lead: "Doks comes with commands for common tasks." -date: 2020-10-13T15:21:01+02:00 -lastmod: 2020-10-13T15:21:01+02:00 +date: 2023-03-31T15:21:01+02:00 draft: false images: [] -#menu: -# docs: -# parent: "prologue" -weight: 130 +weight: 199 toc: true --- -## DRAFT: Release Notes for v0.4.0 +# Release Notes for v0.4.0 This is a major change from the previous one, I'll likely cut the 1.x soon and start following the more typical Semver conventions. Aka Major version is a breaking change, Minor is just that, patches for previous versions. -### API SDK Changes: +Please see the API Changes notes [below](https://software.es.net/gdg/docs/releases/gdg_0.4.0/#api-sdk-changes). + +### New Features + +#### Wild card flag + + You can now set a flag under each context that will ignore Watched Folders and retrieve all dashboards. + + ```yaml + context_name: + filter_override: + ignore_dashboard_filters: false # +``` +#### LibraryElements support added. + +Please see the usage guide [here](https://software.es.net/gdg/docs/gdg/usage_guide/#library-elements) and a brief tutorial available [here](https://software.es.net/gdg/docs/tutorials/library_elements/) + +#### Folders Update + +Introducing a --use-filters. When enabled will only operate on folders configured. Default is to create/update/delete all folders in the grafana instance. + +### Breaking Changes: + +#### SFTP support dropped. + +See the Cloud [configuration](https://software.es.net/gdg/docs/gdg/cloud_configuration/) section. Switched out the library we relied on, which means the auth has moved out of GDG config and relies on the system config. + +## API SDK Changes: I have been trying to find a proper library to use so I'm not re-writing and reinventing the wheel so to speak. For reference, here are all the current "active" (active can be a relative term for some of these project) development I'm aware of. - - https://github.com/grafana-tools/sdk Initial version of GDG was based on this project. It mostly works but getting any PRS accepted can be tedious and it's needs some help. - - https://github.com/grafana/grafana-api-golang-client Owned by the Grafana Org which is nice, but it has a slightly different goal. It's primary goal is to support the terraform provider for Grafana. I also found some endpoints missing very early on. So decided not to go with it. + - [Grafana Tools SDK](https://github.com/grafana-tools/sdk) Initial version of GDG was based on this project. It mostly works but getting any PRS accepted can be tedious and it's needs some help. + - [Grafana API Go Client](https://github.com/grafana/grafana-api-golang-client) Owned by the Grafana Org which is nice, but it has a slightly different goal. It's primary goal is to support the terraform provider for Grafana. I also found some endpoints missing very early on. So decided not to go with it. - Swagger Based: There's a branch that I've been keeping an eye on. https://github.com/grafana/grafana-api-golang-client/tree/papagian/generate-client-from-swagger which makes an effort to generate code based on the swagger manifest that's available from Grafana. It's a mostly automated code that pulls data from the [Schema](https://github.com/grafana/grafana/blob/main/public/api-merged.json) and generates the underlying code. It hasn't had much traction of late so I ended up forking the project currently available [here](https://github.com/esnet/grafana-swagger-api-golang) -Final Choice: +#### Final Choice: Although the Swagger/OpenAPI based version is not great, I've even ran into a few issues where the documented response does not match the result, it's a lot more encompassing and allows further development without being as limited on upstream changes. -## DataModel Changes +### DataModel Changes I've tried to utilize mostly the same endpoints to recreate the same behavior for all the various entities, but there is are some changes. For most use cases this shouldn't matter. But you have been officially warned. diff --git a/documentation/content/en/docs/tutorials/coming_soon.md b/documentation/content/en/docs/tutorials/coming_soon.md deleted file mode 100644 index 43ddb792..00000000 --- a/documentation/content/en/docs/tutorials/coming_soon.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: "Coming Soon" -weight: 15 ---- - -This section is intended to be a more comprehensive guides on using GDG, as more advanced and more complex features come about. \ No newline at end of file diff --git a/documentation/content/en/docs/tutorials/library_elements.md b/documentation/content/en/docs/tutorials/library_elements.md new file mode 100644 index 00000000..e578cbfe --- /dev/null +++ b/documentation/content/en/docs/tutorials/library_elements.md @@ -0,0 +1,186 @@ +--- +title: "Working with Library Panels" +weight: 2 +--- + +Starting with version 0.4, library panels are going to be supported. It's a bit special and the behavior is somewhat unique. + +Rules: + - Library Panels are immutable. They cannot be moved to a different folder. They are linked to one or multiple dashboards. + - The only way I can see to move a lib element is to unlink the panel, delete the panel and re-create it in a different folder, then re-link it. + - In theory it's supposed to move with the dashboards but I haven't been able to re-create that behavior. + - You cannot delete a library element while a dashboard is still using it. + + +## Import components + +will retrieve all the components from Grafana and save to local file system. + + + +```sh +gdg lib import +┌─────────┬───────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ TYPE │ FILENAME │ +├─────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────┤ +│ library │ testing_data/libraryelements/General/dashboard-makeover-extra-cleaning-duty-assignment-today.json │ +│ library │ testing_data/libraryelements/General/dashboard-makeover-lighting-status.json │ +│ library │ testing_data/libraryelements/General/dashboard-makeover-side-dish-prep-times-past-7-days.json │ +│ library │ testing_data/libraryelements/General/dashboard-makeover-time-since-we-purchased-these-spices.json │ +│ library │ testing_data/libraryelements/General/extreme-dashboard-makeover-grill.json │ +│ library │ testing_data/libraryelements/General/extreme-dashboard-makeover-mac-oven.json │ +│ library │ testing_data/libraryelements/General/extreme-dashboard-makeover-refrigerator-temperature-f.json │ +│ library │ testing_data/libraryelements/General/extreme-dashboard-makeover-room-temperature-f.json │ +│ library │ testing_data/libraryelements/General/extreme-dashboard-makeover-salmon-cooking-times-past-7-days.json │ +└─────────┴───────────────────────────────────────────────────────────────────────────────────────────────────────┘ +``` + +## Importing Dashboards +Now that we the library components, pulled let's pull the Dashboard. + +```sh +gdg dash import +INFO[0002] Importing dashboards for context: 'local' +┌───────────┬───────────────────────────────────────────────────────────────────┐ +│ TYPE │ FILENAME │ +├───────────┼───────────────────────────────────────────────────────────────────┤ +│ dashboard │ testing_data/dashboards/General/bandwidth-dashboard.json │ +│ dashboard │ testing_data/dashboards/General/bandwidth-patterns.json │ +│ dashboard │ testing_data/dashboards/Other/dashboard-makeover-challenge.json │ <== uses library panels +│ dashboard │ testing_data/dashboards/Other/flow-analysis.json │ +│ dashboard │ testing_data/dashboards/Other/flow-data-for-circuits.json │ +│ dashboard │ testing_data/dashboards/Other/flow-data-for-projects.json │ +│ dashboard │ testing_data/dashboards/Other/flow-data-per-country.json │ +│ dashboard │ testing_data/dashboards/Other/flow-data-per-organization.json │ +│ dashboard │ testing_data/dashboards/Other/flow-information.json │ +│ dashboard │ testing_data/dashboards/Other/flows-by-science-discipline.json │ +│ dashboard │ testing_data/dashboards/General/individual-flows.json │ +│ dashboard │ testing_data/dashboards/General/individual-flows-per-country.json │ +│ dashboard │ testing_data/dashboards/Ignored/latency-patterns.json │ +│ dashboard │ testing_data/dashboards/General/loss-patterns.json │ +│ dashboard │ testing_data/dashboards/General/other-flow-stats.json │ +│ dashboard │ testing_data/dashboards/General/science-discipline-patterns.json │ +│ dashboard │ testing_data/dashboards/General/top-talkers-over-time.json │ +└───────────┴───────────────────────────────────────────────────────────────────┘ +``` + +The dashboards will have a reference to the library panel linked by UID. + +Here's the json from the dashboard JSON: + +```json + "libraryPanel": { + "description": "", + "meta": { + "connectedDashboards": 3, + "created": "2022-05-17T19:35:06Z", + "createdBy": { + "avatarUrl": "/avatar/579fc54abdc9ab34fb4865322f2870a1", + "id": 13, + "name": "mike.johnson@grafana.com" + }, + "folderName": "mj", + "folderUid": "R0bMCcW7z", + "updated": "2022-05-17T19:37:14Z", + "updatedBy": { + "avatarUrl": "/avatar/579fc54abdc9ab34fb4865322f2870a1", + "id": 13, + "name": "mike.johnson@grafana.com" + } + }, + "name": "Extreme Dashboard Makeover - Grill", + "type": "graph", + "uid": "y1C0A5unz", + "version": 2 + }, +``` + +Please note, this is the Grill panel. + +```json +{ + "name": "Extreme Dashboard Makeover - Grill", + "orgId": 1, + "type": "graph", + "uid": "y1C0A5unz", + "version": 1 +} +``` +## Deleting Elements +If we try to delete all the Library elements, that won't be allowed. + +```sh +./bin/gdg lib clear +ERRO[0000] Failed to delete library panel titled: Dashboard Makeover - Extra Cleaning Duty Assignment Today ErrorMessage="the library element has connections" +ERRO[0000] Failed to delete library panel titled: Dashboard Makeover - Lighting Status ErrorMessage="the library element has connections" +ERRO[0000] Failed to delete library panel titled: Dashboard Makeover - Side Dish Prep Times, past 7 days ErrorMessage="the library element has connections" +ERRO[0000] Failed to delete library panel titled: Dashboard Makeover - Time since we purchased these spices ErrorMessage="the library element has connections" +ERRO[0000] Failed to delete library panel titled: Extreme Dashboard Makeover - Grill ErrorMessage="the library element has connections" +ERRO[0000] Failed to delete library panel titled: Extreme Dashboard Makeover - Mac Oven ErrorMessage="the library element has connections" +ERRO[0000] Failed to delete library panel titled: Extreme Dashboard Makeover - Refrigerator Temperature (F) ErrorMessage="the library element has connections" +ERRO[0000] Failed to delete library panel titled: Extreme Dashboard Makeover - Room Temperature (F) ErrorMessage="the library element has connections" +ERRO[0000] Failed to delete library panel titled: Extreme Dashboard Makeover - Salmon Cooking Times, past 7 days ErrorMessage="the library element has connections" +INFO[0000] No library were found. 0 librarys removed +``` + +### Deleting related dashboard + + +(Future version will allow you to inspect which dashboard has a link to which dashboards) + +```sh +./bin/gdg dash clear -d dashboard-makeover-challenge (gke_esnet-sd-dev_us-central1-c_dev-staging-kafka-3/default) +INFO[0000] 1 dashboards were deleted +┌───────────┬──────────────────────────────┐ +│ TYPE │ FILENAME │ +├───────────┼──────────────────────────────┤ +│ dashboard │ Dashboard Makeover Challenge │ +└───────────┴──────────────────────────────┘ +``` + +Please note the -d, we're explicitly only deleting one dashboard. We can verify the list. + +```sh +./bin/gdg dash list +┌────┬──────────────────────────────┬──────────────────────────────┬─────────┬───────────┬────────────────────────────────────────────────────────────────┐ +│ ID │ TITLE │ SLUG │ FOLDER │ UID │ URL │ +├────┼──────────────────────────────┼──────────────────────────────┼─────────┼───────────┼────────────────────────────────────────────────────────────────┤ +│ 80 │ Bandwidth Dashboard │ bandwidth-dashboard │ General │ 000000003 │ http://localhost:3000/d/000000003/bandwidth-dashboard │ +│ 81 │ Bandwidth Patterns │ bandwidth-patterns │ General │ 000000004 │ http://localhost:3000/d/000000004/bandwidth-patterns │ +│ 90 │ Flow Analysis │ flow-analysis │ Other │ VuuXrnPWz │ http://localhost:3000/d/VuuXrnPWz/flow-analysis │ +│ 91 │ Flow Data for Circuits │ flow-data-for-circuits │ Other │ xk26IFhmk │ http://localhost:3000/d/xk26IFhmk/flow-data-for-circuits │ +│ 92 │ Flow Data for Projects │ flow-data-for-projects │ Other │ ie7TeomGz │ http://localhost:3000/d/ie7TeomGz/flow-data-for-projects │ +│ 93 │ Flow Data per Country │ flow-data-per-country │ Other │ fgrOzz_mk │ http://localhost:3000/d/fgrOzz_mk/flow-data-per-country │ +│ 94 │ Flow Data per Organization │ flow-data-per-organization │ Other │ QfzDJKhik │ http://localhost:3000/d/QfzDJKhik/flow-data-per-organization │ +│ 95 │ Flow Information │ flow-information │ Other │ nzuMyBcGk │ http://localhost:3000/d/nzuMyBcGk/flow-information │ +│ 96 │ Flows by Science Discipline │ flows-by-science-discipline │ Other │ WNn1qyaiz │ http://localhost:3000/d/WNn1qyaiz/flows-by-science-discipline │ +│ 83 │ Individual Flows │ individual-flows │ General │ -l3_u8nWk │ http://localhost:3000/d/-l3_u8nWk/individual-flows │ +│ 82 │ Individual Flows per Country │ individual-flows-per-country │ General │ 80IVUboZk │ http://localhost:3000/d/80IVUboZk/individual-flows-per-country │ +│ 88 │ Latency Patterns │ latency-patterns │ Ignored │ 000000005 │ http://localhost:3000/d/000000005/latency-patterns │ +│ 84 │ Loss Patterns │ loss-patterns │ General │ 000000006 │ http://localhost:3000/d/000000006/loss-patterns │ +│ 85 │ Other Flow Stats │ other-flow-stats │ General │ CJC1FFhmz │ http://localhost:3000/d/CJC1FFhmz/other-flow-stats │ +│ 86 │ Science Discipline Patterns │ science-discipline-patterns │ General │ ufIS9W7Zk │ http://localhost:3000/d/ufIS9W7Zk/science-discipline-patterns │ +│ 87 │ Top Talkers Over Time │ top-talkers-over-time │ General │ b35BWxAZz │ http://localhost:3000/d/b35BWxAZz/top-talkers-over-time │ +└────┴──────────────────────────────┴──────────────────────────────┴─────────┴───────────┴────────────────────────────────────────────────────────────────┘ +``` + +### Removing related components + + +```sh +./bin/gdg lib clear (gke_esnet-sd-dev_us-central1-c_dev-staging-kafka-3/default) +INFO[0000] 9 library were deleted +┌─────────┬────────────────────────────────────────────────────────────────┐ +│ TYPE │ FILENAME │ +├─────────┼────────────────────────────────────────────────────────────────┤ +│ library │ Dashboard Makeover - Extra Cleaning Duty Assignment Today │ +│ library │ Dashboard Makeover - Lighting Status │ +│ library │ Dashboard Makeover - Side Dish Prep Times, past 7 days │ +│ library │ Dashboard Makeover - Time since we purchased these spices │ +│ library │ Extreme Dashboard Makeover - Grill │ +│ library │ Extreme Dashboard Makeover - Mac Oven │ +│ library │ Extreme Dashboard Makeover - Refrigerator Temperature (F) │ +│ library │ Extreme Dashboard Makeover - Room Temperature (F) │ +│ library │ Extreme Dashboard Makeover - Salmon Cooking Times, past 7 days │ +└─────────┴────────────────────────────────────────────────────────────────┘ +``` \ No newline at end of file diff --git a/go.mod b/go.mod index 49158b0f..a44066ab 100644 --- a/go.mod +++ b/go.mod @@ -31,6 +31,7 @@ require ( github.com/carlmjohnson/requests v0.23.2 github.com/esnet/grafana-swagger-api-golang v0.0.0-20230321221207-0889360737aa github.com/go-openapi/runtime v0.25.0 + github.com/tidwall/gjson v1.14.4 gocloud.dev v0.29.0 golang.org/x/exp v0.0.0-20230124195608-d38c7dcee874 ) @@ -110,6 +111,7 @@ require ( github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.4.2 // indirect + github.com/tidwall/match v1.1.1 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/otel v1.14.0 // indirect go.opentelemetry.io/otel/trace v1.14.0 // indirect diff --git a/go.sum b/go.sum index e35fb0e7..6d3c0ca8 100644 --- a/go.sum +++ b/go.sum @@ -1818,7 +1818,12 @@ github.com/tchap/go-patricia v2.2.6+incompatible/go.mod h1:bmLyhP68RS6kStMGxByiQ github.com/tedsuo/ifrit v0.0.0-20180802180643-bea94bb476cc/go.mod h1:eyZnKCc955uh98WQvzOm0dgAeLnf2O0Rz0LPoC5ze+0= github.com/thoas/go-funk v0.9.3 h1:7+nAEx3kn5ZJcnDm2Bh23N2yOtweO14bi//dvRtgLpw= github.com/thoas/go-funk v0.9.3/go.mod h1:+IWnUfUmFO1+WVYQWQtIJHeRRdaIyyYglZN7xzUPe4Q= +github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= +github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= diff --git a/integration_tests/cloud_integration_test.go b/integration_tests/cloud_integration_test.go index 5dca9dea..707361ce 100644 --- a/integration_tests/cloud_integration_test.go +++ b/integration_tests/cloud_integration_test.go @@ -15,7 +15,7 @@ func TestCloudDataSourceCRUD(t *testing.T) { apiClient, _ := initTest(t) //Wipe all data from grafana - dsFilter := api.DatasourceFilter{} + dsFilter := api.NewDataSourceFilter("") apiClient.DeleteAllDataSources(dsFilter) apiClient.ExportDataSources(dsFilter) @@ -48,7 +48,7 @@ func TestDashboardCloudCRUD(t *testing.T) { apiClient, _ := initTest(t) //Wipe all data from grafana - dashFilter := api.NewDashboardFilter() + dashFilter := api.NewDashboardFilter("", "", "") apiClient.DeleteAllDashboards(dashFilter) //Load data into grafana apiClient.ExportDashboards(dashFilter) diff --git a/integration_tests/dashboard_integration_test.go b/integration_tests/dashboard_integration_test.go index 1bb70797..3624b2c7 100644 --- a/integration_tests/dashboard_integration_test.go +++ b/integration_tests/dashboard_integration_test.go @@ -1,27 +1,32 @@ package integration_tests import ( + "github.com/esnet/gdg/api" + "github.com/esnet/gdg/api/filters" "github.com/esnet/grafana-swagger-api-golang/goclient/models" "strings" "testing" - "github.com/esnet/gdg/api" "github.com/esnet/gdg/config" log "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "golang.org/x/exp/slices" ) +//TODO: with full CRUD. +// - Add single dashboard test -d <> +// - Add Folder dashboard test -f <> + func TestDashboardCRUD(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } apiClient, _ := initTest(t) - filters := api.NewDashboardFilter() + filtersEntity := api.NewDashboardFilter("", "", "") log.Info("Exporting all dashboards") - apiClient.ExportDashboards(filters) + apiClient.ExportDashboards(filtersEntity) log.Info("Listing all dashboards") - boards := apiClient.ListDashboards(filters) + boards := apiClient.ListDashboards(filtersEntity) log.Infof("Imported %d dashboards", len(boards)) ignoredSkipped := true var generalBoard *models.Hit @@ -45,13 +50,13 @@ func TestDashboardCRUD(t *testing.T) { validateOtherBoard(t, otherBoard) //Import Dashboards log.Info("Importing Dashboards") - list := apiClient.ImportDashboards(filters) + list := apiClient.ImportDashboards(filtersEntity) assert.Equal(t, len(list), len(boards)) log.Info("Deleting Dashboards") - deleteList := apiClient.DeleteAllDashboards(filters) + deleteList := apiClient.DeleteAllDashboards(filtersEntity) assert.Equal(t, len(deleteList), len(boards)) log.Info("List Dashboards again") - boards = apiClient.ListDashboards(filters) + boards = apiClient.ListDashboards(filtersEntity) assert.Equal(t, len(boards), 0) } @@ -60,16 +65,16 @@ func TestDashboardTagsFilter(t *testing.T) { t.Skip("skipping integration test") } apiClient, _ := initTest(t) - emptyFilter := api.NewDashboardFilter() + emptyFilter := filters.NewBaseFilter() - filters := api.NewDashboardFilter() - filters.AddFilter(api.TagsFilter, strings.Join([]string{"flow", "netsage"}, ",")) + filtersEntity := api.NewDashboardFilter("", "", "") + filtersEntity.AddFilter(filters.TagsFilter, strings.Join([]string{"flow", "netsage"}, ",")) log.Info("Exporting all dashboards") apiClient.ExportDashboards(emptyFilter) log.Info("Listing all dashboards") - boards := apiClient.ListDashboards(filters) + boards := apiClient.ListDashboards(filtersEntity) log.Infof("Imported %d dashboards", len(boards)) for _, board := range boards { @@ -78,15 +83,15 @@ func TestDashboardTagsFilter(t *testing.T) { //Import Dashboards log.Info("Importing Dashboards") - list := apiClient.ImportDashboards(filters) + list := apiClient.ImportDashboards(filtersEntity) assert.Equal(t, len(list), len(boards)) log.Info("Deleting Dashboards") - deleteList := apiClient.DeleteAllDashboards(filters) + deleteList := apiClient.DeleteAllDashboards(filtersEntity) assert.Equal(t, len(deleteList), len(boards)) log.Info("List Dashboards again") - boards = apiClient.ListDashboards(filters) + boards = apiClient.ListDashboards(filtersEntity) assert.Equal(t, len(boards), 0) } @@ -97,10 +102,10 @@ func TestWildcardFilter(t *testing.T) { // Setup Filters apiClient, _ := initTest(t) - emptyFilter := api.NewDashboardFilter() + emptyFilter := filters.NewBaseFilter() - filters := api.NewDashboardFilter() - filters.AddFilter(api.TagsFilter, strings.Join([]string{"flow", "netsage"}, ",")) + filtersEntity := api.NewDashboardFilter("", "", "") + filtersEntity.AddFilter(filters.TagsFilter, strings.Join([]string{"flow", "netsage"}, ",")) // Enable Wildcard testingContext := config.Config().Contexts()["testing"] @@ -111,7 +116,7 @@ func TestWildcardFilter(t *testing.T) { apiClient.ExportDashboards(emptyFilter) boards := apiClient.ListDashboards(emptyFilter) - apiClient.ExportDashboards(filters) + apiClient.ExportDashboards(filtersEntity) boards_filtered := apiClient.ListDashboards(emptyFilter) assert.Equal(t, len(boards), len(boards_filtered)) @@ -121,7 +126,7 @@ func TestWildcardFilter(t *testing.T) { boards = apiClient.ListDashboards(emptyFilter) log.Info("Listing all dashboards ignoring filter") - boards_filtered = apiClient.ListDashboards(filters) + boards_filtered = apiClient.ListDashboards(filtersEntity) assert.Equal(t, len(boards), len(boards_filtered)) @@ -134,7 +139,7 @@ func TestWildcardFilter(t *testing.T) { assert.Equal(t, len(deleteList), len(boards)) log.Info("List Dashboards again") - boards = apiClient.ListDashboards(filters) + boards = apiClient.ListDashboards(filtersEntity) assert.Equal(t, len(boards), 0) } diff --git a/integration_tests/datasource_integration_test.go b/integration_tests/datasource_integration_test.go index 6b44c961..4dade0d1 100644 --- a/integration_tests/datasource_integration_test.go +++ b/integration_tests/datasource_integration_test.go @@ -15,11 +15,11 @@ func TestDataSourceCRUD(t *testing.T) { t.Skip("skipping integration test") } apiClient, _ := initTest(t) - filters := api.NewDashboardFilter() + filtersEntity := api.NewDataSourceFilter("") log.Info("Exporting all datasources") - apiClient.ExportDataSources(filters) + apiClient.ExportDataSources(filtersEntity) log.Info("Listing all datasources") - dataSources := apiClient.ListDataSources(filters) + dataSources := apiClient.ListDataSources(filtersEntity) assert.Equal(t, len(dataSources), 3) var dsItem *models.DataSourceListItemDTO for _, ds := range dataSources { @@ -32,13 +32,13 @@ func TestDataSourceCRUD(t *testing.T) { validateDataSource(t, *dsItem) //Import Dashboards log.Info("Importing datasources") - list := apiClient.ImportDataSources(filters) + list := apiClient.ImportDataSources(filtersEntity) assert.Equal(t, len(list), len(dataSources)) log.Info("Deleting datasources") - deleteList := apiClient.DeleteAllDataSources(filters) + deleteList := apiClient.DeleteAllDataSources(filtersEntity) assert.Equal(t, len(deleteList), len(dataSources)) log.Info("List datasources again") - dataSources = apiClient.ListDataSources(filters) + dataSources = apiClient.ListDataSources(filtersEntity) assert.Equal(t, len(dataSources), 0) } @@ -59,11 +59,11 @@ func TestDataSourceFilter(t *testing.T) { apiClient := api.NewApiService("dummy") - filters := api.NewDashboardFilter() + filtersEntity := api.NewDataSourceFilter("") log.Info("Exporting all datasources") - apiClient.ExportDataSources(filters) + apiClient.ExportDataSources(filtersEntity) log.Info("Listing all datasources") - dataSources := apiClient.ListDataSources(filters) + dataSources := apiClient.ListDataSources(filtersEntity) assert.Equal(t, len(dataSources), 2) var dsItem *models.DataSourceListItemDTO for _, ds := range dataSources { @@ -76,13 +76,13 @@ func TestDataSourceFilter(t *testing.T) { validateDataSource(t, *dsItem) //Import Dashboards log.Info("Importing datasources") - list := apiClient.ImportDataSources(filters) + list := apiClient.ImportDataSources(filtersEntity) assert.Equal(t, len(list), len(dataSources)) log.Info("Deleting datasources") - deleteList := apiClient.DeleteAllDataSources(filters) + deleteList := apiClient.DeleteAllDataSources(filtersEntity) assert.Equal(t, len(deleteList), len(dataSources)) log.Info("List datasources again") - dataSources = apiClient.ListDataSources(filters) + dataSources = apiClient.ListDataSources(filtersEntity) assert.Equal(t, len(dataSources), 0) } diff --git a/integration_tests/libraryelements_integration_test.go b/integration_tests/libraryelements_integration_test.go new file mode 100644 index 00000000..23bd0f3a --- /dev/null +++ b/integration_tests/libraryelements_integration_test.go @@ -0,0 +1,62 @@ +package integration_tests + +import ( + "github.com/esnet/gdg/api" + "github.com/esnet/grafana-swagger-api-golang/goclient/models" + "github.com/gosimple/slug" + log "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestLibraryElementsCRUD(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + apiClient, _ := initTest(t) + apiClient.DeleteAllDashboards(api.NewDashboardFilter("", "", "")) + filtersEntity := api.NewDashboardFilter("", "", "") + log.Info("Exporting all Library Elements") + apiClient.ExportLibraryElements(filtersEntity) + log.Info("Listing all library elements") + boards := apiClient.ListLibraryElements(filtersEntity) + log.Infof("Imported %d library elements", len(boards)) + var generalBoard *models.LibraryElementDTO + var otherBoard *models.LibraryElementDTO + for ndx, board := range boards { + log.Infof(board.Name) + if slug.Make(board.Name) == "dashboard-makeover-extra-cleaning-duty-assignment-today" { + generalBoard = boards[ndx] + } + if slug.Make(board.Name) == "extreme-dashboard-makeover-mac-oven" { + otherBoard = boards[ndx] + } + } + assert.NotNil(t, otherBoard) + assert.NotNil(t, generalBoard) + validateLibraryElement(t, generalBoard, map[string]interface{}{"Name": "Dashboard Makeover - Extra Cleaning Duty Assignment Today", + "Type": "table", "UID": "T47RSwQnz", "Kind": int64(1)}) + validateLibraryElement(t, otherBoard, map[string]interface{}{"Name": "Extreme Dashboard Makeover - Mac Oven", + "Type": "stat", "UID": "VvzpJ5X7z", "Kind": int64(1)}) + + //Import Library Elements + log.Info("Importing Library Elements") + list := apiClient.ImportLibraryElements(filtersEntity) + assert.Equal(t, len(list), len(boards)) + log.Info("Deleting Library Elements") + deleteList := apiClient.DeleteAllLibraryElements(filtersEntity) + assert.Equal(t, len(deleteList), len(boards)) + log.Info("List Dashboards again") + boards = apiClient.ListLibraryElements(filtersEntity) + assert.Equal(t, len(boards), 0) + +} + +func validateLibraryElement(t *testing.T, board *models.LibraryElementDTO, data map[string]interface{}) { + assert.Equal(t, board.Name, data["Name"].(string)) + assert.Equal(t, board.Type, data["Type"].(string)) + assert.Equal(t, board.UID, data["UID"].(string)) + assert.Equal(t, board.Kind, data["Kind"].(int64)) + +} diff --git a/main.go b/main.go index 15c95403..c1d2ca97 100644 --- a/main.go +++ b/main.go @@ -2,9 +2,10 @@ package main import ( _ "embed" - "github.com/esnet/gdg/cmd" "sync" + "github.com/esnet/gdg/cmd" + applogger "github.com/esnet/gdg/log" ) diff --git a/testing_data/dashboards/Other/dashboard-makeover-challenge.json b/testing_data/dashboards/Other/dashboard-makeover-challenge.json new file mode 100644 index 00000000..27d9327f --- /dev/null +++ b/testing_data/dashboards/Other/dashboard-makeover-challenge.json @@ -0,0 +1,1333 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "description": "Extreme Dashboard Makeover Workshop", + "editable": true, + "fiscalYearStartMonth": 0, + "gnetId": 16415, + "graphTooltip": 0, + "id": 93, + "links": [], + "liveNow": false, + "panels": [ + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "testdata", + "uid": "EJHgYyf4z" + }, + "description": "", + "fieldConfig": { + "defaults": { + "unit": "fahrenheit" + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "hiddenSeries": false, + "id": 13, + "interval": "", + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "libraryPanel": { + "description": "", + "meta": { + "connectedDashboards": 3, + "created": "2022-05-17T19:35:06Z", + "createdBy": { + "avatarUrl": "/avatar/579fc54abdc9ab34fb4865322f2870a1", + "id": 13, + "name": "mike.johnson@grafana.com" + }, + "folderName": "mj", + "folderUid": "R0bMCcW7z", + "updated": "2022-05-17T19:37:14Z", + "updatedBy": { + "avatarUrl": "/avatar/579fc54abdc9ab34fb4865322f2870a1", + "id": 13, + "name": "mike.johnson@grafana.com" + } + }, + "name": "Extreme Dashboard Makeover - Grill", + "type": "graph", + "uid": "y1C0A5unz", + "version": 2 + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "8.5.5", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "Grill", + "datasource": { + "type": "testdata", + "uid": "EJHgYyf4z" + }, + "max": 394, + "min": 380, + "refId": "A", + "scenarioId": "random_walk", + "startValue": 383 + } + ], + "thresholds": [], + "timeRegions": [], + "title": "Grill", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "transformations": [], + "transparent": true, + "type": "graph", + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:63", + "format": "fahrenheit", + "logBase": 1, + "show": true + }, + { + "$$hashKey": "object:64", + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "testdata", + "uid": "EJHgYyf4z" + }, + "description": "", + "fieldConfig": { + "defaults": { + "unit": "fahrenheit" + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "hiddenSeries": false, + "id": 14, + "interval": "", + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "libraryPanel": { + "description": "", + "meta": { + "connectedDashboards": 3, + "created": "2022-05-17T19:42:54Z", + "createdBy": { + "avatarUrl": "/avatar/579fc54abdc9ab34fb4865322f2870a1", + "id": 13, + "name": "mike.johnson@grafana.com" + }, + "folderName": "mj", + "folderUid": "R0bMCcW7z", + "updated": "2022-05-17T19:42:54Z", + "updatedBy": { + "avatarUrl": "/avatar/579fc54abdc9ab34fb4865322f2870a1", + "id": 13, + "name": "mike.johnson@grafana.com" + } + }, + "name": "Extreme Dashboard Makeover - Room Temperature (F)", + "type": "graph", + "uid": "3dSe05X7z", + "version": 1 + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "8.5.5", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "Zone 1", + "datasource": { + "type": "testdata", + "uid": "EJHgYyf4z" + }, + "max": 70, + "min": 67, + "refId": "A", + "scenarioId": "random_walk", + "spread": 0.5, + "startValue": 68 + }, + { + "alias": "Zone 2", + "datasource": { + "type": "testdata", + "uid": "EJHgYyf4z" + }, + "hide": false, + "max": 71, + "min": 69, + "noise": 0.1, + "refId": "B", + "scenarioId": "random_walk", + "spread": 0.5, + "startValue": 70 + }, + { + "alias": "Zone 3", + "datasource": { + "type": "testdata", + "uid": "EJHgYyf4z" + }, + "hide": false, + "max": 71, + "min": 69, + "noise": 0.1, + "refId": "C", + "scenarioId": "random_walk", + "spread": 0.5, + "startValue": 70 + }, + { + "alias": "Zone 4", + "datasource": { + "type": "testdata", + "uid": "EJHgYyf4z" + }, + "hide": false, + "max": 71, + "min": 69, + "noise": 0.1, + "refId": "D", + "scenarioId": "random_walk", + "spread": 0.5, + "startValue": 70 + }, + { + "alias": "Zone 5", + "datasource": { + "type": "testdata", + "uid": "EJHgYyf4z" + }, + "hide": false, + "max": 71, + "min": 69, + "noise": 0.1, + "refId": "E", + "scenarioId": "random_walk", + "spread": 0.5, + "startValue": 70 + }, + { + "alias": "Zone 6", + "datasource": { + "type": "testdata", + "uid": "EJHgYyf4z" + }, + "hide": false, + "max": 71, + "min": 69, + "noise": 0.1, + "refId": "F", + "scenarioId": "random_walk", + "spread": 0.5, + "startValue": 70 + }, + { + "alias": "Zone 7", + "datasource": { + "type": "testdata", + "uid": "EJHgYyf4z" + }, + "hide": false, + "max": 71, + "min": 69, + "noise": 0.1, + "refId": "G", + "scenarioId": "random_walk", + "spread": 0.5, + "startValue": 70 + }, + { + "alias": "Zone 8", + "datasource": { + "type": "testdata", + "uid": "EJHgYyf4z" + }, + "hide": false, + "max": 71, + "min": 69, + "noise": 0.1, + "refId": "H", + "scenarioId": "random_walk", + "spread": 0.5, + "startValue": 70 + } + ], + "thresholds": [], + "timeRegions": [], + "title": "Room Temperature (F)", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "transformations": [], + "transparent": true, + "type": "graph", + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:63", + "format": "fahrenheit", + "logBase": 1, + "show": true + }, + { + "$$hashKey": "object:64", + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + }, + { + "datasource": { + "type": "grafana-googlesheets-datasource", + "uid": "uL86Byf4k" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "displayMode": "auto", + "inspect": true + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 35 + }, + { + "color": "red", + "value": 60 + } + ] + }, + "unit": "none" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Purchase Date" + }, + "properties": [ + { + "id": "unit", + "value": "dateTimeFromNow" + } + ] + } + ] + }, + "gridPos": { + "h": 30, + "w": 5, + "x": 0, + "y": 8 + }, + "id": 2, + "interval": "", + "libraryPanel": { + "description": "", + "meta": { + "connectedDashboards": 3, + "created": "2022-04-27T18:58:06Z", + "createdBy": { + "avatarUrl": "/avatar/579fc54abdc9ab34fb4865322f2870a1", + "id": 13, + "name": "mike.johnson@grafana.com" + }, + "folderName": "mj", + "folderUid": "R0bMCcW7z", + "updated": "2022-04-27T20:40:51Z", + "updatedBy": { + "avatarUrl": "/avatar/579fc54abdc9ab34fb4865322f2870a1", + "id": 13, + "name": "mike.johnson@grafana.com" + } + }, + "name": "Dashboard Makeover - Time since we purchased these spices", + "type": "table", + "uid": "1D_h3UQ7k", + "version": 4 + }, + "links": [ + { + "targetBlank": true, + "title": "Raw Data", + "url": "https://docs.google.com/spreadsheets/d/1uoZrXcnv_ZeKoLk56eaBdTOI8kMYaulZdMxFNQUxmGs/edit#gid=527121891" + } + ], + "options": { + "footer": { + "fields": "", + "reducer": ["sum"], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "8.5.5", + "targets": [ + { + "cacheDurationSeconds": 0, + "datasource": { + "type": "grafana-googlesheets-datasource", + "uid": "uL86Byf4k" + }, + "expr": "web_http_5xx_errors", + "legendFormat": "500", + "range": "SpicesAge!A1:B32", + "refId": "A", + "spreadsheet": "1uoZrXcnv_ZeKoLk56eaBdTOI8kMYaulZdMxFNQUxmGs", + "useTimeFilter": false + } + ], + "title": "Time since we purchased these spices", + "transformations": [], + "type": "table" + }, + { + "datasource": { + "type": "grafana-googlesheets-datasource", + "uid": "uL86Byf4k" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "left", + "displayMode": "auto", + "inspect": true + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 35 + }, + { + "color": "red", + "value": 60 + } + ] + }, + "unit": "none" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Purchase Date" + }, + "properties": [ + { + "id": "unit", + "value": "dateTimeFromNow" + } + ] + } + ] + }, + "gridPos": { + "h": 4, + "w": 19, + "x": 5, + "y": 8 + }, + "id": 10, + "interval": "", + "libraryPanel": { + "description": "", + "meta": { + "connectedDashboards": 3, + "created": "2022-04-28T12:36:10Z", + "createdBy": { + "avatarUrl": "/avatar/579fc54abdc9ab34fb4865322f2870a1", + "id": 13, + "name": "mike.johnson@grafana.com" + }, + "folderName": "mj", + "folderUid": "R0bMCcW7z", + "updated": "2022-05-17T19:18:36Z", + "updatedBy": { + "avatarUrl": "/avatar/579fc54abdc9ab34fb4865322f2870a1", + "id": 13, + "name": "mike.johnson@grafana.com" + } + }, + "name": "Dashboard Makeover - Lighting Status", + "type": "table", + "uid": "u97RX_Q7z", + "version": 2 + }, + "links": [], + "options": { + "footer": { + "fields": "", + "reducer": ["sum"], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "8.5.5", + "targets": [ + { + "cacheDurationSeconds": 0, + "datasource": { + "type": "grafana-googlesheets-datasource", + "uid": "uL86Byf4k" + }, + "expr": "web_http_5xx_errors", + "legendFormat": "500", + "range": "Lighting!A1:BW2", + "refId": "A", + "spreadsheet": "1uoZrXcnv_ZeKoLk56eaBdTOI8kMYaulZdMxFNQUxmGs", + "useTimeFilter": false + } + ], + "title": "Lighting Status", + "transformations": [], + "type": "table" + }, + { + "datasource": { + "type": "testdata", + "uid": "EJHgYyf4z" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 1, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "super-light-purple", + "value": null + }, + { + "color": "red", + "value": 380 + } + ] + }, + "unit": "fahrenheit" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 5, + "y": 12 + }, + "id": 16, + "interval": "", + "libraryPanel": { + "description": "", + "meta": { + "connectedDashboards": 3, + "created": "2022-05-17T19:50:27Z", + "createdBy": { + "avatarUrl": "/avatar/579fc54abdc9ab34fb4865322f2870a1", + "id": 13, + "name": "mike.johnson@grafana.com" + }, + "folderName": "mj", + "folderUid": "R0bMCcW7z", + "updated": "2022-05-17T19:50:27Z", + "updatedBy": { + "avatarUrl": "/avatar/579fc54abdc9ab34fb4865322f2870a1", + "id": 13, + "name": "mike.johnson@grafana.com" + } + }, + "name": "Extreme Dashboard Makeover - Mac Oven", + "type": "stat", + "uid": "VvzpJ5X7z", + "version": 1 + }, + "links": [], + "options": { + "colorMode": "background", + "graphMode": "none", + "justifyMode": "center", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "textMode": "value_and_name" + }, + "pluginVersion": "8.5.5", + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "EJHgYyf4z" + }, + "max": 377, + "min": 373, + "refId": "A", + "scenarioId": "random_walk", + "startValue": 375 + } + ], + "title": "Mac Oven", + "transformations": [], + "transparent": true, + "type": "stat" + }, + { + "datasource": { + "type": "testdata", + "uid": "EJHgYyf4z" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 1, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 11, + "w": 11, + "x": 13, + "y": 12 + }, + "id": 19, + "interval": "", + "libraryPanel": { + "description": "", + "meta": { + "connectedDashboards": 3, + "created": "2022-05-17T19:55:14Z", + "createdBy": { + "avatarUrl": "/avatar/579fc54abdc9ab34fb4865322f2870a1", + "id": 13, + "name": "mike.johnson@grafana.com" + }, + "folderName": "mj", + "folderUid": "R0bMCcW7z", + "updated": "2022-05-17T20:57:48Z", + "updatedBy": { + "avatarUrl": "/avatar/579fc54abdc9ab34fb4865322f2870a1", + "id": 13, + "name": "mike.johnson@grafana.com" + } + }, + "name": "Extreme Dashboard Makeover - Salmon Cooking Times, past 7 days", + "type": "gauge", + "uid": "fwwLJ5X7k", + "version": 4 + }, + "links": [], + "options": { + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true + }, + "pluginVersion": "8.5.5", + "targets": [ + { + "alias": "Rick", + "datasource": { + "type": "testdata", + "uid": "EJHgYyf4z" + }, + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "147,167,171,144,159,166" + }, + { + "alias": "Jose", + "datasource": { + "type": "testdata", + "uid": "EJHgYyf4z" + }, + "refId": "B", + "scenarioId": "csv_metric_values", + "stringInput": "141,144,139,141,140,140,149" + }, + { + "alias": "Kaitlyn", + "datasource": { + "type": "testdata", + "uid": "EJHgYyf4z" + }, + "refId": "C", + "scenarioId": "csv_metric_values", + "stringInput": "151,150,150,151,150,150,149" + }, + { + "alias": "Raj", + "datasource": { + "type": "testdata", + "uid": "EJHgYyf4z" + }, + "refId": "D", + "scenarioId": "csv_metric_values", + "stringInput": "147,150,155,148,154,146,149" + }, + { + "alias": "Lee", + "datasource": { + "type": "testdata", + "uid": "EJHgYyf4z" + }, + "refId": "E", + "scenarioId": "csv_metric_values", + "stringInput": "153,147,155,152,156,150,149" + } + ], + "title": "Salmon Cooking Times per side, past 7 days", + "transformations": [], + "type": "gauge" + }, + { + "datasource": { + "type": "testdata", + "uid": "EJHgYyf4z" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "blue", + "value": null + }, + { + "color": "green", + "value": 35 + }, + { + "color": "red", + "value": 39 + } + ] + }, + "unit": "fahrenheit" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 5, + "y": 19 + }, + "id": 18, + "interval": "", + "libraryPanel": { + "description": "", + "meta": { + "connectedDashboards": 3, + "created": "2022-05-17T19:47:32Z", + "createdBy": { + "avatarUrl": "/avatar/579fc54abdc9ab34fb4865322f2870a1", + "id": 13, + "name": "mike.johnson@grafana.com" + }, + "folderName": "mj", + "folderUid": "R0bMCcW7z", + "updated": "2022-05-17T19:47:51Z", + "updatedBy": { + "avatarUrl": "/avatar/579fc54abdc9ab34fb4865322f2870a1", + "id": 13, + "name": "mike.johnson@grafana.com" + } + }, + "name": "Extreme Dashboard Makeover - Refrigerator Temperature (F)", + "type": "stat", + "uid": "kGPSJcu7z", + "version": 2 + }, + "links": [], + "options": { + "colorMode": "background", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "8.5.5", + "targets": [ + { + "alias": "Grill", + "datasource": { + "type": "testdata", + "uid": "EJHgYyf4z" + }, + "max": 38, + "min": 33, + "refId": "A", + "scenarioId": "random_walk", + "startValue": 33 + } + ], + "title": "Fridge Temp (F)", + "transformations": [], + "transparent": true, + "type": "stat" + }, + { + "datasource": { + "type": "grafana-googlesheets-datasource", + "uid": "uL86Byf4k" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "displayMode": "auto", + "inspect": false + }, + "decimals": 1, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "m" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Image" + }, + "properties": [ + { + "id": "custom.displayMode", + "value": "image" + }, + { + "id": "custom.width", + "value": 72 + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 11, + "x": 13, + "y": 23 + }, + "id": 9, + "interval": "", + "libraryPanel": { + "description": "", + "meta": { + "connectedDashboards": 3, + "created": "2022-04-27T21:27:23Z", + "createdBy": { + "avatarUrl": "/avatar/579fc54abdc9ab34fb4865322f2870a1", + "id": 13, + "name": "mike.johnson@grafana.com" + }, + "folderName": "mj", + "folderUid": "R0bMCcW7z", + "updated": "2022-04-27T21:29:50Z", + "updatedBy": { + "avatarUrl": "/avatar/579fc54abdc9ab34fb4865322f2870a1", + "id": 13, + "name": "mike.johnson@grafana.com" + } + }, + "name": "Dashboard Makeover - Extra Cleaning Duty Assignment Today", + "type": "table", + "uid": "T47RSwQnz", + "version": 2 + }, + "links": [], + "options": { + "footer": { + "fields": "", + "reducer": ["sum"], + "show": false + }, + "showHeader": true, + "sortBy": [] + }, + "pluginVersion": "8.5.0", + "targets": [ + { + "cacheDurationSeconds": 0, + "datasource": { + "type": "grafana-googlesheets-datasource", + "uid": "uL86Byf4k" + }, + "expr": "web_http_5xx_errors", + "legendFormat": "500", + "range": "ExtraCleaningDuty!G1:J6", + "refId": "A", + "spreadsheet": "1uoZrXcnv_ZeKoLk56eaBdTOI8kMYaulZdMxFNQUxmGs", + "useTimeFilter": false + } + ], + "title": "Extra Cleaning Duty Assignment Today", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "Value": true + }, + "indexByName": {}, + "renameByName": { + "Duty": "" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "testdata", + "uid": "EJHgYyf4z" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 1, + "mappings": [ + { + "options": { + "4000": { + "index": 0, + "text": "Max Time Allotted" + } + }, + "type": "value" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "#EAB839", + "value": 4000 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 11, + "w": 8, + "x": 5, + "y": 27 + }, + "id": 4, + "interval": "6h", + "libraryPanel": { + "description": "", + "meta": { + "connectedDashboards": 3, + "created": "2022-04-27T18:49:19Z", + "createdBy": { + "avatarUrl": "/avatar/579fc54abdc9ab34fb4865322f2870a1", + "id": 13, + "name": "mike.johnson@grafana.com" + }, + "folderName": "mj", + "folderUid": "R0bMCcW7z", + "updated": "2022-05-20T18:43:44Z", + "updatedBy": { + "avatarUrl": "/avatar/579fc54abdc9ab34fb4865322f2870a1", + "id": 13, + "name": "mike.johnson@grafana.com" + } + }, + "name": "Dashboard Makeover - Side Dish Prep Times, past 7 days", + "type": "gauge", + "uid": "WDs3jUQ7k", + "version": 9 + }, + "links": [], + "options": { + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true + }, + "pluginVersion": "8.5.3", + "targets": [ + { + "alias": "Rick", + "datasource": { + "type": "testdata", + "uid": "EJHgYyf4z" + }, + "max": 5400, + "min": 3800, + "refId": "A", + "scenarioId": "random_walk", + "spread": 100, + "startValue": 4500 + }, + { + "alias": "Jose", + "datasource": { + "type": "testdata", + "uid": "EJHgYyf4z" + }, + "hide": false, + "max": 3800, + "min": 3400, + "refId": "B", + "scenarioId": "random_walk", + "spread": 50, + "startValue": 3600 + }, + { + "alias": "Kaitlyn", + "datasource": { + "type": "testdata", + "uid": "EJHgYyf4z" + }, + "hide": false, + "max": 3800, + "min": 3400, + "refId": "C", + "scenarioId": "random_walk", + "spread": 50, + "startValue": 3600 + }, + { + "alias": "Raj", + "datasource": { + "type": "testdata", + "uid": "EJHgYyf4z" + }, + "hide": false, + "max": 3800, + "min": 3400, + "refId": "D", + "scenarioId": "random_walk", + "spread": 50, + "startValue": 3600 + }, + { + "alias": "Lee", + "datasource": { + "type": "testdata", + "uid": "EJHgYyf4z" + }, + "hide": false, + "max": 3800, + "min": 3400, + "refId": "E", + "scenarioId": "random_walk", + "spread": 50, + "startValue": 3600 + } + ], + "title": "Side Dish Prep Times, past 7 days", + "transformations": [], + "type": "gauge" + }, + { + "datasource": { + "type": "testdata", + "uid": "EJHgYyf4z" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "Time until we need new soup in the number of overall minutes", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "bars", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 1, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 11, + "x": 13, + "y": 31 + }, + "id": 8, + "interval": "", + "links": [], + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "8.5.0", + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "EJHgYyf4z" + }, + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "1470,1410,1350,1290,1170,1110,1050,990,930,870,810,750,690,630,570,510,450,390,330,270,210,150,90" + } + ], + "timeFrom": "24h", + "timeShift": "1d", + "title": "Soup: Minutes until we need to throw this soup out and make some new soup for our guests/customers", + "transformations": [], + "type": "timeseries" + } + ], + "refresh": "", + "schemaVersion": 36, + "style": "dark", + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-7d", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Dashboard Makeover Challenge", + "uid": "F3eInwQ7z", + "version": 1, + "weekStart": "" +} diff --git a/testing_data/datasources/google-sheets.json b/testing_data/datasources/google-sheets.json index 09076426..1e122f74 100644 --- a/testing_data/datasources/google-sheets.json +++ b/testing_data/datasources/google-sheets.json @@ -1,18 +1,19 @@ { - "id": 56, + "id": 1, "orgId": 1, - "uid": "HVAZDb8nz", + "uid": "uL86Byf4k", "name": "Google Sheets", "type": "grafana-googlesheets-datasource", - "typeLogoUrl": "public/img/icn-datasource.svg", + "typeLogoUrl": "public/plugins/grafana-googlesheets-datasource/img/sheets.svg", "access": "proxy", "url": "", - "password": "", "user": "", "database": "", "basicAuth": false, "readOnly": false, - "isDefault": false, - "jsonData": {}, + "isDefault": true, + "jsonData": { + "authType": "key" + }, "secureJsonData": null } \ No newline at end of file diff --git a/testing_data/libraryelements/General/dashboard-makeover-extra-cleaning-duty-assignment-today.json b/testing_data/libraryelements/General/dashboard-makeover-extra-cleaning-duty-assignment-today.json new file mode 100644 index 00000000..bcde5d6c --- /dev/null +++ b/testing_data/libraryelements/General/dashboard-makeover-extra-cleaning-duty-assignment-today.json @@ -0,0 +1,131 @@ +{ + "id": 29, + "kind": 1, + "meta": { + "created": "2023-03-30T17:18:08.000Z", + "createdBy": { + "avatarUrl": "/avatar/46d229b033af06a191ff2267bca9ae56", + "id": 1, + "name": "admin" + }, + "folderName": "General", + "updated": "2023-03-30T17:18:08.000Z", + "updatedBy": { + "avatarUrl": "/avatar/46d229b033af06a191ff2267bca9ae56", + "id": 1, + "name": "admin" + } + }, + "model": { + "datasource": { + "type": "grafana-googlesheets-datasource", + "uid": "uL86Byf4k" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "displayMode": "auto", + "inspect": false + }, + "decimals": 1, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "m" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Image" + }, + "properties": [ + { + "id": "custom.displayMode", + "value": "image" + }, + { + "id": "custom.width", + "value": 72 + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 11, + "x": 13, + "y": 23 + }, + "id": 9, + "interval": "", + "libraryPanel": { + "uid": "T47RSwQnz" + }, + "links": [], + "options": { + "footer": { + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "sortBy": [] + }, + "pluginVersion": "8.5.0", + "targets": [ + { + "cacheDurationSeconds": 0, + "datasource": { + "type": "grafana-googlesheets-datasource", + "uid": "uL86Byf4k" + }, + "expr": "web_http_5xx_errors", + "legendFormat": "500", + "range": "ExtraCleaningDuty!G1:J6", + "refId": "A", + "spreadsheet": "1uoZrXcnv_ZeKoLk56eaBdTOI8kMYaulZdMxFNQUxmGs", + "useTimeFilter": false + } + ], + "title": "Extra Cleaning Duty Assignment Today", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "Value": true + }, + "indexByName": {}, + "renameByName": { + "Duty": "" + } + } + } + ], + "type": "table" + }, + "name": "Dashboard Makeover - Extra Cleaning Duty Assignment Today", + "orgId": 1, + "type": "table", + "uid": "T47RSwQnz", + "version": 1 +} \ No newline at end of file diff --git a/testing_data/libraryelements/General/dashboard-makeover-lighting-status.json b/testing_data/libraryelements/General/dashboard-makeover-lighting-status.json new file mode 100644 index 00000000..d7e746ef --- /dev/null +++ b/testing_data/libraryelements/General/dashboard-makeover-lighting-status.json @@ -0,0 +1,118 @@ +{ + "id": 30, + "kind": 1, + "meta": { + "created": "2023-03-30T17:18:08.000Z", + "createdBy": { + "avatarUrl": "/avatar/46d229b033af06a191ff2267bca9ae56", + "id": 1, + "name": "admin" + }, + "folderName": "General", + "updated": "2023-03-30T17:18:08.000Z", + "updatedBy": { + "avatarUrl": "/avatar/46d229b033af06a191ff2267bca9ae56", + "id": 1, + "name": "admin" + } + }, + "model": { + "datasource": { + "type": "grafana-googlesheets-datasource", + "uid": "uL86Byf4k" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "left", + "displayMode": "auto", + "inspect": true + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 35 + }, + { + "color": "red", + "value": 60 + } + ] + }, + "unit": "none" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Purchase Date" + }, + "properties": [ + { + "id": "unit", + "value": "dateTimeFromNow" + } + ] + } + ] + }, + "gridPos": { + "h": 4, + "w": 19, + "x": 5, + "y": 8 + }, + "id": 10, + "interval": "", + "libraryPanel": { + "uid": "u97RX_Q7z" + }, + "links": [], + "options": { + "footer": { + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "8.5.5", + "targets": [ + { + "cacheDurationSeconds": 0, + "datasource": { + "type": "grafana-googlesheets-datasource", + "uid": "uL86Byf4k" + }, + "expr": "web_http_5xx_errors", + "legendFormat": "500", + "range": "Lighting!A1:BW2", + "refId": "A", + "spreadsheet": "1uoZrXcnv_ZeKoLk56eaBdTOI8kMYaulZdMxFNQUxmGs", + "useTimeFilter": false + } + ], + "title": "Lighting Status", + "transformations": [], + "type": "table" + }, + "name": "Dashboard Makeover - Lighting Status", + "orgId": 1, + "type": "table", + "uid": "u97RX_Q7z", + "version": 1 +} \ No newline at end of file diff --git a/testing_data/libraryelements/General/dashboard-makeover-side-dish-prep-times-past-7-days.json b/testing_data/libraryelements/General/dashboard-makeover-side-dish-prep-times-past-7-days.json new file mode 100644 index 00000000..e5806628 --- /dev/null +++ b/testing_data/libraryelements/General/dashboard-makeover-side-dish-prep-times-past-7-days.json @@ -0,0 +1,163 @@ +{ + "id": 31, + "kind": 1, + "meta": { + "created": "2023-03-30T17:18:08.000Z", + "createdBy": { + "avatarUrl": "/avatar/46d229b033af06a191ff2267bca9ae56", + "id": 1, + "name": "admin" + }, + "folderName": "General", + "updated": "2023-03-30T17:18:08.000Z", + "updatedBy": { + "avatarUrl": "/avatar/46d229b033af06a191ff2267bca9ae56", + "id": 1, + "name": "admin" + } + }, + "model": { + "datasource": { + "type": "testdata", + "uid": "EJHgYyf4z" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 1, + "mappings": [ + { + "options": { + "4000": { + "index": 0, + "text": "Max Time Allotted" + } + }, + "type": "value" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "#EAB839", + "value": 4000 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 11, + "w": 8, + "x": 5, + "y": 27 + }, + "id": 4, + "interval": "6h", + "libraryPanel": { + "uid": "WDs3jUQ7k" + }, + "links": [], + "options": { + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true + }, + "pluginVersion": "8.5.3", + "targets": [ + { + "alias": "Rick", + "datasource": { + "type": "testdata", + "uid": "EJHgYyf4z" + }, + "max": 5400, + "min": 3800, + "refId": "A", + "scenarioId": "random_walk", + "spread": 100, + "startValue": 4500 + }, + { + "alias": "Jose", + "datasource": { + "type": "testdata", + "uid": "EJHgYyf4z" + }, + "hide": false, + "max": 3800, + "min": 3400, + "refId": "B", + "scenarioId": "random_walk", + "spread": 50, + "startValue": 3600 + }, + { + "alias": "Kaitlyn", + "datasource": { + "type": "testdata", + "uid": "EJHgYyf4z" + }, + "hide": false, + "max": 3800, + "min": 3400, + "refId": "C", + "scenarioId": "random_walk", + "spread": 50, + "startValue": 3600 + }, + { + "alias": "Raj", + "datasource": { + "type": "testdata", + "uid": "EJHgYyf4z" + }, + "hide": false, + "max": 3800, + "min": 3400, + "refId": "D", + "scenarioId": "random_walk", + "spread": 50, + "startValue": 3600 + }, + { + "alias": "Lee", + "datasource": { + "type": "testdata", + "uid": "EJHgYyf4z" + }, + "hide": false, + "max": 3800, + "min": 3400, + "refId": "E", + "scenarioId": "random_walk", + "spread": 50, + "startValue": 3600 + } + ], + "title": "Side Dish Prep Times, past 7 days", + "transformations": [], + "type": "gauge" + }, + "name": "Dashboard Makeover - Side Dish Prep Times, past 7 days", + "orgId": 1, + "type": "gauge", + "uid": "WDs3jUQ7k", + "version": 1 +} \ No newline at end of file diff --git a/testing_data/libraryelements/General/dashboard-makeover-time-since-we-purchased-these-spices.json b/testing_data/libraryelements/General/dashboard-makeover-time-since-we-purchased-these-spices.json new file mode 100644 index 00000000..6274af57 --- /dev/null +++ b/testing_data/libraryelements/General/dashboard-makeover-time-since-we-purchased-these-spices.json @@ -0,0 +1,124 @@ +{ + "id": 32, + "kind": 1, + "meta": { + "created": "2023-03-30T17:18:08.000Z", + "createdBy": { + "avatarUrl": "/avatar/46d229b033af06a191ff2267bca9ae56", + "id": 1, + "name": "admin" + }, + "folderName": "General", + "updated": "2023-03-30T17:18:08.000Z", + "updatedBy": { + "avatarUrl": "/avatar/46d229b033af06a191ff2267bca9ae56", + "id": 1, + "name": "admin" + } + }, + "model": { + "datasource": { + "type": "grafana-googlesheets-datasource", + "uid": "uL86Byf4k" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "displayMode": "auto", + "inspect": true + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 35 + }, + { + "color": "red", + "value": 60 + } + ] + }, + "unit": "none" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Purchase Date" + }, + "properties": [ + { + "id": "unit", + "value": "dateTimeFromNow" + } + ] + } + ] + }, + "gridPos": { + "h": 30, + "w": 5, + "x": 0, + "y": 8 + }, + "id": 2, + "interval": "", + "libraryPanel": { + "uid": "1D_h3UQ7k" + }, + "links": [ + { + "targetBlank": true, + "title": "Raw Data", + "url": "https://docs.google.com/spreadsheets/d/1uoZrXcnv_ZeKoLk56eaBdTOI8kMYaulZdMxFNQUxmGs/edit#gid=527121891" + } + ], + "options": { + "footer": { + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "8.5.5", + "targets": [ + { + "cacheDurationSeconds": 0, + "datasource": { + "type": "grafana-googlesheets-datasource", + "uid": "uL86Byf4k" + }, + "expr": "web_http_5xx_errors", + "legendFormat": "500", + "range": "SpicesAge!A1:B32", + "refId": "A", + "spreadsheet": "1uoZrXcnv_ZeKoLk56eaBdTOI8kMYaulZdMxFNQUxmGs", + "useTimeFilter": false + } + ], + "title": "Time since we purchased these spices", + "transformations": [], + "type": "table" + }, + "name": "Dashboard Makeover - Time since we purchased these spices", + "orgId": 1, + "type": "table", + "uid": "1D_h3UQ7k", + "version": 1 +} \ No newline at end of file diff --git a/testing_data/libraryelements/General/extreme-dashboard-makeover-grill.json b/testing_data/libraryelements/General/extreme-dashboard-makeover-grill.json new file mode 100644 index 00000000..4baf8813 --- /dev/null +++ b/testing_data/libraryelements/General/extreme-dashboard-makeover-grill.json @@ -0,0 +1,127 @@ +{ + "id": 33, + "kind": 1, + "meta": { + "created": "2023-03-30T17:18:08.000Z", + "createdBy": { + "avatarUrl": "/avatar/46d229b033af06a191ff2267bca9ae56", + "id": 1, + "name": "admin" + }, + "folderName": "General", + "updated": "2023-03-30T17:18:08.000Z", + "updatedBy": { + "avatarUrl": "/avatar/46d229b033af06a191ff2267bca9ae56", + "id": 1, + "name": "admin" + } + }, + "model": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "testdata", + "uid": "EJHgYyf4z" + }, + "description": "", + "fieldConfig": { + "defaults": { + "unit": "fahrenheit" + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "hiddenSeries": false, + "id": 13, + "interval": "", + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "libraryPanel": { + "uid": "y1C0A5unz" + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "8.5.5", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "Grill", + "datasource": { + "type": "testdata", + "uid": "EJHgYyf4z" + }, + "max": 394, + "min": 380, + "refId": "A", + "scenarioId": "random_walk", + "startValue": 383 + } + ], + "thresholds": [], + "timeRegions": [], + "title": "Grill", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "transformations": [], + "transparent": true, + "type": "graph", + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:63", + "format": "fahrenheit", + "logBase": 1, + "show": true + }, + { + "$$hashKey": "object:64", + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + }, + "name": "Extreme Dashboard Makeover - Grill", + "orgId": 1, + "type": "graph", + "uid": "y1C0A5unz", + "version": 1 +} \ No newline at end of file diff --git a/testing_data/libraryelements/General/extreme-dashboard-makeover-mac-oven.json b/testing_data/libraryelements/General/extreme-dashboard-makeover-mac-oven.json new file mode 100644 index 00000000..b00c17c2 --- /dev/null +++ b/testing_data/libraryelements/General/extreme-dashboard-makeover-mac-oven.json @@ -0,0 +1,99 @@ +{ + "id": 34, + "kind": 1, + "meta": { + "created": "2023-03-30T17:18:09.000Z", + "createdBy": { + "avatarUrl": "/avatar/46d229b033af06a191ff2267bca9ae56", + "id": 1, + "name": "admin" + }, + "folderName": "General", + "updated": "2023-03-30T17:18:09.000Z", + "updatedBy": { + "avatarUrl": "/avatar/46d229b033af06a191ff2267bca9ae56", + "id": 1, + "name": "admin" + } + }, + "model": { + "datasource": { + "type": "testdata", + "uid": "EJHgYyf4z" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 1, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "super-light-purple", + "value": null + }, + { + "color": "red", + "value": 380 + } + ] + }, + "unit": "fahrenheit" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 5, + "y": 12 + }, + "id": 16, + "interval": "", + "libraryPanel": { + "uid": "VvzpJ5X7z" + }, + "links": [], + "options": { + "colorMode": "background", + "graphMode": "none", + "justifyMode": "center", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "value_and_name" + }, + "pluginVersion": "8.5.5", + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "EJHgYyf4z" + }, + "max": 377, + "min": 373, + "refId": "A", + "scenarioId": "random_walk", + "startValue": 375 + } + ], + "title": "Mac Oven", + "transformations": [], + "transparent": true, + "type": "stat" + }, + "name": "Extreme Dashboard Makeover - Mac Oven", + "orgId": 1, + "type": "stat", + "uid": "VvzpJ5X7z", + "version": 1 +} \ No newline at end of file diff --git a/testing_data/libraryelements/General/extreme-dashboard-makeover-refrigerator-temperature-f.json b/testing_data/libraryelements/General/extreme-dashboard-makeover-refrigerator-temperature-f.json new file mode 100644 index 00000000..ff31697b --- /dev/null +++ b/testing_data/libraryelements/General/extreme-dashboard-makeover-refrigerator-temperature-f.json @@ -0,0 +1,103 @@ +{ + "id": 35, + "kind": 1, + "meta": { + "created": "2023-03-30T17:18:09.000Z", + "createdBy": { + "avatarUrl": "/avatar/46d229b033af06a191ff2267bca9ae56", + "id": 1, + "name": "admin" + }, + "folderName": "General", + "updated": "2023-03-30T17:18:09.000Z", + "updatedBy": { + "avatarUrl": "/avatar/46d229b033af06a191ff2267bca9ae56", + "id": 1, + "name": "admin" + } + }, + "model": { + "datasource": { + "type": "testdata", + "uid": "EJHgYyf4z" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "blue", + "value": null + }, + { + "color": "green", + "value": 35 + }, + { + "color": "red", + "value": 39 + } + ] + }, + "unit": "fahrenheit" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 5, + "y": 19 + }, + "id": 18, + "interval": "", + "libraryPanel": { + "uid": "kGPSJcu7z" + }, + "links": [], + "options": { + "colorMode": "background", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "8.5.5", + "targets": [ + { + "alias": "Grill", + "datasource": { + "type": "testdata", + "uid": "EJHgYyf4z" + }, + "max": 38, + "min": 33, + "refId": "A", + "scenarioId": "random_walk", + "startValue": 33 + } + ], + "title": "Fridge Temp (F)", + "transformations": [], + "transparent": true, + "type": "stat" + }, + "name": "Extreme Dashboard Makeover - Refrigerator Temperature (F)", + "orgId": 1, + "type": "stat", + "uid": "kGPSJcu7z", + "version": 1 +} \ No newline at end of file diff --git a/testing_data/libraryelements/General/extreme-dashboard-makeover-room-temperature-f.json b/testing_data/libraryelements/General/extreme-dashboard-makeover-room-temperature-f.json new file mode 100644 index 00000000..20dc1c11 --- /dev/null +++ b/testing_data/libraryelements/General/extreme-dashboard-makeover-room-temperature-f.json @@ -0,0 +1,233 @@ +{ + "id": 36, + "kind": 1, + "meta": { + "created": "2023-03-30T17:18:09.000Z", + "createdBy": { + "avatarUrl": "/avatar/46d229b033af06a191ff2267bca9ae56", + "id": 1, + "name": "admin" + }, + "folderName": "General", + "updated": "2023-03-30T17:18:09.000Z", + "updatedBy": { + "avatarUrl": "/avatar/46d229b033af06a191ff2267bca9ae56", + "id": 1, + "name": "admin" + } + }, + "model": { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "testdata", + "uid": "EJHgYyf4z" + }, + "description": "", + "fieldConfig": { + "defaults": { + "unit": "fahrenheit" + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "hiddenSeries": false, + "id": 14, + "interval": "", + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "libraryPanel": { + "uid": "3dSe05X7z" + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "8.5.5", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "Zone 1", + "datasource": { + "type": "testdata", + "uid": "EJHgYyf4z" + }, + "max": 70, + "min": 67, + "refId": "A", + "scenarioId": "random_walk", + "spread": 0.5, + "startValue": 68 + }, + { + "alias": "Zone 2", + "datasource": { + "type": "testdata", + "uid": "EJHgYyf4z" + }, + "hide": false, + "max": 71, + "min": 69, + "noise": 0.1, + "refId": "B", + "scenarioId": "random_walk", + "spread": 0.5, + "startValue": 70 + }, + { + "alias": "Zone 3", + "datasource": { + "type": "testdata", + "uid": "EJHgYyf4z" + }, + "hide": false, + "max": 71, + "min": 69, + "noise": 0.1, + "refId": "C", + "scenarioId": "random_walk", + "spread": 0.5, + "startValue": 70 + }, + { + "alias": "Zone 4", + "datasource": { + "type": "testdata", + "uid": "EJHgYyf4z" + }, + "hide": false, + "max": 71, + "min": 69, + "noise": 0.1, + "refId": "D", + "scenarioId": "random_walk", + "spread": 0.5, + "startValue": 70 + }, + { + "alias": "Zone 5", + "datasource": { + "type": "testdata", + "uid": "EJHgYyf4z" + }, + "hide": false, + "max": 71, + "min": 69, + "noise": 0.1, + "refId": "E", + "scenarioId": "random_walk", + "spread": 0.5, + "startValue": 70 + }, + { + "alias": "Zone 6", + "datasource": { + "type": "testdata", + "uid": "EJHgYyf4z" + }, + "hide": false, + "max": 71, + "min": 69, + "noise": 0.1, + "refId": "F", + "scenarioId": "random_walk", + "spread": 0.5, + "startValue": 70 + }, + { + "alias": "Zone 7", + "datasource": { + "type": "testdata", + "uid": "EJHgYyf4z" + }, + "hide": false, + "max": 71, + "min": 69, + "noise": 0.1, + "refId": "G", + "scenarioId": "random_walk", + "spread": 0.5, + "startValue": 70 + }, + { + "alias": "Zone 8", + "datasource": { + "type": "testdata", + "uid": "EJHgYyf4z" + }, + "hide": false, + "max": 71, + "min": 69, + "noise": 0.1, + "refId": "H", + "scenarioId": "random_walk", + "spread": 0.5, + "startValue": 70 + } + ], + "thresholds": [], + "timeRegions": [], + "title": "Room Temperature (F)", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "transformations": [], + "transparent": true, + "type": "graph", + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:63", + "format": "fahrenheit", + "logBase": 1, + "show": true + }, + { + "$$hashKey": "object:64", + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + }, + "name": "Extreme Dashboard Makeover - Room Temperature (F)", + "orgId": 1, + "type": "graph", + "uid": "3dSe05X7z", + "version": 1 +} \ No newline at end of file diff --git a/testing_data/libraryelements/General/extreme-dashboard-makeover-salmon-cooking-times-past-7-days.json b/testing_data/libraryelements/General/extreme-dashboard-makeover-salmon-cooking-times-past-7-days.json new file mode 100644 index 00000000..6041aa04 --- /dev/null +++ b/testing_data/libraryelements/General/extreme-dashboard-makeover-salmon-cooking-times-past-7-days.json @@ -0,0 +1,131 @@ +{ + "id": 37, + "kind": 1, + "meta": { + "created": "2023-03-30T17:18:09.000Z", + "createdBy": { + "avatarUrl": "/avatar/46d229b033af06a191ff2267bca9ae56", + "id": 1, + "name": "admin" + }, + "folderName": "General", + "updated": "2023-03-30T17:18:09.000Z", + "updatedBy": { + "avatarUrl": "/avatar/46d229b033af06a191ff2267bca9ae56", + "id": 1, + "name": "admin" + } + }, + "model": { + "datasource": { + "type": "testdata", + "uid": "EJHgYyf4z" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 1, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 11, + "w": 11, + "x": 13, + "y": 12 + }, + "id": 19, + "interval": "", + "libraryPanel": { + "uid": "fwwLJ5X7k" + }, + "links": [], + "options": { + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true + }, + "pluginVersion": "8.5.5", + "targets": [ + { + "alias": "Rick", + "datasource": { + "type": "testdata", + "uid": "EJHgYyf4z" + }, + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "147,167,171,144,159,166" + }, + { + "alias": "Jose", + "datasource": { + "type": "testdata", + "uid": "EJHgYyf4z" + }, + "refId": "B", + "scenarioId": "csv_metric_values", + "stringInput": "141,144,139,141,140,140,149" + }, + { + "alias": "Kaitlyn", + "datasource": { + "type": "testdata", + "uid": "EJHgYyf4z" + }, + "refId": "C", + "scenarioId": "csv_metric_values", + "stringInput": "151,150,150,151,150,150,149" + }, + { + "alias": "Raj", + "datasource": { + "type": "testdata", + "uid": "EJHgYyf4z" + }, + "refId": "D", + "scenarioId": "csv_metric_values", + "stringInput": "147,150,155,148,154,146,149" + }, + { + "alias": "Lee", + "datasource": { + "type": "testdata", + "uid": "EJHgYyf4z" + }, + "refId": "E", + "scenarioId": "csv_metric_values", + "stringInput": "153,147,155,152,156,150,149" + } + ], + "title": "Salmon Cooking Times per side, past 7 days", + "transformations": [], + "type": "gauge" + }, + "name": "Extreme Dashboard Makeover - Salmon Cooking Times, past 7 days", + "orgId": 1, + "type": "gauge", + "uid": "fwwLJ5X7k", + "version": 1 +} \ No newline at end of file diff --git a/version/version.go b/version/version.go index a892e22e..08639193 100644 --- a/version/version.go +++ b/version/version.go @@ -9,7 +9,7 @@ import ( var GitCommit string // Version returns the main version number that is being run at the moment. -const Version = "0.3.2" +const Version = "0.4.0" // BuildDate returns the date the binary was built var BuildDate = ""