Previews: remove dashboard previews backend (#66176)

* remove dashboard previews backend

* remove dashboard previews backend

* bring back the migration

* bring back the migration

* bring back the migration
This commit is contained in:
Artur Wierzbicki
2023-04-13 21:42:24 +04:00
committed by GitHub
parent e0b2aeffa3
commit 2136e680c4
37 changed files with 45 additions and 2695 deletions

View File

@@ -479,12 +479,6 @@ func (hs *HTTPServer) registerRoutes() {
})
})
dashboardRoute.Group("/uid/:uid", func(dashUidRoute routing.RouteRegister) {
if hs.ThumbService != nil {
dashUidRoute.Get("/img/:kind/:theme", hs.ThumbService.GetImage)
}
})
dashboardRoute.Post("/calculate-diff", authorize(reqSignedIn, ac.EvalPermission(dashboards.ActionDashboardsWrite)), routing.Wrap(hs.CalculateDashboardDiff))
dashboardRoute.Post("/validate", authorize(reqSignedIn, ac.EvalPermission(dashboards.ActionDashboardsWrite)), routing.Wrap(hs.ValidateDashboard))
dashboardRoute.Post("/trim", routing.Wrap(hs.TrimDashboard))

View File

@@ -64,16 +64,6 @@ type FrontendSettingsUnifiedAlertingDTO struct {
MinInterval string `json:"minInterval"`
}
type DashboardPreviewsSystemRequirements struct {
Met bool `json:"met"`
RequiredImageRendererPluginVersion string `json:"requiredImageRendererPluginVersion"`
}
type DashboardPreviewsSetupConfig struct {
SystemRequirements DashboardPreviewsSystemRequirements `json:"systemRequirements"`
ThumbnailsExist bool `json:"thumbnailsExist"`
}
// Enterprise-only
type FrontendSettingsLicensingDTO struct {
Slug *string `json:"slug,omitempty"`
@@ -210,8 +200,6 @@ type FrontendSettingsDTO struct {
SamlName string `json:"samlName"`
TokenExpirationDayLimit int `json:"tokenExpirationDayLimit"`
DashboardPreviews DashboardPreviewsSetupConfig `json:"dashboardPreviews,omitempty"`
GeomapDefaultBaseLayerConfig *map[string]interface{} `json:"geomapDefaultBaseLayerConfig,omitempty"`
GeomapDisableCustomBaseLayer bool `json:"geomapDisableCustomBaseLayer"`

View File

@@ -233,10 +233,6 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro
frontendSettings.PluginsCDNBaseURL = cdnBaseURL
}
if hs.ThumbService != nil {
frontendSettings.DashboardPreviews = hs.ThumbService.GetDashboardPreviewsSetupSettings(c)
}
if hs.Cfg.GeomapDefaultBaseLayerConfig != nil {
frontendSettings.GeomapDefaultBaseLayerConfig = &hs.Cfg.GeomapDefaultBaseLayerConfig
}

View File

@@ -96,7 +96,6 @@ import (
"github.com/grafana/grafana/pkg/services/team"
"github.com/grafana/grafana/pkg/services/teamguardian"
tempUser "github.com/grafana/grafana/pkg/services/temp_user"
"github.com/grafana/grafana/pkg/services/thumbs"
"github.com/grafana/grafana/pkg/services/updatechecker"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/services/validations"
@@ -145,7 +144,6 @@ type HTTPServer struct {
CorrelationsService correlations.Service
Live *live.GrafanaLive
LivePushGateway *pushhttp.Gateway
ThumbService thumbs.Service
StorageService store.StorageService
httpEntityStore httpentitystore.HTTPEntityStore
SearchV2HTTPService searchV2.SearchHTTPService
@@ -198,20 +196,19 @@ type HTTPServer struct {
kvStore kvstore.KVStore
pluginsCDNService *pluginscdn.Service
userService user.Service
tempUserService tempUser.Service
dashboardThumbsService thumbs.DashboardThumbService
loginAttemptService loginAttempt.Service
orgService org.Service
teamService team.Service
accesscontrolService accesscontrol.Service
annotationsRepo annotations.Repository
tagService tag.Service
oauthTokenService oauthtoken.OAuthTokenService
statsService stats.Service
authnService authn.Service
starApi *starApi.API
cachingService caching.CachingService
userService user.Service
tempUserService tempUser.Service
loginAttemptService loginAttempt.Service
orgService org.Service
teamService team.Service
accesscontrolService accesscontrol.Service
annotationsRepo annotations.Repository
tagService tag.Service
oauthTokenService oauthtoken.OAuthTokenService
statsService stats.Service
authnService authn.Service
starApi *starApi.API
cachingService caching.CachingService
}
type ServerOptions struct {
@@ -225,8 +222,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
pluginDashboardService plugindashboards.Service, pluginStore plugins.Store, pluginClient plugins.Client,
pluginErrorResolver plugins.ErrorResolver, pluginInstaller plugins.Installer, settingsProvider setting.Provider,
dataSourceCache datasources.CacheService, userTokenService auth.UserTokenService,
cleanUpService *cleanup.CleanUpService, shortURLService shorturls.Service, queryHistoryService queryhistory.Service, correlationsService correlations.Service,
thumbService thumbs.Service, remoteCache *remotecache.RemoteCache, provisioningService provisioning.ProvisioningService,
cleanUpService *cleanup.CleanUpService, shortURLService shorturls.Service, queryHistoryService queryhistory.Service, correlationsService correlations.Service, remoteCache *remotecache.RemoteCache, provisioningService provisioning.ProvisioningService,
loginService login.Service, authenticator loginpkg.Authenticator, accessControl accesscontrol.AccessControl,
dataSourceProxy *datasourceproxy.DataSourceProxyService, searchService *search.SearchService,
live *live.GrafanaLive, livePushGateway *pushhttp.Gateway, plugCtxProvider *plugincontext.Provider,
@@ -251,7 +247,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
secretsPluginMigrator spm.SecretMigrationProvider, secretsStore secretsKV.SecretsKVStore,
publicDashboardsApi *publicdashboardsApi.Api, userService user.Service, tempUserService tempUser.Service,
loginAttemptService loginAttempt.Service, orgService org.Service, teamService team.Service,
accesscontrolService accesscontrol.Service, dashboardThumbsService thumbs.DashboardThumbService, navTreeService navtree.Service,
accesscontrolService accesscontrol.Service, navTreeService navtree.Service,
annotationRepo annotations.Repository, tagService tag.Service, searchv2HTTPService searchV2.SearchHTTPService, oauthTokenService oauthtoken.OAuthTokenService,
statsService stats.Service, authnService authn.Service, pluginsCDNService *pluginscdn.Service,
starApi *starApi.API, cachingService caching.CachingService,
@@ -288,7 +284,6 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
QueryHistoryService: queryHistoryService,
CorrelationsService: correlationsService,
Features: features,
ThumbService: thumbService,
StorageService: storageService,
RemoteCacheService: remoteCache,
ProvisioningService: provisioningService,
@@ -348,7 +343,6 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
PublicDashboardsApi: publicDashboardsApi,
userService: userService,
tempUserService: tempUserService,
dashboardThumbsService: dashboardThumbsService,
loginAttemptService: loginAttemptService,
orgService: orgService,
teamService: teamService,

View File

@@ -35,7 +35,6 @@ import (
"github.com/grafana/grafana/pkg/services/store/entity"
"github.com/grafana/grafana/pkg/services/store/sanitizer"
"github.com/grafana/grafana/pkg/services/supportbundles/supportbundlesimpl"
"github.com/grafana/grafana/pkg/services/thumbs"
"github.com/grafana/grafana/pkg/services/updatechecker"
)
@@ -46,8 +45,7 @@ func ProvideBackgroundServiceRegistry(
provisioning *provisioning.ProvisioningServiceImpl, alerting *alerting.AlertEngine, usageStats *uss.UsageStats,
statsCollector *statscollector.Service, grafanaUpdateChecker *updatechecker.GrafanaService,
pluginsUpdateChecker *updatechecker.PluginsService, metrics *metrics.InternalMetricsService,
secretsService *secretsManager.SecretsService, remoteCache *remotecache.RemoteCache,
thumbnailsService thumbs.Service, StorageService store.StorageService, searchService searchV2.SearchService, entityEventsService store.EntityEventsService,
secretsService *secretsManager.SecretsService, remoteCache *remotecache.RemoteCache, StorageService store.StorageService, searchService searchV2.SearchService, entityEventsService store.EntityEventsService,
saService *samanager.ServiceAccountsService, authInfoService *authinfoservice.Implementation,
grpcServerProvider grpcserver.Provider, secretMigrationProvider secretsMigrations.SecretMigrationProvider, loginAttemptService *loginattemptimpl.Service,
bundleService *supportbundlesimpl.Service,
@@ -78,7 +76,6 @@ func ProvideBackgroundServiceRegistry(
remoteCache,
secretsService,
StorageService,
thumbnailsService,
searchService,
entityEventsService,
grpcServerProvider,

View File

@@ -3,15 +3,12 @@ package usagestatssvcs
import (
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/thumbs"
)
func ProvideUsageStatsProvidersRegistry(
thumbsService thumbs.Service,
accesscontrol accesscontrol.Service,
) *UsageStatsProvidersRegistry {
return NewUsageStatsProvidersRegistry(
thumbsService,
accesscontrol,
)
}

View File

@@ -136,8 +136,6 @@ import (
teamguardianManager "github.com/grafana/grafana/pkg/services/teamguardian/manager"
tempuser "github.com/grafana/grafana/pkg/services/temp_user"
"github.com/grafana/grafana/pkg/services/temp_user/tempuserimpl"
"github.com/grafana/grafana/pkg/services/thumbs"
"github.com/grafana/grafana/pkg/services/thumbs/dashboardthumbsimpl"
"github.com/grafana/grafana/pkg/services/updatechecker"
"github.com/grafana/grafana/pkg/services/user/userimpl"
"github.com/grafana/grafana/pkg/setting"
@@ -176,7 +174,6 @@ var wireBasicSet = wire.NewSet(
wire.Bind(new(query.Service), new(*query.ServiceImpl)),
bus.ProvideBus,
wire.Bind(new(bus.Bus), new(*bus.InProcBus)),
thumbs.ProvideService,
rendering.ProvideService,
wire.Bind(new(rendering.Service), new(*rendering.RenderingService)),
routing.ProvideRegister,
@@ -186,7 +183,6 @@ var wireBasicSet = wire.NewSet(
localcache.ProvideService,
bundleregistry.ProvideService,
wire.Bind(new(supportbundles.Service), new(*bundleregistry.Service)),
dashboardthumbsimpl.ProvideService,
updatechecker.ProvideGrafanaService,
updatechecker.ProvidePluginsService,
uss.ProvideService,

View File

@@ -36,7 +36,6 @@ import (
"github.com/grafana/grafana/pkg/services/searchusers"
"github.com/grafana/grafana/pkg/services/searchusers/filters"
"github.com/grafana/grafana/pkg/services/sqlstore/migrations"
"github.com/grafana/grafana/pkg/services/thumbs"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/services/validations"
"github.com/grafana/grafana/pkg/setting"
@@ -56,8 +55,6 @@ var wireExtsBasicSet = wire.NewSet(
wire.Bind(new(accesscontrol.RoleRegistry), new(*acimpl.Service)),
wire.Bind(new(plugins.RoleRegistry), new(*acimpl.Service)),
wire.Bind(new(accesscontrol.Service), new(*acimpl.Service)),
thumbs.ProvideCrawlerAuthSetupService,
wire.Bind(new(thumbs.CrawlerAuthSetupService), new(*thumbs.OSSCrawlerAuthSetupService)),
validations.ProvideValidator,
wire.Bind(new(validations.PluginRequestValidator), new(*validations.OSSPluginRequestValidator)),
provisioning.ProvideService,

View File

@@ -116,11 +116,6 @@ var (
StatusCode: 404,
Status: "not-found",
}
ErrDashboardThumbnailNotFound = DashboardErr{
Reason: "Dashboard thumbnail not found",
StatusCode: 404,
Status: "not-found",
}
ErrFolderNotFound = errors.New("folder not found")
ErrFolderVersionMismatch = errors.New("the folder has been changed by someone else")

View File

@@ -27,12 +27,6 @@ var (
State: FeatureStateStable,
Owner: hostedGrafanaTeam,
},
{
Name: "dashboardPreviews",
Description: "Create and show thumbnails for dashboard search results",
State: FeatureStateAlpha,
Owner: grafanaAppPlatformSquad,
},
{
Name: "live-service-web-worker",
Description: "This will use a webworker thread to processes events rather than the main thread",

View File

@@ -2,7 +2,6 @@ Name,State,Owner,requiresDevMode,RequiresLicense,RequiresRestart,FrontendOnly
trimDefaults,beta,@grafana/grafana-as-code,false,false,false,false
disableEnvelopeEncryption,stable,@grafana/grafana-as-code,false,false,false,false
database_metrics,stable,@grafana/hosted-grafana-team,false,false,false,false
dashboardPreviews,alpha,@grafana/grafana-app-platform-squad,false,false,false,false
live-service-web-worker,alpha,@grafana/grafana-app-platform-squad,false,false,false,true
queryOverLive,alpha,@grafana/grafana-app-platform-squad,false,false,false,true
panelTitleSearch,beta,@grafana/grafana-app-platform-squad,false,false,false,false
1 Name State Owner requiresDevMode RequiresLicense RequiresRestart FrontendOnly
2 trimDefaults beta @grafana/grafana-as-code false false false false
3 disableEnvelopeEncryption stable @grafana/grafana-as-code false false false false
4 database_metrics stable @grafana/hosted-grafana-team false false false false
dashboardPreviews alpha @grafana/grafana-app-platform-squad false false false false
5 live-service-web-worker alpha @grafana/grafana-app-platform-squad false false false true
6 queryOverLive alpha @grafana/grafana-app-platform-squad false false false true
7 panelTitleSearch beta @grafana/grafana-app-platform-squad false false false false

View File

@@ -19,10 +19,6 @@ const (
// Add Prometheus metrics for database tables
FlagDatabaseMetrics = "database_metrics"
// FlagDashboardPreviews
// Create and show thumbnails for dashboard search results
FlagDashboardPreviews = "dashboardPreviews"
// FlagLiveServiceWebWorker
// This will use a webworker thread to processes events rather than the main thread
FlagLiveServiceWebWorker = "live-service-web-worker"

View File

@@ -11,7 +11,6 @@ func TestFeatureUsageStats(t *testing.T) {
featureManagerWithAllFeatures := WithFeatures(
"trimDefaults",
"database_metrics",
"dashboardPreviews",
"live-config",
"UPPER_SNAKE_CASE",
"feature.with.a.dot",
@@ -20,7 +19,6 @@ func TestFeatureUsageStats(t *testing.T) {
require.Equal(t, map[string]interface{}{
"stats.features.trim_defaults.count": 1,
"stats.features.database_metrics.count": 1,
"stats.features.dashboard_previews.count": 1,
"stats.features.live_config.count": 1,
"stats.features.upper_snake_case.count": 1,
"stats.features.feature_with_a_dot.count": 1,

View File

@@ -1,31 +1,32 @@
package migrations
import "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
// This migration was run behind the `dashboardPreviews` flag.
// The feature flag and the whole dashboard previews feature was removed in https://github.com/grafana/grafana/pull/66176
func addDashboardThumbsMigrations(mg *migrator.Migrator) {
dashThumbs := migrator.Table{
Name: "dashboard_thumbnail",
Columns: []*migrator.Column{
{Name: "id", Type: migrator.DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
{Name: "dashboard_id", Type: migrator.DB_BigInt, Nullable: false}, // can join with dashboard table
{Name: "dashboard_version", Type: migrator.DB_Int, Nullable: false}, // screenshoted version of the dashboard
{Name: "state", Type: migrator.DB_NVarchar, Length: 10, Nullable: false}, // stale | locked
{Name: "panel_id", Type: migrator.DB_SmallInt, Nullable: false, Default: "0"}, // for panel thumbnails
{Name: "image", Type: migrator.DB_MediumBlob, Nullable: false}, // image stored as blob. MediumBlob has a max limit of 16mb in MySQL
{Name: "mime_type", Type: migrator.DB_NVarchar, Length: 255, Nullable: false}, // e.g. image/png, image/webp
{Name: "kind", Type: migrator.DB_NVarchar, Length: 8, Nullable: false}, // thumb | tall
{Name: "theme", Type: migrator.DB_NVarchar, Length: 8, Nullable: false}, // light|dark
{Name: "updated", Type: migrator.DB_DateTime, Nullable: false},
},
Indices: []*migrator.Index{
{Cols: []string{"dashboard_id", "panel_id", "kind", "theme"}, Type: migrator.UniqueIndex},
},
}
mg.AddMigration("create dashboard_thumbnail table", migrator.NewAddTableMigration(dashThumbs))
mg.AddMigration("add unique indexes for dashboard_thumbnail", migrator.NewAddIndexMigration(dashThumbs, dashThumbs.Indices[0]))
mg.AddMigration("Add ds_uids column to dashboard_thumbnail table", migrator.NewAddColumnMigration(dashThumbs,
// uids of datasources used in the dashboard when taking preview
&migrator.Column{Name: "ds_uids", Type: migrator.DB_Text, Nullable: true, Default: ""},
))
}
//func addDashboardThumbsMigrations(mg *migrator.Migrator) {
// dashThumbs := migrator.Table{
// Name: "dashboard_thumbnail",
// Columns: []*migrator.Column{
// {Name: "id", Type: migrator.DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
// {Name: "dashboard_id", Type: migrator.DB_BigInt, Nullable: false}, // can join with dashboard table
// {Name: "dashboard_version", Type: migrator.DB_Int, Nullable: false}, // screenshoted version of the dashboard
// {Name: "state", Type: migrator.DB_NVarchar, Length: 10, Nullable: false}, // stale | locked
// {Name: "panel_id", Type: migrator.DB_SmallInt, Nullable: false, Default: "0"}, // for panel thumbnails
// {Name: "image", Type: migrator.DB_MediumBlob, Nullable: false}, // image stored as blob. MediumBlob has a max limit of 16mb in MySQL
// {Name: "mime_type", Type: migrator.DB_NVarchar, Length: 255, Nullable: false}, // e.g. image/png, image/webp
// {Name: "kind", Type: migrator.DB_NVarchar, Length: 8, Nullable: false}, // thumb | tall
// {Name: "theme", Type: migrator.DB_NVarchar, Length: 8, Nullable: false}, // light|dark
// {Name: "updated", Type: migrator.DB_DateTime, Nullable: false},
// },
// Indices: []*migrator.Index{
// {Cols: []string{"dashboard_id", "panel_id", "kind", "theme"}, Type: migrator.UniqueIndex},
// },
// }
//
// mg.AddMigration("create dashboard_thumbnail table", migrator.NewAddTableMigration(dashThumbs))
// mg.AddMigration("add unique indexes for dashboard_thumbnail", migrator.NewAddIndexMigration(dashThumbs, dashThumbs.Indices[0]))
// mg.AddMigration("Add ds_uids column to dashboard_thumbnail table", migrator.NewAddColumnMigration(dashThumbs,
// // uids of datasources used in the dashboard when taking preview
// &migrator.Column{Name: "ds_uids", Type: migrator.DB_Text, Nullable: true, Default: ""},
// ))
//}

View File

@@ -53,11 +53,6 @@ func (*OSSMigrations) AddMigration(mg *Migrator) {
ualert.AddTablesMigrations(mg)
ualert.AddDashAlertMigration(mg)
addLibraryElementsMigrations(mg)
if mg.Cfg != nil && mg.Cfg.IsFeatureToggleEnabled != nil {
if mg.Cfg.IsFeatureToggleEnabled(featuremgmt.FlagDashboardPreviews) {
addDashboardThumbsMigrations(mg)
}
}
ualert.RerunDashAlertMigration(mg)
addSecretsMigration(mg)

View File

@@ -535,7 +535,6 @@ type InitTestDBOpt struct {
}
var featuresEnabledDuringTests = []string{
featuremgmt.FlagDashboardPreviews,
featuremgmt.FlagPanelTitleSearch,
featuremgmt.FlagEntityStore,
}

View File

@@ -1,349 +0,0 @@
package thumbs
import (
"context"
"encoding/json"
"fmt"
"os"
"sort"
"strings"
"sync"
"time"
"golang.org/x/sync/errgroup"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/live"
"github.com/grafana/grafana/pkg/services/rendering"
"github.com/grafana/grafana/pkg/setting"
)
type simpleCrawler struct {
renderService rendering.Service
threadCount int
concurrentLimit int
renderingTimeout time.Duration
glive *live.GrafanaLive
thumbnailRepo thumbnailRepo
mode CrawlerMode
thumbnailKind ThumbnailKind
auth CrawlerAuth
opts rendering.Opts
status crawlStatus
statusMutex sync.RWMutex
queue []*DashboardWithStaleThumbnail
queueMutex sync.Mutex
log log.Logger
renderingSessionByOrgId map[int64]rendering.Session
dsUidsLookup getDatasourceUidsForDashboard
}
func newSimpleCrawler(renderService rendering.Service, gl *live.GrafanaLive, repo thumbnailRepo, cfg *setting.Cfg, settings setting.DashboardPreviewsSettings, dsUidsLookup getDatasourceUidsForDashboard) dashRenderer {
threadCount := int(settings.CrawlThreadCount)
c := &simpleCrawler{
// temporarily increases the concurrentLimit from the 'cfg.RendererConcurrentRequestLimit' to 'cfg.RendererConcurrentRequestLimit + crawlerThreadCount'
concurrentLimit: cfg.RendererConcurrentRequestLimit + threadCount,
renderingTimeout: settings.RenderingTimeout,
renderService: renderService,
threadCount: threadCount,
glive: gl,
dsUidsLookup: dsUidsLookup,
thumbnailRepo: repo,
log: log.New("thumbnails_crawler"),
status: crawlStatus{
State: initializing,
Complete: 0,
Queue: 0,
},
renderingSessionByOrgId: make(map[int64]rendering.Session),
queue: nil,
}
c.broadcastStatus()
return c
}
func (r *simpleCrawler) next(ctx context.Context) (*DashboardWithStaleThumbnail, rendering.Session, rendering.AuthOpts, error) {
r.queueMutex.Lock()
defer r.queueMutex.Unlock()
if r.queue == nil || len(r.queue) < 1 {
return nil, nil, rendering.AuthOpts{}, nil
}
v := r.queue[0]
r.queue = r.queue[1:]
authOpts := rendering.AuthOpts{
OrgID: v.OrgId,
UserID: r.auth.GetUserId(v.OrgId),
OrgRole: r.auth.GetOrgRole(),
}
if renderingSession, ok := r.renderingSessionByOrgId[v.OrgId]; ok {
return v, renderingSession, authOpts, nil
}
renderingSession, err := r.renderService.CreateRenderingSession(ctx, authOpts, rendering.SessionOpts{
Expiry: 5 * time.Minute,
RefreshExpiryOnEachRequest: true,
})
if err != nil {
return nil, nil, authOpts, err
}
r.renderingSessionByOrgId[v.OrgId] = renderingSession
return v, renderingSession, authOpts, nil
}
func (r *simpleCrawler) broadcastStatus() {
s, err := r.Status()
if err != nil {
r.log.Warn("Error reading status", "err", err)
return
}
msg, err := json.Marshal(s)
if err != nil {
r.log.Warn("Error making message", "err", err)
return
}
err = r.glive.Publish(r.opts.OrgID, "grafana/broadcast/crawler", msg)
if err != nil {
r.log.Warn("Error Publish message", "err", err)
return
}
}
type byOrgId []*DashboardWithStaleThumbnail
func (d byOrgId) Len() int { return len(d) }
func (d byOrgId) Less(i, j int) bool { return d[i].OrgId > d[j].OrgId }
func (d byOrgId) Swap(i, j int) { d[i], d[j] = d[j], d[i] }
func (r *simpleCrawler) Run(ctx context.Context, auth CrawlerAuth, mode CrawlerMode, theme models.Theme, thumbnailKind ThumbnailKind) error {
res, err := r.renderService.HasCapability(ctx, rendering.ScalingDownImages)
if err != nil {
return err
}
if !res.IsSupported {
return fmt.Errorf("cant run dashboard crawler - rendering service needs to be updated. "+
"current version: %s, requiredVersion: %s", r.renderService.Version(), res.SemverConstraint)
}
runStarted := time.Now()
r.queueMutex.Lock()
if r.IsRunning() {
r.queueMutex.Unlock()
r.log.Info("Already running")
return nil
}
items, err := r.thumbnailRepo.findDashboardsWithStaleThumbnails(ctx, theme, thumbnailKind)
if err != nil {
r.log.Error("Error when fetching dashboards with stale thumbnails", "err", err.Error())
r.queueMutex.Unlock()
return err
}
if len(items) == 0 {
r.queueMutex.Unlock()
return nil
}
// sort the items so that we render all items from each org before moving on to the next one
// helps us avoid having to maintain multiple active rendering sessions
sort.Sort(byOrgId(items))
r.mode = mode
r.thumbnailKind = thumbnailKind
r.auth = auth
r.opts = rendering.Opts{
TimeoutOpts: rendering.TimeoutOpts{
Timeout: r.renderingTimeout,
RequestTimeoutMultiplier: 3,
},
Theme: theme,
ConcurrentLimit: r.concurrentLimit,
}
r.renderingSessionByOrgId = make(map[int64]rendering.Session)
r.queue = items
r.status = crawlStatus{
Started: runStarted,
State: running,
Complete: 0,
}
r.broadcastStatus()
r.queueMutex.Unlock()
r.log.Info("Starting dashboard crawler", "threadCount", r.threadCount, "dashboardsToCrawl", len(items), "mode", string(mode), "theme", string(theme), "kind", string(thumbnailKind), "crawlerSetupTime", time.Since(runStarted))
group, gCtx := errgroup.WithContext(ctx)
// create a pool of workers
for i := 0; i < r.threadCount; i++ {
walkerId := i
group.Go(func() error {
r.walk(gCtx, walkerId)
return nil
})
}
err = group.Wait()
status, _ := r.Status()
r.log.Info("Crawl finished", "completedCount", status.Complete, "errorCount", status.Errors, "threadCount", r.threadCount, "dashboardsToCrawl", len(items), "mode", string(mode), "theme", string(theme), "kind", string(thumbnailKind), "crawlerRunTime", time.Since(runStarted))
if err != nil {
r.log.Error("Crawl ended with an error", "err", err)
}
r.crawlFinished()
r.broadcastStatus()
return err
}
func (r *simpleCrawler) IsRunning() bool {
r.statusMutex.Lock()
defer r.statusMutex.Unlock()
return r.status.State == running
}
func (r *simpleCrawler) Stop() (crawlStatus, error) {
r.statusMutex.Lock()
if r.status.State == running {
r.status.State = stopping
}
r.statusMutex.Unlock()
return r.Status()
}
func (r *simpleCrawler) Status() (crawlStatus, error) {
r.statusMutex.RLock()
defer r.statusMutex.RUnlock()
status := crawlStatus{
State: r.status.State,
Started: r.status.Started,
Complete: r.status.Complete,
Errors: r.status.Errors,
Queue: len(r.queue),
Last: r.status.Last,
}
return status, nil
}
func (r *simpleCrawler) newErrorResult() {
r.statusMutex.Lock()
defer r.statusMutex.Unlock()
r.status.Errors++
r.status.Last = time.Now()
}
func (r *simpleCrawler) newSuccessResult() {
r.statusMutex.Lock()
defer r.statusMutex.Unlock()
r.status.Complete++
r.status.Last = time.Now()
}
func (r *simpleCrawler) crawlFinished() {
r.statusMutex.Lock()
defer r.statusMutex.Unlock()
r.status.State = stopped
r.status.Finished = time.Now()
}
func (r *simpleCrawler) shouldWalk() bool {
r.statusMutex.RLock()
defer r.statusMutex.RUnlock()
return r.status.State == running
}
func (r *simpleCrawler) walk(ctx context.Context, id int) {
walkerStarted := time.Now()
for {
if !r.shouldWalk() {
break
}
itemStarted := time.Now()
item, renderingSession, authOpts, err := r.next(ctx)
if err != nil {
r.log.Error("Render item retrieval error", "walkerId", id, "error", err)
break
}
if item == nil || renderingSession == nil {
break
}
url := dashboards.GetKioskModeDashboardURL(item.Uid, item.Slug, r.opts.Theme)
r.log.Info("Getting dashboard thumbnail", "walkerId", id, "dashboardUID", item.Uid, "url", url)
dsUids, err := r.dsUidsLookup(ctx, item.Uid, item.OrgId)
if err != nil {
r.log.Warn("Error getting datasource uids", "walkerId", id, "dashboardUID", item.Uid, "url", url, "err", err)
r.newErrorResult()
continue
}
res, err := r.renderService.Render(ctx, rendering.Opts{
Width: 320,
Height: 240,
Path: strings.TrimPrefix(url, "/"),
AuthOpts: authOpts,
TimeoutOpts: r.opts.TimeoutOpts,
ConcurrentLimit: r.opts.ConcurrentLimit,
Theme: r.opts.Theme,
DeviceScaleFactor: -5, // negative numbers will render larger and then scale down.
}, renderingSession)
if err != nil {
r.log.Warn("Error getting image", "walkerId", id, "dashboardUID", item.Uid, "url", url, "err", err)
r.newErrorResult()
} else if res.FilePath == "" {
r.log.Warn("Error getting image... no response", "walkerId", id, "dashboardUID", item.Uid, "url", url)
r.newErrorResult()
} else if strings.Contains(res.FilePath, "public/img") {
r.log.Warn("Error getting image... internal result", "walkerId", id, "dashboardUID", item.Uid, "url", url, "img", res.FilePath)
// rendering service returned a static error image - we should not remove that file
r.newErrorResult()
} else {
func() {
defer func() {
err := os.Remove(res.FilePath)
if err != nil {
r.log.Error("Failed to remove thumbnail temp file", "walkerId", id, "dashboardUID", item.Uid, "url", url, "err", err)
}
}()
thumbnailId, err := r.thumbnailRepo.saveFromFile(ctx, res.FilePath, DashboardThumbnailMeta{
DashboardUID: item.Uid,
OrgId: item.OrgId,
Theme: r.opts.Theme,
Kind: r.thumbnailKind,
}, item.Version, dsUids)
if err != nil {
r.log.Warn("Error saving image image", "walkerId", id, "dashboardUID", item.Uid, "url", url, "err", err, "itemTime", time.Since(itemStarted))
r.newErrorResult()
} else {
r.log.Info("Saved thumbnail", "walkerId", id, "dashboardUID", item.Uid, "url", url, "thumbnailId", thumbnailId, "itemTime", time.Since(itemStarted))
r.newSuccessResult()
}
}()
}
r.broadcastStatus()
}
r.log.Info("Walker finished", "walkerId", id, "walkerTime", time.Since(walkerStarted))
}

View File

@@ -1,139 +0,0 @@
package thumbs
import (
"context"
"errors"
"strconv"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/serviceaccounts"
)
type CrawlerAuthSetupService interface {
Setup(ctx context.Context) (CrawlerAuth, error)
}
func ProvideCrawlerAuthSetupService(serviceAccounts serviceaccounts.Service,
sqlStore db.DB, orgService org.Service) *OSSCrawlerAuthSetupService {
return &OSSCrawlerAuthSetupService{
serviceAccountNamePrefix: "dashboard-previews-crawler-org-",
serviceAccounts: serviceAccounts,
log: log.New("oss_crawler_account_setup_service"),
sqlStore: sqlStore,
orgService: orgService,
}
}
type OSSCrawlerAuthSetupService struct {
log log.Logger
serviceAccountNamePrefix string
serviceAccounts serviceaccounts.Service
sqlStore db.DB
orgService org.Service
}
type CrawlerAuth interface {
GetUserId(orgId int64) int64
GetLogin(orgId int64) string
GetOrgRole() org.RoleType
}
func (o *OSSCrawlerAuthSetupService) findAllOrgIds(ctx context.Context) ([]int64, error) {
searchAllOrgsQuery := &org.SearchOrgsQuery{}
result, err := o.orgService.Search(ctx, searchAllOrgsQuery)
if err != nil {
o.log.Error("Error when searching for orgs", "err", err)
return nil, err
}
orgIds := make([]int64, 0)
for i := range result {
orgIds = append(orgIds, result[i].ID)
}
return orgIds, nil
}
type crawlerAuth struct {
accountIdByOrgId map[int64]int64
loginByOrgId map[int64]string
orgRole org.RoleType
}
func (o *crawlerAuth) GetOrgRole() org.RoleType {
return o.orgRole
}
func (o *crawlerAuth) GetUserId(orgId int64) int64 {
return o.accountIdByOrgId[orgId]
}
func (o *crawlerAuth) GetLogin(orgId int64) string {
return o.loginByOrgId[orgId]
}
func (o *OSSCrawlerAuthSetupService) Setup(ctx context.Context) (CrawlerAuth, error) {
orgIds, err := o.findAllOrgIds(ctx)
if err != nil {
return nil, err
}
// userId:0 and RoleAdmin grants the crawler process permissions to view all dashboards in all folders & orgs
// the process doesn't and shouldn't actually need to edit/modify any resources from the UI
orgRole := org.RoleAdmin
accountIdByOrgId := make(map[int64]int64)
loginByOrgId := make(map[int64]string)
for _, orgId := range orgIds {
o.log.Info("Creating account for org", "orgId", orgId)
serviceAccountNameOrg := o.serviceAccountNamePrefix + strconv.FormatInt(orgId, 10)
saForm := serviceaccounts.CreateServiceAccountForm{
Name: serviceAccountNameOrg,
Role: &orgRole,
}
serviceAccount, err := o.serviceAccounts.CreateServiceAccount(ctx, orgId, &saForm)
accountAlreadyExists := errors.Is(err, serviceaccounts.ErrServiceAccountAlreadyExists)
if !accountAlreadyExists && err != nil {
o.log.Error("Failed to create the service account", "err", err, "accountName", serviceAccountNameOrg, "orgId", orgId)
return nil, err
}
var serviceAccountLogin string
var serviceAccountId int64
if accountAlreadyExists {
id, err := o.serviceAccounts.RetrieveServiceAccountIdByName(ctx, orgId, serviceAccountNameOrg)
if err != nil {
o.log.Error("Failed to retrieve service account", "err", err, "accountName", serviceAccountNameOrg)
return nil, err
}
// update org_role to make sure everything works properly if someone has changed the role since SA's original creation
dto, err := o.serviceAccounts.UpdateServiceAccount(ctx, orgId, id, &serviceaccounts.UpdateServiceAccountForm{
Name: &serviceAccountNameOrg,
Role: &orgRole,
})
if err != nil {
o.log.Error("Failed to update service account's role", "err", err, "accountName", serviceAccountNameOrg)
return nil, err
}
serviceAccountLogin = dto.Login
serviceAccountId = id
} else {
serviceAccountLogin = serviceAccount.Login
serviceAccountId = serviceAccount.Id
}
accountIdByOrgId[orgId] = serviceAccountId
loginByOrgId[orgId] = serviceAccountLogin
}
return &crawlerAuth{accountIdByOrgId: accountIdByOrgId, loginByOrgId: loginByOrgId, orgRole: orgRole}, nil
}

View File

@@ -1,13 +0,0 @@
package thumbs
import (
"context"
)
type DashboardThumbService interface {
GetThumbnail(ctx context.Context, query *GetDashboardThumbnailCommand) (*DashboardThumbnail, error)
SaveThumbnail(ctx context.Context, cmd *SaveDashboardThumbnailCommand) (*DashboardThumbnail, error)
UpdateThumbnailState(ctx context.Context, cmd *UpdateThumbnailStateCommand) error
FindThumbnailCount(ctx context.Context, cmd *FindDashboardThumbnailCountCommand) (int64, error)
FindDashboardsWithStaleThumbnails(ctx context.Context, cmd *FindDashboardsWithStaleThumbnailsCommand) ([]*DashboardWithStaleThumbnail, error)
}

View File

@@ -1,43 +0,0 @@
package dashboardthumbsimpl
import (
"context"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/services/thumbs"
)
type Service struct {
store store
}
func ProvideService(db db.DB) thumbs.DashboardThumbService {
return &Service{
store: &xormStore{db: db},
}
}
func (s *Service) GetThumbnail(ctx context.Context, query *thumbs.GetDashboardThumbnailCommand) (*thumbs.DashboardThumbnail, error) {
dt, err := s.store.Get(ctx, query)
return dt, err
}
func (s *Service) SaveThumbnail(ctx context.Context, cmd *thumbs.SaveDashboardThumbnailCommand) (*thumbs.DashboardThumbnail, error) {
dt, err := s.store.Save(ctx, cmd)
return dt, err
}
func (s *Service) UpdateThumbnailState(ctx context.Context, cmd *thumbs.UpdateThumbnailStateCommand) error {
err := s.store.UpdateState(ctx, cmd)
return err
}
func (s *Service) FindThumbnailCount(ctx context.Context, cmd *thumbs.FindDashboardThumbnailCountCommand) (int64, error) {
n, err := s.store.Count(ctx, cmd)
return n, err
}
func (s *Service) FindDashboardsWithStaleThumbnails(ctx context.Context, cmd *thumbs.FindDashboardsWithStaleThumbnailsCommand) ([]*thumbs.DashboardWithStaleThumbnail, error) {
thumbs, err := s.store.FindDashboardsWithStaleThumbnails(ctx, cmd)
return thumbs, err
}

View File

@@ -1,15 +0,0 @@
package dashboardthumbsimpl
import (
"context"
"github.com/grafana/grafana/pkg/services/thumbs"
)
type store interface {
Get(ctx context.Context, query *thumbs.GetDashboardThumbnailCommand) (*thumbs.DashboardThumbnail, error)
Save(ctx context.Context, cmd *thumbs.SaveDashboardThumbnailCommand) (*thumbs.DashboardThumbnail, error)
UpdateState(ctx context.Context, cmd *thumbs.UpdateThumbnailStateCommand) error
Count(ctx context.Context, cmd *thumbs.FindDashboardThumbnailCountCommand) (int64, error)
FindDashboardsWithStaleThumbnails(ctx context.Context, cmd *thumbs.FindDashboardsWithStaleThumbnailsCommand) ([]*thumbs.DashboardWithStaleThumbnail, error)
}

View File

@@ -1,402 +0,0 @@
package dashboardthumbsimpl
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/dashboards"
dashver "github.com/grafana/grafana/pkg/services/dashboardversion"
"github.com/grafana/grafana/pkg/services/thumbs"
"github.com/grafana/grafana/pkg/util"
)
var theme = models.ThemeDark
var kind = thumbs.ThumbnailKindDefault
func TestIntegrationSqlStorage(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
var sqlStore db.DB
var store store
var savedFolder *dashboards.Dashboard
setup := func() {
sqlStore = db.InitTestDB(t)
store = &xormStore{db: sqlStore}
savedFolder = insertTestDashboard(t, sqlStore, "1 test dash folder", 1, 0, true, "prod", "webapp")
}
t.Run("Should insert dashboard in default state", func(t *testing.T) {
setup()
dash := insertTestDashboard(t, sqlStore, "test dash 23", 1, savedFolder.ID, false, "prod", "webapp")
upsertTestDashboardThumbnail(t, store, dash.UID, dash.OrgID, dash.Version)
thumb := getThumbnail(t, store, dash.UID, dash.OrgID)
require.Positive(t, thumb.Id)
require.Equal(t, thumbs.ThumbnailStateDefault, thumb.State)
require.Equal(t, dash.Version, thumb.DashboardVersion)
})
t.Run("Should be able to update the thumbnail", func(t *testing.T) {
setup()
dash := insertTestDashboard(t, sqlStore, "test dash 23", 1, savedFolder.ID, false, "prod", "webapp")
upsertTestDashboardThumbnail(t, store, dash.UID, dash.OrgID, dash.Version)
thumb := getThumbnail(t, store, dash.UID, dash.OrgID)
insertedThumbnailId := thumb.Id
upsertTestDashboardThumbnail(t, store, dash.UID, dash.OrgID, dash.Version+1)
updatedThumb := getThumbnail(t, store, dash.UID, dash.OrgID)
require.Equal(t, insertedThumbnailId, updatedThumb.Id)
require.Equal(t, dash.Version+1, updatedThumb.DashboardVersion)
})
t.Run("Should return empty array if all dashboards have thumbnails", func(t *testing.T) {
setup()
dash := insertTestDashboard(t, sqlStore, "test dash 23", 1, savedFolder.ID, false, "prod", "webapp")
upsertTestDashboardThumbnail(t, store, dash.UID, dash.OrgID, dash.Version)
cmd := thumbs.FindDashboardsWithStaleThumbnailsCommand{
Kind: kind,
Theme: theme,
}
res, err := store.FindDashboardsWithStaleThumbnails(context.Background(), &cmd)
require.NoError(t, err)
require.Len(t, res, 0)
})
t.Run("Should return dashboards with thumbnails with empty ds_uids array", func(t *testing.T) {
setup()
dash := insertTestDashboard(t, sqlStore, "test dash 23", 1, savedFolder.ID, false, "prod", "webapp")
upsertTestDashboardThumbnail(t, store, dash.UID, dash.OrgID, dash.Version)
cmd := thumbs.FindDashboardsWithStaleThumbnailsCommand{
Kind: kind,
IncludeThumbnailsWithEmptyDsUIDs: true,
Theme: theme,
}
res, err := store.FindDashboardsWithStaleThumbnails(context.Background(), &cmd)
require.NoError(t, err)
require.Len(t, res, 1)
require.Equal(t, dash.ID, res[0].Id)
})
t.Run("Should return dashboards with thumbnails marked as stale", func(t *testing.T) {
setup()
dash := insertTestDashboard(t, sqlStore, "test dash 23", 1, savedFolder.ID, false, "prod", "webapp")
upsertTestDashboardThumbnail(t, store, dash.UID, dash.OrgID, dash.Version)
updateThumbnailState(t, store, dash.UID, dash.OrgID, thumbs.ThumbnailStateStale)
cmd := thumbs.FindDashboardsWithStaleThumbnailsCommand{
Kind: kind,
Theme: theme,
}
res, err := store.FindDashboardsWithStaleThumbnails(context.Background(), &cmd)
require.NoError(t, err)
require.Len(t, res, 1)
require.Equal(t, dash.ID, res[0].Id)
})
t.Run("Should not return dashboards with updated thumbnails that had been marked as stale", func(t *testing.T) {
setup()
dash := insertTestDashboard(t, sqlStore, "test dash 23", 1, savedFolder.ID, false, "prod", "webapp")
upsertTestDashboardThumbnail(t, store, dash.UID, dash.OrgID, dash.Version)
updateThumbnailState(t, store, dash.UID, dash.OrgID, thumbs.ThumbnailStateStale)
upsertTestDashboardThumbnail(t, store, dash.UID, dash.OrgID, dash.Version)
cmd := thumbs.FindDashboardsWithStaleThumbnailsCommand{
Kind: kind,
Theme: theme,
}
res, err := store.FindDashboardsWithStaleThumbnails(context.Background(), &cmd)
require.NoError(t, err)
require.Len(t, res, 0)
})
t.Run("Should find dashboards without thumbnails", func(t *testing.T) {
setup()
dash := insertTestDashboard(t, sqlStore, "test dash 23", 1, savedFolder.ID, false, "prod", "webapp")
cmd := thumbs.FindDashboardsWithStaleThumbnailsCommand{
Kind: kind,
Theme: theme,
}
res, err := store.FindDashboardsWithStaleThumbnails(context.Background(), &cmd)
require.NoError(t, err)
require.Len(t, res, 1)
require.Equal(t, dash.ID, res[0].Id)
})
t.Run("Should find dashboards with outdated thumbnails", func(t *testing.T) {
setup()
dash := insertTestDashboard(t, sqlStore, "test dash 23", 1, savedFolder.ID, false, "prod", "webapp")
upsertTestDashboardThumbnail(t, store, dash.UID, dash.OrgID, dash.Version)
updateTestDashboard(t, sqlStore, dash, map[string]interface{}{
"tags": "different-tag",
})
cmd := thumbs.FindDashboardsWithStaleThumbnailsCommand{
Kind: kind,
Theme: theme,
}
res, err := store.FindDashboardsWithStaleThumbnails(context.Background(), &cmd)
require.NoError(t, err)
require.Len(t, res, 1)
require.Equal(t, dash.ID, res[0].Id)
})
t.Run("Should not return dashboards with locked thumbnails even if they are outdated", func(t *testing.T) {
setup()
dash := insertTestDashboard(t, sqlStore, "test dash 23", 1, savedFolder.ID, false, "prod", "webapp")
upsertTestDashboardThumbnail(t, store, dash.UID, dash.OrgID, dash.Version)
updateThumbnailState(t, store, dash.UID, dash.OrgID, thumbs.ThumbnailStateLocked)
updateTestDashboard(t, sqlStore, dash, map[string]interface{}{
"tags": "different-tag",
})
cmd := thumbs.FindDashboardsWithStaleThumbnailsCommand{
Kind: kind,
Theme: theme,
}
res, err := store.FindDashboardsWithStaleThumbnails(context.Background(), &cmd)
require.NoError(t, err)
require.Len(t, res, 0)
})
t.Run("Should not return dashboards with manually uploaded thumbnails by default", func(t *testing.T) {
setup()
dash := insertTestDashboard(t, sqlStore, "test dash 23", 1, savedFolder.ID, false, "prod", "webapp")
upsertTestDashboardThumbnail(t, store, dash.UID, dash.OrgID, thumbs.DashboardVersionForManualThumbnailUpload)
updateTestDashboard(t, sqlStore, dash, map[string]interface{}{
"tags": "different-tag",
})
cmd := thumbs.FindDashboardsWithStaleThumbnailsCommand{
Kind: kind,
Theme: theme,
}
res, err := store.FindDashboardsWithStaleThumbnails(context.Background(), &cmd)
require.NoError(t, err)
require.Len(t, res, 0)
})
t.Run("Should return dashboards with manually uploaded thumbnails if requested", func(t *testing.T) {
setup()
dash := insertTestDashboard(t, sqlStore, "test dash 23", 1, savedFolder.ID, false, "prod", "webapp")
upsertTestDashboardThumbnail(t, store, dash.UID, dash.OrgID, thumbs.DashboardVersionForManualThumbnailUpload)
updateTestDashboard(t, sqlStore, dash, map[string]interface{}{
"tags": "different-tag",
})
cmd := thumbs.FindDashboardsWithStaleThumbnailsCommand{
Kind: kind,
Theme: theme,
IncludeManuallyUploadedThumbnails: true,
}
res, err := store.FindDashboardsWithStaleThumbnails(context.Background(), &cmd)
require.NoError(t, err)
require.Len(t, res, 1)
require.Equal(t, dash.ID, res[0].Id)
})
t.Run("Should count all dashboard thumbnails", func(t *testing.T) {
setup()
dash := insertTestDashboard(t, sqlStore, "test dash 23", 1, savedFolder.ID, false, "prod", "webapp")
upsertTestDashboardThumbnail(t, store, dash.UID, dash.OrgID, 1)
dash2 := insertTestDashboard(t, sqlStore, "test dash 23", 2, savedFolder.ID, false, "prod", "webapp")
upsertTestDashboardThumbnail(t, store, dash2.UID, dash2.OrgID, 1)
updateTestDashboard(t, sqlStore, dash, map[string]interface{}{
"tags": "different-tag",
})
cmd := thumbs.FindDashboardThumbnailCountCommand{}
res, err := store.Count(context.Background(), &cmd)
require.NoError(t, err)
require.Equal(t, res, int64(2))
})
}
func getThumbnail(t *testing.T, store store, dashboardUID string, orgId int64) *thumbs.DashboardThumbnail {
t.Helper()
cmd := thumbs.GetDashboardThumbnailCommand{
DashboardThumbnailMeta: thumbs.DashboardThumbnailMeta{
DashboardUID: dashboardUID,
OrgId: orgId,
PanelID: 0,
Kind: kind,
Theme: theme,
},
}
thumb, err := store.Get(context.Background(), &cmd)
require.NoError(t, err)
return thumb
}
func upsertTestDashboardThumbnail(t *testing.T, store store, dashboardUID string, orgId int64, dashboardVersion int) *thumbs.DashboardThumbnail {
t.Helper()
cmd := thumbs.SaveDashboardThumbnailCommand{
DashboardThumbnailMeta: thumbs.DashboardThumbnailMeta{
DashboardUID: dashboardUID,
OrgId: orgId,
PanelID: 0,
Kind: kind,
Theme: theme,
},
DashboardVersion: dashboardVersion,
Image: make([]byte, 0),
MimeType: "image/png",
}
dash, err := store.Save(context.Background(), &cmd)
require.NoError(t, err)
require.NotNil(t, dash)
return dash
}
func updateThumbnailState(t *testing.T, store store, dashboardUID string, orgId int64, state thumbs.ThumbnailState) {
t.Helper()
cmd := thumbs.UpdateThumbnailStateCommand{
DashboardThumbnailMeta: thumbs.DashboardThumbnailMeta{
DashboardUID: dashboardUID,
OrgId: orgId,
PanelID: 0,
Kind: kind,
Theme: theme,
},
State: state,
}
err := store.UpdateState(context.Background(), &cmd)
require.NoError(t, err)
}
func updateTestDashboard(t *testing.T, sqlStore db.DB, dashModel *dashboards.Dashboard, data map[string]interface{}) {
t.Helper()
data["id"] = dashModel.ID
parentVersion := dashModel.Version
cmd := dashboards.SaveDashboardCommand{
OrgID: dashModel.OrgID,
Overwrite: true,
Dashboard: simplejson.NewFromAny(data),
}
var dash *dashboards.Dashboard
err := sqlStore.WithDbSession(context.Background(), func(sess *db.Session) error {
var existing dashboards.Dashboard
dash = cmd.GetDashboardModel()
dashWithIdExists, err := sess.Where("id=? AND org_id=?", dash.ID, dash.OrgID).Get(&existing)
require.NoError(t, err)
require.True(t, dashWithIdExists)
if dash.Version != existing.Version {
dash.SetVersion(existing.Version)
dash.Version = existing.Version
}
dash.SetVersion(dash.Version + 1)
dash.Created = time.Now()
dash.Updated = time.Now()
dash.ID = dashModel.ID
dash.UID = util.GenerateShortUID()
_, err = sess.MustCols("folder_id").ID(dash.ID).Update(dash)
return err
})
require.Nil(t, err)
err = sqlStore.WithDbSession(context.Background(), func(sess *db.Session) error {
dashVersion := &dashver.DashboardVersion{
DashboardID: dash.ID,
ParentVersion: parentVersion,
RestoredFrom: cmd.RestoredFrom,
Version: dash.Version,
Created: time.Now(),
CreatedBy: dash.UpdatedBy,
Message: cmd.Message,
Data: dash.Data,
}
if affectedRows, err := sess.Insert(dashVersion); err != nil {
return err
} else if affectedRows == 0 {
return dashboards.ErrDashboardNotFound
}
return nil
})
require.NoError(t, err)
}
func insertTestDashboard(t *testing.T, sqlStore db.DB, title string, orgId int64,
folderId int64, isFolder bool, tags ...interface{}) *dashboards.Dashboard {
t.Helper()
cmd := dashboards.SaveDashboardCommand{
OrgID: orgId,
FolderID: folderId,
IsFolder: isFolder,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": nil,
"title": title,
"tags": tags,
}),
}
var dash *dashboards.Dashboard
err := sqlStore.WithDbSession(context.Background(), func(sess *db.Session) error {
dash = cmd.GetDashboardModel()
dash.SetVersion(1)
dash.Created = time.Now()
dash.Updated = time.Now()
dash.UID = util.GenerateShortUID()
_, err := sess.Insert(dash)
return err
})
require.NoError(t, err)
require.NotNil(t, dash)
dash.Data.Set("id", dash.ID)
dash.Data.Set("uid", dash.UID)
err = sqlStore.WithDbSession(context.Background(), func(sess *db.Session) error {
dashVersion := &dashver.DashboardVersion{
DashboardID: dash.ID,
ParentVersion: dash.Version,
RestoredFrom: cmd.RestoredFrom,
Version: dash.Version,
Created: time.Now(),
CreatedBy: dash.UpdatedBy,
Message: cmd.Message,
Data: dash.Data,
}
require.NoError(t, err)
if affectedRows, err := sess.Insert(dashVersion); err != nil {
return err
} else if affectedRows == 0 {
return dashboards.ErrDashboardNotFound
}
return nil
})
require.NoError(t, err)
return dash
}

View File

@@ -1,216 +0,0 @@
package dashboardthumbsimpl
import (
"context"
"encoding/json"
"errors"
"time"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/thumbs"
)
type xormStore struct {
db db.DB
}
func (ss *xormStore) Get(ctx context.Context, query *thumbs.GetDashboardThumbnailCommand) (result *thumbs.DashboardThumbnail, err error) {
err = ss.db.WithDbSession(ctx, func(sess *db.Session) error {
thumb, err := findThumbnailByMeta(sess, query.DashboardThumbnailMeta)
if err != nil {
return err
}
result = thumb
return nil
})
return result, err
}
func marshalDatasourceUids(dsUids []string) (string, error) {
if dsUids == nil {
return "", nil
}
b, err := json.Marshal(dsUids)
if err != nil {
return "", err
}
return string(b), nil
}
func (ss *xormStore) Save(ctx context.Context, cmd *thumbs.SaveDashboardThumbnailCommand) (result *thumbs.DashboardThumbnail, err error) {
err = ss.db.WithTransactionalDbSession(ctx, func(sess *db.Session) error {
existing, err := findThumbnailByMeta(sess, cmd.DashboardThumbnailMeta)
if err != nil && !errors.Is(err, dashboards.ErrDashboardThumbnailNotFound) {
return err
}
dsUids, err := marshalDatasourceUids(cmd.DatasourceUIDs)
if err != nil {
return err
}
if existing != nil {
existing.Image = cmd.Image
existing.MimeType = cmd.MimeType
existing.Updated = time.Now()
existing.DashboardVersion = cmd.DashboardVersion
existing.DsUIDs = dsUids
existing.State = thumbs.ThumbnailStateDefault
_, err = sess.ID(existing.Id).Update(existing)
result = existing
return err
}
thumb := &thumbs.DashboardThumbnail{}
dash, err := findDashboardIdByThumbMeta(sess, cmd.DashboardThumbnailMeta)
if err != nil {
return err
}
thumb.Updated = time.Now()
thumb.Theme = cmd.Theme
thumb.Kind = cmd.Kind
thumb.Image = cmd.Image
thumb.DsUIDs = dsUids
thumb.MimeType = cmd.MimeType
thumb.DashboardId = dash.Id
thumb.DashboardVersion = cmd.DashboardVersion
thumb.State = thumbs.ThumbnailStateDefault
thumb.PanelId = cmd.PanelID
_, err = sess.Insert(thumb)
result = thumb
return err
})
return result, err
}
func (ss *xormStore) UpdateState(ctx context.Context, cmd *thumbs.UpdateThumbnailStateCommand) error {
err := ss.db.WithTransactionalDbSession(ctx, func(sess *db.Session) error {
existing, err := findThumbnailByMeta(sess, cmd.DashboardThumbnailMeta)
if err != nil {
return err
}
existing.State = cmd.State
_, err = sess.ID(existing.Id).Update(existing)
return err
})
return err
}
func (ss *xormStore) Count(ctx context.Context, cmd *thumbs.FindDashboardThumbnailCountCommand) (n int64, err error) {
err = ss.db.WithDbSession(ctx, func(sess *db.Session) error {
count, err := sess.Count(&thumbs.DashboardThumbnail{})
if err != nil {
return err
}
n = count
return nil
})
return n, err
}
func (ss *xormStore) FindDashboardsWithStaleThumbnails(ctx context.Context, cmd *thumbs.FindDashboardsWithStaleThumbnailsCommand) (result []*thumbs.DashboardWithStaleThumbnail, err error) {
err = ss.db.WithDbSession(ctx, func(sess *db.Session) error {
sess.Table("dashboard")
sess.Join("LEFT", "dashboard_thumbnail", "dashboard.id = dashboard_thumbnail.dashboard_id AND dashboard_thumbnail.theme = ? AND dashboard_thumbnail.kind = ?", cmd.Theme, cmd.Kind)
sess.Where("dashboard.is_folder = ?", ss.db.GetDialect().BooleanStr(false))
query := "(dashboard.version != dashboard_thumbnail.dashboard_version " +
"OR dashboard_thumbnail.state = ? " +
"OR dashboard_thumbnail.id IS NULL"
args := []interface{}{thumbs.ThumbnailStateStale}
if cmd.IncludeThumbnailsWithEmptyDsUIDs {
query += " OR dashboard_thumbnail.ds_uids = ? OR dashboard_thumbnail.ds_uids IS NULL"
args = append(args, "")
}
sess.Where(query+")", args...)
if !cmd.IncludeManuallyUploadedThumbnails {
sess.Where("(dashboard_thumbnail.id is not null AND dashboard_thumbnail.dashboard_version != ?) "+
"OR dashboard_thumbnail.id is null "+
"OR dashboard_thumbnail.state = ?", thumbs.DashboardVersionForManualThumbnailUpload, thumbs.ThumbnailStateStale)
}
sess.Where("(dashboard_thumbnail.id IS NULL OR dashboard_thumbnail.state != ?)", thumbs.ThumbnailStateLocked)
sess.Cols("dashboard.id",
"dashboard.uid",
"dashboard.org_id",
"dashboard.version",
"dashboard.slug")
var list = make([]*thumbs.DashboardWithStaleThumbnail, 0)
err := sess.Find(&list)
if err != nil {
return err
}
result = list
return err
})
return result, err
}
func findThumbnailByMeta(sess *db.Session, meta thumbs.DashboardThumbnailMeta) (*thumbs.DashboardThumbnail, error) {
result := &thumbs.DashboardThumbnail{}
sess.Table("dashboard_thumbnail")
sess.Join("INNER", "dashboard", "dashboard.id = dashboard_thumbnail.dashboard_id")
sess.Where("dashboard.uid = ? AND dashboard.org_id = ? AND panel_id = ? AND kind = ? AND theme = ?", meta.DashboardUID, meta.OrgId, meta.PanelID, meta.Kind, meta.Theme)
sess.Cols("dashboard_thumbnail.id",
"dashboard_thumbnail.dashboard_id",
"dashboard_thumbnail.panel_id",
"dashboard_thumbnail.image",
"dashboard_thumbnail.dashboard_version",
"dashboard_thumbnail.state",
"dashboard_thumbnail.kind",
"dashboard_thumbnail.ds_uids",
"dashboard_thumbnail.mime_type",
"dashboard_thumbnail.theme",
"dashboard_thumbnail.updated")
exists, err := sess.Get(result)
if !exists {
return nil, dashboards.ErrDashboardThumbnailNotFound
}
if err != nil {
return nil, err
}
return result, nil
}
type dash struct {
Id int64
}
func findDashboardIdByThumbMeta(sess *db.Session, meta thumbs.DashboardThumbnailMeta) (*dash, error) {
result := &dash{}
sess.Table("dashboard").Where("dashboard.uid = ? AND dashboard.org_id = ?", meta.DashboardUID, meta.OrgId).Cols("id")
exists, err := sess.Get(result)
if err != nil {
return nil, err
}
if !exists {
return nil, dashboards.ErrDashboardNotFound
}
return result, err
}

View File

@@ -1,92 +0,0 @@
package thumbs
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/searchV2"
"github.com/grafana/grafana/pkg/tsdb/grafanads"
)
type getDatasourceUidsForDashboard func(ctx context.Context, dashboardUid string, orgId int64) ([]string, error)
type dsUidsLookup struct {
searchService searchV2.SearchService
crawlerAuth CrawlerAuth
features featuremgmt.FeatureToggles
}
func getDatasourceUIDs(resp *backend.DataResponse, uid string) ([]string, error) {
if resp == nil {
return nil, errors.New("nil response")
}
if resp.Error != nil {
return nil, resp.Error
}
if len(resp.Frames) == 0 {
return nil, errors.New("empty response")
}
frame := resp.Frames[0]
field, idx := frame.FieldByName("ds_uid")
if field.Len() == 0 || idx == -1 {
return nil, fmt.Errorf("no ds_uid field for uid %s", uid)
}
rawValue, ok := field.At(0).(json.RawMessage)
if !ok || rawValue == nil {
return nil, fmt.Errorf("invalid value for uid %s in ds_uid field: %s", uid, field.At(0))
}
jsonValue, err := rawValue.MarshalJSON()
if err != nil {
return nil, err
}
var uids []string
err = json.Unmarshal(jsonValue, &uids)
if err != nil {
return nil, err
}
return uids, nil
}
func filterOutGrafanaDs(uids []string) []string {
filtered := make([]string, 0)
for _, uid := range uids {
if uid != grafanads.DatasourceUID {
filtered = append(filtered, uid)
}
}
return filtered
}
func (d *dsUidsLookup) getDatasourceUidsForDashboard(ctx context.Context, dashboardUid string, orgId int64) ([]string, error) {
if d.searchService.IsDisabled() {
return nil, nil
}
dashQueryResponse := d.searchService.DoDashboardQuery(ctx, &backend.User{
Login: d.crawlerAuth.GetLogin(orgId),
Role: string(d.crawlerAuth.GetOrgRole()),
}, orgId, searchV2.DashboardQuery{
UIDs: []string{dashboardUid},
})
uids, err := getDatasourceUIDs(dashQueryResponse, dashboardUid)
if err != nil {
return nil, err
}
return filterOutGrafanaDs(uids), nil
}

View File

@@ -1,85 +0,0 @@
package thumbs
import (
"context"
_ "embed"
"testing"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/searchV2"
)
var (
//go:embed testdata/search_response_frame.json
exampleListFrameJSON string
exampleListFrame = &data.Frame{}
_ = exampleListFrame.UnmarshalJSON([]byte(exampleListFrameJSON))
//go:embed testdata/empty_search_response_frame.json
listFrameJSONWithNoDatasources string
listFrameWithNoDatasources = &data.Frame{}
_ = listFrameWithNoDatasources.UnmarshalJSON([]byte(listFrameJSONWithNoDatasources))
)
func TestShouldParseUidFromSearchResponseFrame(t *testing.T) {
searchService := &searchV2.MockSearchService{}
dsLookup := &dsUidsLookup{
searchService: searchService,
crawlerAuth: &crawlerAuth{},
features: featuremgmt.WithFeatures(featuremgmt.FlagPanelTitleSearch),
}
dashboardUid := "abc"
searchService.On("IsDisabled").Return(false)
searchService.On("DoDashboardQuery", mock.Anything, mock.Anything, mock.Anything, searchV2.DashboardQuery{
UIDs: []string{dashboardUid},
}).Return(&backend.DataResponse{
Frames: []*data.Frame{exampleListFrame},
})
uids, err := dsLookup.getDatasourceUidsForDashboard(context.Background(), dashboardUid, 1)
require.NoError(t, err)
require.Equal(t, []string{"datasource-2", "datasource-3", "datasource-4"}, uids)
}
func TestShouldReturnEmptyArrayIfThereAreNoDatasources(t *testing.T) {
searchService := &searchV2.MockSearchService{}
dsLookup := &dsUidsLookup{
searchService: searchService,
crawlerAuth: &crawlerAuth{},
features: featuremgmt.WithFeatures(featuremgmt.FlagPanelTitleSearch),
}
dashboardUid := "abc"
searchService.On("IsDisabled").Return(false)
searchService.On("DoDashboardQuery", mock.Anything, mock.Anything, mock.Anything, searchV2.DashboardQuery{
UIDs: []string{dashboardUid},
}).Return(&backend.DataResponse{
Frames: []*data.Frame{listFrameWithNoDatasources},
})
uids, err := dsLookup.getDatasourceUidsForDashboard(context.Background(), dashboardUid, 1)
require.NoError(t, err)
require.Equal(t, []string{}, uids)
require.NotNil(t, uids)
}
func TestShouldReturnNullIfSearchServiceIsDisabled(t *testing.T) {
searchService := &searchV2.MockSearchService{}
dsLookup := &dsUidsLookup{
searchService: searchService,
crawlerAuth: &crawlerAuth{},
features: featuremgmt.WithFeatures(featuremgmt.FlagPanelTitleSearch),
}
dashboardUid := "abc"
searchService.On("IsDisabled").Return(true)
uids, err := dsLookup.getDatasourceUidsForDashboard(context.Background(), dashboardUid, 1)
require.NoError(t, err)
require.Nil(t, uids)
}

View File

@@ -1,65 +0,0 @@
package thumbs
import (
"context"
"net/http"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/response"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
)
// When the feature flag is not enabled we just implement a dummy service
type dummyService struct{}
func (ds *dummyService) GetUsageStats(ctx context.Context) map[string]interface{} {
return make(map[string]interface{})
}
func (ds *dummyService) GetImage(c *contextmodel.ReqContext) {
c.JSON(400, map[string]string{"error": "invalid size"})
}
func (ds *dummyService) UpdateThumbnailState(c *contextmodel.ReqContext) {
c.JSON(400, map[string]string{"error": "invalid size"})
}
func (ds *dummyService) SetImage(c *contextmodel.ReqContext) {
c.JSON(400, map[string]string{"error": "invalid size"})
}
func (ds *dummyService) Enabled() bool {
return false
}
func (ds *dummyService) GetDashboardPreviewsSetupSettings(c *contextmodel.ReqContext) dtos.DashboardPreviewsSetupConfig {
return dtos.DashboardPreviewsSetupConfig{
SystemRequirements: dtos.DashboardPreviewsSystemRequirements{
Met: false,
RequiredImageRendererPluginVersion: "",
},
ThumbnailsExist: false,
}
}
func (ds *dummyService) StartCrawler(c *contextmodel.ReqContext) response.Response {
result := make(map[string]string)
result["error"] = "Not enabled"
return response.JSON(http.StatusOK, result)
}
func (ds *dummyService) StopCrawler(c *contextmodel.ReqContext) response.Response {
result := make(map[string]string)
result["error"] = "Not enabled"
return response.JSON(http.StatusOK, result)
}
func (ds *dummyService) CrawlerStatus(c *contextmodel.ReqContext) response.Response {
result := make(map[string]string)
result["error"] = "Not enabled"
return response.JSON(http.StatusOK, result)
}
func (ds *dummyService) Run(ctx context.Context) error {
return nil
}

View File

@@ -1,210 +0,0 @@
package thumbs
import (
"context"
"encoding/json"
"errors"
"fmt"
"time"
"github.com/grafana/grafana/pkg/models"
)
type CrawlerMode string
const (
// CrawlerModeThumbs will create small thumbnails for everything.
CrawlerModeThumbs CrawlerMode = "thumbs"
// CrawlerModeAnalytics will get full page results for everything.
CrawlerModeAnalytics CrawlerMode = "analytics"
// CrawlerModeMigrate will migrate all dashboards with old schema.
CrawlerModeMigrate CrawlerMode = "migrate"
)
type crawlerState string
const (
initializing crawlerState = "initializing"
running crawlerState = "running"
stopping crawlerState = "stopping"
stopped crawlerState = "stopped"
)
type previewRequest struct {
OrgID int64 `json:"orgId"`
UID string `json:"uid"`
Kind ThumbnailKind `json:"kind"`
Theme models.Theme `json:"theme"`
}
type crawlCmd struct {
Mode CrawlerMode `json:"mode"` // thumbs | analytics | migrate
Theme models.Theme `json:"theme"` // light | dark
}
type crawlStatus struct {
State crawlerState `json:"state"`
Started time.Time `json:"started,omitempty"`
Finished time.Time `json:"finished,omitempty"`
Complete int `json:"complete"`
Errors int `json:"errors"`
Queue int `json:"queue"`
Last time.Time `json:"last,omitempty"`
}
type ThumbnailKind string
type ThumbnailState string
const (
// ThumbnailKindDefault is a small 320x240 preview
ThumbnailKindDefault ThumbnailKind = "thumb"
// unsupported for now
// - ThumbnailKindLarge ThumbnailKind = "large"
// - ThumbnailKindTall ThumbnailKind = "tall"
)
const (
// ThumbnailStateDefault is the initial state for all thumbnails. Thumbnails in the "default" state will be considered stale,
// and thus refreshed by the crawler, if the dashboard version from the time of taking the thumbnail is different from the current dashboard version
ThumbnailStateDefault ThumbnailState = "default"
// ThumbnailStateStale is a manually assigned state. Thumbnails in the "stale" state will be refreshed on the next crawler run
ThumbnailStateStale ThumbnailState = "stale"
// ThumbnailStateLocked is a manually assigned state. Thumbnails in the "locked" state will not be refreshed by the crawler as long as they remain in the "locked" state.
ThumbnailStateLocked ThumbnailState = "locked"
)
func (s ThumbnailState) IsValid() bool {
return s == ThumbnailStateDefault || s == ThumbnailStateStale || s == ThumbnailStateLocked
}
func (s *ThumbnailState) UnmarshalJSON(data []byte) error {
var str string
err := json.Unmarshal(data, &str)
if err != nil {
return err
}
*s = ThumbnailState(str)
if !s.IsValid() {
if (*s) != "" {
return fmt.Errorf("JSON validation error: invalid thumbnail state value: %s", *s)
}
*s = ThumbnailStateDefault
}
return nil
}
// IsKnownThumbnailKind checks if the value is supported
func (p ThumbnailKind) IsKnownThumbnailKind() bool {
switch p {
case
ThumbnailKindDefault:
return true
}
return false
}
func ParseThumbnailKind(str string) (ThumbnailKind, error) {
switch str {
case string(ThumbnailKindDefault):
return ThumbnailKindDefault, nil
}
return ThumbnailKindDefault, errors.New("unknown thumbnail kind " + str)
}
// A DashboardThumbnail includes all metadata for a dashboard thumbnail
type DashboardThumbnail struct {
Id int64 `json:"id"`
DashboardId int64 `json:"dashboardId"`
DashboardVersion int `json:"dashboardVersion"`
State ThumbnailState `json:"state"`
PanelId int64 `json:"panelId,omitempty"`
Kind ThumbnailKind `json:"kind"`
Theme models.Theme `json:"theme"`
Image []byte `json:"image"`
MimeType string `json:"mimeType"`
Updated time.Time `json:"updated"`
DsUIDs string `json:"-" xorm:"ds_uids"`
}
//
// Commands
//
// DashboardThumbnailMeta uniquely identifies a thumbnail; a natural key
type DashboardThumbnailMeta struct {
DashboardUID string
OrgId int64
PanelID int64
Kind ThumbnailKind
Theme models.Theme
}
type GetDashboardThumbnailCommand struct {
DashboardThumbnailMeta
}
const DashboardVersionForManualThumbnailUpload = -1
type DashboardWithStaleThumbnail struct {
Id int64
OrgId int64
Uid string
Version int
Slug string
}
type FindDashboardThumbnailCountCommand struct {
}
type FindDashboardsWithStaleThumbnailsCommand struct {
IncludeManuallyUploadedThumbnails bool
IncludeThumbnailsWithEmptyDsUIDs bool
Theme models.Theme
Kind ThumbnailKind
}
type SaveDashboardThumbnailCommand struct {
DashboardThumbnailMeta
DashboardVersion int
Image []byte
MimeType string
DatasourceUIDs []string
}
type UpdateThumbnailStateCommand struct {
State ThumbnailState
DashboardThumbnailMeta
}
type dashRenderer interface {
// Run Assumes you have already authenticated as admin.
Run(ctx context.Context, auth CrawlerAuth, mode CrawlerMode, theme models.Theme, kind ThumbnailKind) error
// Assumes you have already authenticated as admin.
Stop() (crawlStatus, error)
// Assumes you have already authenticated as admin.
Status() (crawlStatus, error)
IsRunning() bool
}
type thumbnailRepo interface {
updateThumbnailState(ctx context.Context, state ThumbnailState, meta DashboardThumbnailMeta) error
doThumbnailsExist(ctx context.Context) (bool, error)
saveFromFile(ctx context.Context, filePath string, meta DashboardThumbnailMeta, dashboardVersion int, dsUids []string) (int64, error)
saveFromBytes(ctx context.Context, bytes []byte, mimeType string, meta DashboardThumbnailMeta, dashboardVersion int, dsUids []string) (int64, error)
getThumbnail(ctx context.Context, meta DashboardThumbnailMeta) (*DashboardThumbnail, error)
findDashboardsWithStaleThumbnails(ctx context.Context, theme models.Theme, thumbnailKind ThumbnailKind) ([]*DashboardWithStaleThumbnail, error)
}

View File

@@ -1,107 +0,0 @@
package thumbs
import (
"context"
"errors"
"os"
"path/filepath"
"strings"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/searchV2"
)
func newThumbnailRepo(thumbsService DashboardThumbService, search searchV2.SearchService) thumbnailRepo {
repo := &sqlThumbnailRepository{
store: thumbsService,
search: search,
log: log.New("thumbnails_repo"),
}
return repo
}
type sqlThumbnailRepository struct {
store DashboardThumbService
search searchV2.SearchService
log log.Logger
}
func (r *sqlThumbnailRepository) saveFromFile(ctx context.Context, filePath string, meta DashboardThumbnailMeta, dashboardVersion int, dsUids []string) (int64, error) {
// the filePath variable is never set by the user. it refers to a temporary file created either in
// 1. thumbs/service.go, when user uploads a thumbnail
// 2. the rendering service, when image-renderer returns a screenshot
if !filepath.IsAbs(filePath) {
r.log.Error("Received relative path", "dashboardUID", meta.DashboardUID, "err", filePath)
return 0, errors.New("relative paths are not supported")
}
content, err := os.ReadFile(filepath.Clean(filePath))
if err != nil {
r.log.Error("error reading file", "dashboardUID", meta.DashboardUID, "err", err)
return 0, err
}
return r.saveFromBytes(ctx, content, getMimeType(filePath), meta, dashboardVersion, dsUids)
}
func getMimeType(filePath string) string {
if strings.HasSuffix(filePath, ".webp") {
return "image/webp"
}
return "image/png"
}
func (r *sqlThumbnailRepository) saveFromBytes(ctx context.Context, content []byte, mimeType string, meta DashboardThumbnailMeta, dashboardVersion int, dsUids []string) (int64, error) {
cmd := &SaveDashboardThumbnailCommand{
DashboardThumbnailMeta: meta,
Image: content,
MimeType: mimeType,
DashboardVersion: dashboardVersion,
DatasourceUIDs: dsUids,
}
result, err := r.store.SaveThumbnail(ctx, cmd)
if err != nil {
r.log.Error("Error saving to the db", "dashboardUID", meta.DashboardUID, "err", err)
return 0, err
}
return result.Id, nil
}
func (r *sqlThumbnailRepository) updateThumbnailState(ctx context.Context, state ThumbnailState, meta DashboardThumbnailMeta) error {
return r.store.UpdateThumbnailState(ctx, &UpdateThumbnailStateCommand{
State: state,
DashboardThumbnailMeta: meta,
})
}
func (r *sqlThumbnailRepository) getThumbnail(ctx context.Context, meta DashboardThumbnailMeta) (*DashboardThumbnail, error) {
query := &GetDashboardThumbnailCommand{
DashboardThumbnailMeta: meta,
}
return r.store.GetThumbnail(ctx, query)
}
func (r *sqlThumbnailRepository) findDashboardsWithStaleThumbnails(ctx context.Context, theme models.Theme, kind ThumbnailKind) ([]*DashboardWithStaleThumbnail, error) {
return r.store.FindDashboardsWithStaleThumbnails(ctx, &FindDashboardsWithStaleThumbnailsCommand{
IncludeManuallyUploadedThumbnails: false,
IncludeThumbnailsWithEmptyDsUIDs: !r.search.IsDisabled(),
Theme: theme,
Kind: kind,
})
}
func (r *sqlThumbnailRepository) doThumbnailsExist(ctx context.Context) (bool, error) {
cmd := &FindDashboardThumbnailCountCommand{}
count, err := r.store.FindThumbnailCount(ctx, cmd)
if err != nil {
r.log.Error("Error finding thumbnails", "err", err)
return false, err
}
return count > 0, err
}

View File

@@ -1,549 +0,0 @@
package thumbs
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"time"
"github.com/segmentio/encoding/json"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/serverlock"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/registry"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/datasources/permissions"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/services/licensing"
"github.com/grafana/grafana/pkg/services/live"
"github.com/grafana/grafana/pkg/services/rendering"
"github.com/grafana/grafana/pkg/services/searchV2"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/web"
)
type Service interface {
registry.ProvidesUsageStats
Run(ctx context.Context) error
Enabled() bool
GetImage(c *contextmodel.ReqContext)
GetDashboardPreviewsSetupSettings(c *contextmodel.ReqContext) dtos.DashboardPreviewsSetupConfig
// from dashboard page
SetImage(c *contextmodel.ReqContext) // form post
UpdateThumbnailState(c *contextmodel.ReqContext)
// Must be admin
StartCrawler(c *contextmodel.ReqContext) response.Response
StopCrawler(c *contextmodel.ReqContext) response.Response
CrawlerStatus(c *contextmodel.ReqContext) response.Response
}
type thumbService struct {
scheduleOptions crawlerScheduleOptions
renderer dashRenderer
renderingService rendering.Service
thumbnailRepo thumbnailRepo
lockService *serverlock.ServerLockService
features featuremgmt.FeatureToggles
store db.DB
crawlLockServiceActionName string
log log.Logger
canRunCrawler bool
settings setting.DashboardPreviewsSettings
dashboardService dashboards.DashboardService
dsUidsLookup getDatasourceUidsForDashboard
dsPermissionsService permissions.DatasourcePermissionsService
licensing licensing.Licensing
searchService searchV2.SearchService
}
type crawlerScheduleOptions struct {
crawlInterval time.Duration
tickerInterval time.Duration
maxCrawlDuration time.Duration
crawlerMode CrawlerMode
thumbnailKind ThumbnailKind
themes []models.Theme
auth CrawlerAuth
}
func ProvideService(cfg *setting.Cfg, features featuremgmt.FeatureToggles,
lockService *serverlock.ServerLockService, renderService rendering.Service,
gl *live.GrafanaLive, store db.DB, authSetupService CrawlerAuthSetupService,
dashboardService dashboards.DashboardService, dashboardThumbsService DashboardThumbService, searchService searchV2.SearchService,
dsPermissionsService permissions.DatasourcePermissionsService, licensing licensing.Licensing) Service {
if !features.IsEnabled(featuremgmt.FlagDashboardPreviews) {
return &dummyService{}
}
logger := log.New("previews_service")
thumbnailRepo := newThumbnailRepo(dashboardThumbsService, searchService)
canRunCrawler := true
authSetupStarted := time.Now()
crawlerAuth, err := authSetupService.Setup(context.Background())
if err != nil {
logger.Error("Crawler auth setup failed", "err", err, "crawlerAuthSetupTime", time.Since(authSetupStarted))
canRunCrawler = false
} else {
logger.Info("Crawler auth setup complete", "crawlerAuthSetupTime", time.Since(authSetupStarted))
}
dsUidsLookup := &dsUidsLookup{
searchService: searchService,
crawlerAuth: crawlerAuth,
features: features,
}
t := &thumbService{
licensing: licensing,
renderingService: renderService,
renderer: newSimpleCrawler(renderService, gl, thumbnailRepo, cfg, cfg.DashboardPreviews, dsUidsLookup.getDatasourceUidsForDashboard),
thumbnailRepo: thumbnailRepo,
store: store,
features: features,
lockService: lockService,
crawlLockServiceActionName: "dashboard-crawler",
searchService: searchService,
log: logger,
canRunCrawler: canRunCrawler,
dsUidsLookup: dsUidsLookup.getDatasourceUidsForDashboard,
settings: cfg.DashboardPreviews,
dsPermissionsService: dsPermissionsService,
scheduleOptions: crawlerScheduleOptions{
tickerInterval: 5 * time.Minute,
crawlInterval: cfg.DashboardPreviews.SchedulerInterval,
maxCrawlDuration: cfg.DashboardPreviews.MaxCrawlDuration,
crawlerMode: CrawlerModeThumbs,
thumbnailKind: ThumbnailKindDefault,
themes: []models.Theme{models.ThemeDark, models.ThemeLight},
auth: crawlerAuth,
},
dashboardService: dashboardService,
}
return t
}
func (hs *thumbService) GetUsageStats(ctx context.Context) map[string]interface{} {
s := hs.getDashboardPreviewsSetupSettings(ctx)
stats := make(map[string]interface{})
if s.SystemRequirements.Met {
stats["stats.dashboard_previews.system_req_met.count"] = 1
}
if s.ThumbnailsExist {
stats["stats.dashboard_previews.thumbnails_exist.count"] = 1
}
return stats
}
func (hs *thumbService) Enabled() bool {
return hs.features.IsEnabled(featuremgmt.FlagDashboardPreviews)
}
func (hs *thumbService) parseImageReq(c *contextmodel.ReqContext, checkSave bool) *previewRequest {
params := web.Params(c.Req)
kind, err := ParseThumbnailKind(params[":kind"])
if err != nil {
c.JSON(400, map[string]string{"error": "invalid size"})
return nil
}
theme, err := models.ParseTheme(params[":theme"])
if err != nil {
c.JSON(400, map[string]string{"error": "invalid theme"})
return nil
}
req := &previewRequest{
OrgID: c.OrgID,
UID: params[":uid"],
Theme: theme,
Kind: kind,
}
if len(req.UID) < 1 {
c.JSON(400, map[string]string{"error": "missing UID"})
return nil
}
// Check permissions and status
status, err := hs.getStatus(c, req.UID, checkSave)
if err != nil {
c.JSON(status, map[string]string{"error": err.Error()})
return nil
}
if status != 200 {
c.JSON(status, map[string]string{"error": fmt.Sprintf("code: %d", status)})
return nil
}
return req
}
type updateThumbnailStateRequest struct {
State ThumbnailState `json:"state" binding:"Required"`
}
func (hs *thumbService) UpdateThumbnailState(c *contextmodel.ReqContext) {
req := hs.parseImageReq(c, false)
if req == nil {
return // already returned value
}
var body = &updateThumbnailStateRequest{}
err := web.Bind(c.Req, body)
if err != nil {
hs.log.Error("Error parsing update thumbnail state request", "dashboardUid", req.UID, "err", err.Error())
c.JSON(500, map[string]string{"dashboardUID": req.UID, "error": "unknown"})
return
}
err = hs.thumbnailRepo.updateThumbnailState(c.Req.Context(), body.State, DashboardThumbnailMeta{
DashboardUID: req.UID,
OrgId: req.OrgID,
Theme: req.Theme,
Kind: ThumbnailKindDefault,
})
if err != nil {
hs.log.Error("Error when trying to update thumbnail state", "dashboardUid", req.UID, "err", err.Error(), "newState", body.State)
c.JSON(500, map[string]string{"dashboardUID": req.UID, "error": "unknown"})
return
}
hs.log.Info("Updated dashboard thumbnail state", "dashboardUid", req.UID, "theme", req.Theme, "newState", body.State)
c.JSON(http.StatusOK, map[string]string{"success": "true"})
}
func (hs *thumbService) GetImage(c *contextmodel.ReqContext) {
req := hs.parseImageReq(c, false)
if req == nil {
return // already returned value
}
res, err := hs.thumbnailRepo.getThumbnail(c.Req.Context(), DashboardThumbnailMeta{
DashboardUID: req.UID,
OrgId: req.OrgID,
Theme: req.Theme,
Kind: ThumbnailKindDefault,
})
if errors.Is(err, dashboards.ErrDashboardThumbnailNotFound) {
c.Resp.WriteHeader(404)
return
}
if err != nil || res == nil {
hs.log.Error("Error when retrieving thumbnail", "dashboardUid", req.UID, "err", err.Error())
c.JSON(500, map[string]string{"dashboardUID": req.UID, "error": "unknown"})
return
}
if !hs.hasAccessToPreview(c, res, req) {
return
}
currentEtag := fmt.Sprintf("%d", res.Updated.Unix())
c.Resp.Header().Set("ETag", currentEtag)
previousEtag := c.Req.Header.Get("If-None-Match")
if previousEtag == currentEtag {
c.Resp.WriteHeader(http.StatusNotModified)
return
}
c.Resp.Header().Set("Content-Type", res.MimeType)
if _, err := c.Resp.Write(res.Image); err != nil {
hs.log.Error("Error writing to response", "dashboardUid", req.UID, "err", err)
}
}
func (hs *thumbService) hasAccessToPreview(c *contextmodel.ReqContext, res *DashboardThumbnail, req *previewRequest) bool {
if !hs.licensing.FeatureEnabled("accesscontrol.enforcement") {
return true
}
if hs.searchService.IsDisabled() {
c.JSON(404, map[string]string{"dashboardUID": req.UID, "error": "unknown"})
return false
}
if res.DsUIDs == "" {
hs.log.Debug("dashboard preview is stale; no datasource uids", "dashboardUid", req.UID)
c.JSON(404, map[string]string{"dashboardUID": req.UID, "error": "unknown"})
return false
}
var dsUids []string
err := json.Unmarshal([]byte(res.DsUIDs), &dsUids)
if err != nil {
hs.log.Error("Error when retrieving datasource uids", "dashboardUid", req.UID, "err", err)
c.JSON(404, map[string]string{"dashboardUID": req.UID, "error": "unknown"})
return false
}
accessibleDatasources, err := hs.dsPermissionsService.FilterDatasourceUidsBasedOnQueryPermissions(c.Req.Context(), c.SignedInUser, dsUids)
if err != nil && !errors.Is(err, permissions.ErrNotImplemented) {
hs.log.Error("Error when filtering datasource uids", "dashboardUid", req.UID, "err", err)
c.JSON(500, map[string]string{"dashboardUID": req.UID, "error": "unknown"})
return false
}
if !errors.Is(err, permissions.ErrNotImplemented) {
canQueryAllDatasources := len(accessibleDatasources) == len(dsUids)
if !canQueryAllDatasources {
hs.log.Info("Denied access to dashboard preview", "dashboardUid", req.UID, "err", err, "dashboardDatasources", dsUids, "accessibleDatasources", accessibleDatasources)
c.JSON(404, map[string]string{"dashboardUID": req.UID, "error": "unknown"})
return false
}
}
return true
}
func (hs *thumbService) GetDashboardPreviewsSetupSettings(c *contextmodel.ReqContext) dtos.DashboardPreviewsSetupConfig {
return hs.getDashboardPreviewsSetupSettings(c.Req.Context())
}
func (hs *thumbService) getDashboardPreviewsSetupSettings(ctx context.Context) dtos.DashboardPreviewsSetupConfig {
systemRequirements := hs.getSystemRequirements(ctx)
thumbnailsExist, err := hs.thumbnailRepo.doThumbnailsExist(ctx)
if err != nil {
return dtos.DashboardPreviewsSetupConfig{
SystemRequirements: systemRequirements,
ThumbnailsExist: false,
}
}
return dtos.DashboardPreviewsSetupConfig{
SystemRequirements: systemRequirements,
ThumbnailsExist: thumbnailsExist,
}
}
func (hs *thumbService) getSystemRequirements(ctx context.Context) dtos.DashboardPreviewsSystemRequirements {
res, err := hs.renderingService.HasCapability(ctx, rendering.ScalingDownImages)
if err != nil {
hs.log.Error("Error when verifying dashboard previews system requirements thumbnail", "err", err.Error())
return dtos.DashboardPreviewsSystemRequirements{
Met: false,
}
}
if !res.IsSupported {
return dtos.DashboardPreviewsSystemRequirements{
Met: false,
RequiredImageRendererPluginVersion: res.SemverConstraint,
}
}
return dtos.DashboardPreviewsSystemRequirements{
Met: true,
}
}
// Hack for now -- lets you upload images explicitly
func (hs *thumbService) SetImage(c *contextmodel.ReqContext) {
req := hs.parseImageReq(c, false)
if req == nil {
return // already returned value
}
r := c.Req
// Parse our multipart form, 10 << 20 specifies a maximum
// upload of 10 MB files.
err := r.ParseMultipartForm(10 << 20)
if err != nil {
c.JSON(400, map[string]string{"error": "invalid upload size"})
return
}
// FormFile returns the first file for the given key `myFile`
// it also returns the FileHeader so we can get the Filename,
// the Header and the size of the file
file, handler, err := r.FormFile("file")
if err != nil {
c.JSON(400, map[string]string{"error": "missing multi-part form field named 'file'"})
fmt.Println("error", err)
return
}
defer func() {
_ = file.Close()
}()
hs.log.Info("Uploaded File: %+v\n", handler.Filename)
hs.log.Info("File Size: %+v\n", handler.Size)
hs.log.Info("MIME Header: %+v\n", handler.Header)
fileBytes, err := io.ReadAll(file)
if err != nil {
fmt.Println(err)
c.JSON(400, map[string]string{"error": "error reading file"})
return
}
dsUids, err := hs.dsUidsLookup(c.Req.Context(), req.UID, req.OrgID)
if err != nil {
hs.log.Error("error looking up datasource ids", "err", err, "dashboardUid", req.UID)
c.JSON(500, map[string]string{"error": "internal server error"})
return
}
_, err = hs.thumbnailRepo.saveFromBytes(c.Req.Context(), fileBytes, getMimeType(handler.Filename), DashboardThumbnailMeta{
DashboardUID: req.UID,
OrgId: req.OrgID,
Theme: req.Theme,
Kind: req.Kind,
}, DashboardVersionForManualThumbnailUpload, dsUids)
if err != nil {
c.JSON(400, map[string]string{"error": "error saving thumbnail file"})
fmt.Println("error", err)
return
}
c.JSON(http.StatusOK, map[string]int{"OK": len(fileBytes)})
}
func (hs *thumbService) StartCrawler(c *contextmodel.ReqContext) response.Response {
body, err := io.ReadAll(c.Req.Body)
if err != nil {
return response.Error(500, "error reading bytes", err)
}
cmd := &crawlCmd{}
err = json.Unmarshal(body, cmd)
if err != nil {
return response.Error(500, "error parsing bytes", err)
}
if cmd.Mode == "" {
cmd.Mode = CrawlerModeThumbs
}
go hs.runOnDemandCrawl(context.Background(), cmd.Theme, cmd.Mode, ThumbnailKindDefault, rendering.AuthOpts{
OrgID: c.OrgID,
UserID: c.UserID,
OrgRole: c.OrgRole,
})
status, err := hs.renderer.Status()
if err != nil {
return response.Error(500, "error starting", err)
}
return response.JSON(http.StatusOK, status)
}
func (hs *thumbService) StopCrawler(c *contextmodel.ReqContext) response.Response {
msg, err := hs.renderer.Stop()
if err != nil {
return response.Error(500, "error starting", err)
}
return response.JSON(http.StatusOK, msg)
}
func (hs *thumbService) CrawlerStatus(c *contextmodel.ReqContext) response.Response {
msg, err := hs.renderer.Status()
if err != nil {
return response.Error(500, "error starting", err)
}
return response.JSON(http.StatusOK, msg)
}
// Ideally this service would not require first looking up the full dashboard just to bet the id!
func (hs *thumbService) getStatus(c *contextmodel.ReqContext, uid string, checkSave bool) (int, error) {
guardian, err := guardian.NewByUID(c.Req.Context(), uid, c.OrgID, c.SignedInUser)
if err != nil {
return 0, err
}
if checkSave {
if canSave, err := guardian.CanSave(); err != nil || !canSave {
return 403, nil // forbidden
}
return 200, nil
}
if canView, err := guardian.CanView(); err != nil || !canView {
return 403, nil // forbidden
}
return 200, nil // found and OK
}
func (hs *thumbService) runOnDemandCrawl(parentCtx context.Context, theme models.Theme, mode CrawlerMode, kind ThumbnailKind, authOpts rendering.AuthOpts) {
if !hs.canRunCrawler {
return
}
crawlerCtx, cancel := context.WithTimeout(parentCtx, hs.scheduleOptions.maxCrawlDuration)
defer cancel()
// wait for at least a minute after the last completed run
interval := time.Minute
err := hs.lockService.LockAndExecute(crawlerCtx, hs.crawlLockServiceActionName, interval, func(ctx context.Context) {
if err := hs.renderer.Run(crawlerCtx, hs.scheduleOptions.auth, mode, theme, kind); err != nil {
hs.log.Error("On demand crawl error", "mode", mode, "theme", theme, "kind", kind, "userId", authOpts.UserID, "orgId", authOpts.OrgID, "orgRole", authOpts.OrgRole)
}
})
if err != nil {
hs.log.Error("On demand crawl lock error", "err", err)
}
}
func (hs *thumbService) runScheduledCrawl(parentCtx context.Context) {
crawlerCtx, cancel := context.WithTimeout(parentCtx, hs.scheduleOptions.maxCrawlDuration)
defer cancel()
err := hs.lockService.LockAndExecute(crawlerCtx, hs.crawlLockServiceActionName, hs.scheduleOptions.crawlInterval, func(ctx context.Context) {
for _, theme := range hs.scheduleOptions.themes {
if err := hs.renderer.Run(crawlerCtx, hs.scheduleOptions.auth, hs.scheduleOptions.crawlerMode, theme, hs.scheduleOptions.thumbnailKind); err != nil {
hs.log.Error("Scheduled crawl error", "theme", theme, "kind", hs.scheduleOptions.thumbnailKind, "err", err)
}
}
})
if err != nil {
hs.log.Error("Scheduled crawl lock error", "err", err)
}
}
func (hs *thumbService) Run(ctx context.Context) error {
if !hs.canRunCrawler {
return nil
}
gc := time.NewTicker(hs.scheduleOptions.tickerInterval)
for {
select {
case <-gc.C:
go hs.runScheduledCrawl(ctx)
case <-ctx.Done():
hs.log.Debug("Grafana is shutting down - stopping dashboard crawler")
gc.Stop()
return nil
}
}
}

View File

@@ -1,120 +0,0 @@
{
"schema": {
"name": "Query results",
"refId": "Search",
"meta": {
"type": "search-results",
"custom": {
"count": 106,
"locationInfo": {
"yboVMzb7z": {
"name": "gdev dashboards",
"kind": "folder",
"url": "/dashboards/f/yboVMzb7z/gdev-dashboards"
}
},
"sortBy": "name_sort"
}
},
"fields": [
{
"name": "kind",
"type": "string",
"typeInfo": {
"frame": "string"
}
},
{
"name": "uid",
"type": "string",
"typeInfo": {
"frame": "string"
}
},
{
"name": "name",
"type": "string",
"typeInfo": {
"frame": "string"
}
},
{
"name": "panel_type",
"type": "string",
"typeInfo": {
"frame": "string"
}
},
{
"name": "url",
"type": "string",
"typeInfo": {
"frame": "string"
},
"config": {
"links": [
{
"title": "link",
"url": "${__value.text}"
}
]
}
},
{
"name": "tags",
"type": "other",
"typeInfo": {
"frame": "json.RawMessage",
"nullable": true
}
},
{
"name": "ds_uid",
"type": "other",
"typeInfo": {
"frame": "json.RawMessage",
"nullable": false
}
},
{
"name": "location",
"type": "string",
"typeInfo": {
"frame": "string"
}
}
]
},
"data": {
"values": [
[
"dashboard"
],
[
"ujaM1h6nz"
],
[
"abc2"
],
[
""
],
[
"/dashboards/f/ujaM1h6nz/abc2"
],
[
[
"gdev"
]
],
[
[
"grafana"
]
],
[
""
]
]
}
}

View File

@@ -1,123 +0,0 @@
{
"schema": {
"name": "Query results",
"refId": "Search",
"meta": {
"type": "search-results",
"custom": {
"count": 106,
"locationInfo": {
"yboVMzb7z": {
"name": "gdev dashboards",
"kind": "folder",
"url": "/dashboards/f/yboVMzb7z/gdev-dashboards"
}
},
"sortBy": "name_sort"
}
},
"fields": [
{
"name": "kind",
"type": "string",
"typeInfo": {
"frame": "string"
}
},
{
"name": "uid",
"type": "string",
"typeInfo": {
"frame": "string"
}
},
{
"name": "name",
"type": "string",
"typeInfo": {
"frame": "string"
}
},
{
"name": "panel_type",
"type": "string",
"typeInfo": {
"frame": "string"
}
},
{
"name": "url",
"type": "string",
"typeInfo": {
"frame": "string"
},
"config": {
"links": [
{
"title": "link",
"url": "${__value.text}"
}
]
}
},
{
"name": "tags",
"type": "other",
"typeInfo": {
"frame": "json.RawMessage",
"nullable": true
}
},
{
"name": "ds_uid",
"type": "other",
"typeInfo": {
"frame": "json.RawMessage",
"nullable": false
}
},
{
"name": "location",
"type": "string",
"typeInfo": {
"frame": "string"
}
}
]
},
"data": {
"values": [
[
"dashboard"
],
[
"ujaM1h6nz"
],
[
"abc2"
],
[
""
],
[
"/dashboards/f/ujaM1h6nz/abc2"
],
[
[
"gdev"
]
],
[
[
"datasource-2",
"datasource-3",
"datasource-4",
"grafana"
]
],
[
""
]
]
}
}

View File

@@ -484,8 +484,6 @@ type Cfg struct {
// Query history
QueryHistoryEnabled bool
DashboardPreviews DashboardPreviewsSettings
Storage StorageSettings
Search SearchSettings
@@ -1116,7 +1114,6 @@ func (cfg *Cfg) Load(args CommandLineArgs) error {
cfg.readDataSourcesSettings()
cfg.DashboardPreviews = readDashboardPreviewsSettings(iniFile)
cfg.Storage = readStorageSettings(iniFile)
cfg.Search = readSearchSettings(iniFile)

View File

@@ -1,31 +0,0 @@
package setting
import (
"time"
"gopkg.in/ini.v1"
)
type DashboardPreviewsSettings struct {
SchedulerInterval time.Duration
MaxCrawlDuration time.Duration
RenderingTimeout time.Duration
CrawlThreadCount uint32
}
func readDashboardPreviewsSettings(iniFile *ini.File) DashboardPreviewsSettings {
maxThreadCount := uint32(20)
s := DashboardPreviewsSettings{}
previewsCrawlerSection := iniFile.Section("dashboard_previews.crawler")
s.CrawlThreadCount = uint32(previewsCrawlerSection.Key("thread_count").MustUint(6))
if s.CrawlThreadCount > maxThreadCount {
s.CrawlThreadCount = maxThreadCount
}
s.SchedulerInterval = previewsCrawlerSection.Key("scheduler_interval").MustDuration(12 * time.Hour)
s.MaxCrawlDuration = previewsCrawlerSection.Key("max_crawl_duration").MustDuration(1 * time.Hour)
s.RenderingTimeout = previewsCrawlerSection.Key("rendering_timeout").MustDuration(20 * time.Second)
return s
}