Previews: capability check (#44601)

* add SQL migrations

* dashboard previews from sql: poc

* added todos

* refactor: use the same enums where possible

* use useEffect, always return json

* added todo

* refactor + delete files after use

* refactor + fix manual thumbnail upload

* refactor: move all interactions with sqlStore to thumbnail repo

* refactor: remove file operations in thumb crawler/service

* refactor: fix dashboard_thumbs sql store

* refactor: extracted thumbnail fetching/updating to a hook

* refactor: store thumbnails in redux store

* refactor: store thumbnails in redux store

* refactor: private'd repo methods

* removed redux storage, saving images as blobs

* allow for configurable rendering timeouts

* added 1) query for dashboards with stale thumbnails, 2) command for marking thumbnails as stale

* use sql-based queue in crawler

* ui for marking thumbnails as stale

* replaced `stale` boolean prop with `state` enum

* introduce rendering session

* compilation errors

* fix crawler stop button

* rename thumbnail state frozen to locked

* #44449: fix merge conflicts

* #44449: remove thumb methods from `Store` interface

* #44449: clean filepath, defer file closing

* #44449: fix rendering.Theme cyclic import

* #44449: linting

* #44449: linting

* #44449: mutex'd crawlerStatus access

* #44449: added integration tests for `sqlstore.dashboard_thumbs`

* #44449: added comments to explain the `ThumbnailState` enum

* #44449: use os.ReadFile rather then os.Open

* #44449: always enable dashboardPreviews feature during integration tests

* #44449: add /previews/system-requirements API

* #44449: remove sleep time, adjust number of threads

* #44449: review fix: add `orgId` to `DashboardThumbnailMeta`

* #44449: review fix: automatic parsing of thumbnailState

* #44449: update returned json

* #44449: UI changes - dashboard previews sytem req check

* #44449: lint fixes

* #44449: fix tests

* #44449: typo

* #44449: fix getSystemRequirements API: return 200 even if we plugin version is invalid

* #44449: fix getSystemRequirements API: don't return SemverConstraint on error

* #44449: fix getSystemRequirements API

* #44449: fix previews sytem requirements text

* #44449: add `doThumbnailsExist` to repo

* #44449: remove redux api

* #44449: add missing model

* #44449: implement frontedsettings-driven capability check

* #44449: simplify

* #44449: revert test changes

* #44449: add dummy setup settings

* #44449: implicit typing over `FC<Props>`

* #44449: refactor conditionals

* #44449: replace `getText` with a react component

* #44449: fix component interface

* #44449: add onRemove to `PreviewsSystemRequirements` alert

* #44449: add bottom/top margin to previewSystemRequirements modal

* #44449: merge conflict fix

* #44449: remove console.log

Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
Co-authored-by: Alexander Emelin <frvzmb@gmail.com>
This commit is contained in:
Artur Wierzbicki 2022-02-16 21:49:50 +04:00 committed by GitHub
parent 1554bffcb8
commit 6c76aa71e8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 298 additions and 52 deletions

View File

@ -66,6 +66,13 @@ export class GrafanaBootConfig implements GrafanaConfig {
featureToggles: FeatureToggles = {}; featureToggles: FeatureToggles = {};
licenseInfo: LicenseInfo = {} as LicenseInfo; licenseInfo: LicenseInfo = {} as LicenseInfo;
rendererAvailable = false; rendererAvailable = false;
dashboardPreviews: {
systemRequirements: {
met: boolean;
requiredImageRendererPluginVersion: string;
};
thumbnailsExist: boolean;
} = { systemRequirements: { met: false, requiredImageRendererPluginVersion: '' }, thumbnailsExist: false };
rendererVersion = ''; rendererVersion = '';
http2Enabled = false; http2Enabled = false;
dateFormats?: SystemDateFormatSettings; dateFormats?: SystemDateFormatSettings;

View File

@ -19,6 +19,7 @@ export interface Props extends HTMLAttributes<HTMLDivElement> {
elevated?: boolean; elevated?: boolean;
buttonContent?: React.ReactNode | string; buttonContent?: React.ReactNode | string;
bottomSpacing?: number; bottomSpacing?: number;
topSpacing?: number;
} }
function getIconFromSeverity(severity: AlertVariant): string { function getIconFromSeverity(severity: AlertVariant): string {
@ -37,11 +38,22 @@ function getIconFromSeverity(severity: AlertVariant): string {
export const Alert = React.forwardRef<HTMLDivElement, Props>( export const Alert = React.forwardRef<HTMLDivElement, Props>(
( (
{ title, onRemove, children, buttonContent, elevated, bottomSpacing, className, severity = 'error', ...restProps }, {
title,
onRemove,
children,
buttonContent,
elevated,
bottomSpacing,
topSpacing,
className,
severity = 'error',
...restProps
},
ref ref
) => { ) => {
const theme = useTheme2(); const theme = useTheme2();
const styles = getStyles(theme, severity, elevated, bottomSpacing); const styles = getStyles(theme, severity, elevated, bottomSpacing, topSpacing);
return ( return (
<div <div
@ -77,7 +89,13 @@ export const Alert = React.forwardRef<HTMLDivElement, Props>(
Alert.displayName = 'Alert'; Alert.displayName = 'Alert';
const getStyles = (theme: GrafanaTheme2, severity: AlertVariant, elevated?: boolean, bottomSpacing?: number) => { const getStyles = (
theme: GrafanaTheme2,
severity: AlertVariant,
elevated?: boolean,
bottomSpacing?: number,
topSpacing?: number
) => {
const color = theme.colors[severity]; const color = theme.colors[severity];
const borderRadius = theme.shape.borderRadius(); const borderRadius = theme.shape.borderRadius();
@ -92,6 +110,7 @@ const getStyles = (theme: GrafanaTheme2, severity: AlertVariant, elevated?: bool
background: ${theme.colors.background.secondary}; background: ${theme.colors.background.secondary};
box-shadow: ${elevated ? theme.shadows.z3 : theme.shadows.z1}; box-shadow: ${elevated ? theme.shadows.z3 : theme.shadows.z1};
margin-bottom: ${theme.spacing(bottomSpacing ?? 2)}; margin-bottom: ${theme.spacing(bottomSpacing ?? 2)};
margin-top: ${theme.spacing(topSpacing ?? 0)};
&:before { &:before {
content: ''; content: '';

View File

@ -275,6 +275,10 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *models.ReqContext) (map[string]i
"unifiedAlertingEnabled": hs.Cfg.UnifiedAlerting.Enabled, "unifiedAlertingEnabled": hs.Cfg.UnifiedAlerting.Enabled,
} }
if hs.ThumbService != nil {
jsonObj["dashboardPreviews"] = hs.ThumbService.GetDashboardPreviewsSetupSettings(c)
}
if hs.Cfg.GeomapDefaultBaseLayerConfig != nil { if hs.Cfg.GeomapDefaultBaseLayerConfig != nil {
jsonObj["geomapDefaultBaseLayerConfig"] = hs.Cfg.GeomapDefaultBaseLayerConfig jsonObj["geomapDefaultBaseLayerConfig"] = hs.Cfg.GeomapDefaultBaseLayerConfig
} }

View File

@ -117,6 +117,10 @@ type DashboardWithStaleThumbnail struct {
Slug string Slug string
} }
type FindDashboardThumbnailCountCommand struct {
Result int64
}
type FindDashboardsWithStaleThumbnailsCommand struct { type FindDashboardsWithStaleThumbnailsCommand struct {
IncludeManuallyUploadedThumbnails bool IncludeManuallyUploadedThumbnails bool
Theme Theme Theme Theme

View File

@ -81,6 +81,20 @@ func (ss *SQLStore) UpdateThumbnailState(ctx context.Context, cmd *models.Update
return err return err
} }
func (ss *SQLStore) FindThumbnailCount(ctx context.Context, cmd *models.FindDashboardThumbnailCountCommand) (int64, error) {
err := ss.WithDbSession(ctx, func(sess *DBSession) error {
count, err := sess.Count(&models.DashboardThumbnail{})
if err != nil {
return err
}
cmd.Result = count
return nil
})
return cmd.Result, err
}
func (ss *SQLStore) FindDashboardsWithStaleThumbnails(ctx context.Context, cmd *models.FindDashboardsWithStaleThumbnailsCommand) ([]*models.DashboardWithStaleThumbnail, error) { func (ss *SQLStore) FindDashboardsWithStaleThumbnails(ctx context.Context, cmd *models.FindDashboardsWithStaleThumbnailsCommand) ([]*models.DashboardWithStaleThumbnail, error) {
err := ss.WithDbSession(ctx, func(sess *DBSession) error { err := ss.WithDbSession(ctx, func(sess *DBSession) error {
sess.Table("dashboard") sess.Table("dashboard")

View File

@ -185,6 +185,23 @@ func TestSqlStorage(t *testing.T) {
require.Len(t, res, 1) require.Len(t, res, 1)
require.Equal(t, dash.Id, res[0].Id) 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, sqlStore, dash.Uid, dash.OrgId, 1)
dash2 := insertTestDashboard(t, sqlStore, "test dash 23", 2, savedFolder.Id, false, "prod", "webapp")
upsertTestDashboardThumbnail(t, sqlStore, dash2.Uid, dash2.OrgId, 1)
updateTestDashboard(t, sqlStore, dash, map[string]interface{}{
"tags": "different-tag",
})
cmd := models.FindDashboardThumbnailCountCommand{}
res, err := sqlStore.FindThumbnailCount(context.Background(), &cmd)
require.NoError(t, err)
require.Equal(t, res, int64(2))
})
} }
func getThumbnail(t *testing.T, sqlStore *SQLStore, dashboardUID string, orgId int64) *models.DashboardThumbnail { func getThumbnail(t *testing.T, sqlStore *SQLStore, dashboardUID string, orgId int64) *models.DashboardThumbnail {

View File

@ -26,6 +26,16 @@ func (ds *dummyService) Enabled() bool {
return false return false
} }
func (ds *dummyService) GetDashboardPreviewsSetupSettings(c *models.ReqContext) dashboardPreviewsSetupConfig {
return dashboardPreviewsSetupConfig{
SystemRequirements: dashboardPreviewsSystemRequirements{
Met: false,
RequiredImageRendererPluginVersion: "",
},
ThumbnailsExist: false,
}
}
func (ds *dummyService) StartCrawler(c *models.ReqContext) response.Response { func (ds *dummyService) StartCrawler(c *models.ReqContext) response.Response {
result := make(map[string]string) result := make(map[string]string)
result["error"] = "Not enabled" result["error"] = "Not enabled"

View File

@ -53,6 +53,16 @@ type crawlStatus struct {
Last time.Time `json:"last,omitempty"` Last time.Time `json:"last,omitempty"`
} }
type dashboardPreviewsSystemRequirements struct {
Met bool `json:"met"`
RequiredImageRendererPluginVersion string `json:"requiredImageRendererPluginVersion"`
}
type dashboardPreviewsSetupConfig struct {
SystemRequirements dashboardPreviewsSystemRequirements `json:"systemRequirements"`
ThumbnailsExist bool `json:"thumbnailsExist"`
}
type dashRenderer interface { type dashRenderer interface {
// Run Assumes you have already authenticated as admin. // Run Assumes you have already authenticated as admin.
@ -69,6 +79,7 @@ type dashRenderer interface {
type thumbnailRepo interface { type thumbnailRepo interface {
updateThumbnailState(ctx context.Context, state models.ThumbnailState, meta models.DashboardThumbnailMeta) error updateThumbnailState(ctx context.Context, state models.ThumbnailState, meta models.DashboardThumbnailMeta) error
doThumbnailsExist(ctx context.Context) (bool, error)
saveFromFile(ctx context.Context, filePath string, meta models.DashboardThumbnailMeta, dashboardVersion int) (int64, error) saveFromFile(ctx context.Context, filePath string, meta models.DashboardThumbnailMeta, dashboardVersion int) (int64, error)
saveFromBytes(ctx context.Context, bytes []byte, mimeType string, meta models.DashboardThumbnailMeta, dashboardVersion int) (int64, error) saveFromBytes(ctx context.Context, bytes []byte, mimeType string, meta models.DashboardThumbnailMeta, dashboardVersion int) (int64, error)
getThumbnail(ctx context.Context, meta models.DashboardThumbnailMeta) (*models.DashboardThumbnail, error) getThumbnail(ctx context.Context, meta models.DashboardThumbnailMeta) (*models.DashboardThumbnail, error)

View File

@ -63,7 +63,7 @@ func (r *sqlThumbnailRepository) saveFromBytes(ctx context.Context, content []by
_, err := r.store.SaveThumbnail(ctx, cmd) _, err := r.store.SaveThumbnail(ctx, cmd)
if err != nil { if err != nil {
r.log.Error("error saving to the db", "dashboardUID", meta.DashboardUID, "err", err) r.log.Error("Error saving to the db", "dashboardUID", meta.DashboardUID, "err", err)
return 0, err return 0, err
} }
@ -91,3 +91,13 @@ func (r *sqlThumbnailRepository) findDashboardsWithStaleThumbnails(ctx context.C
Kind: kind, Kind: kind,
}) })
} }
func (r *sqlThumbnailRepository) doThumbnailsExist(ctx context.Context) (bool, error) {
cmd := &models.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

@ -27,6 +27,7 @@ type Service interface {
Run(ctx context.Context) error Run(ctx context.Context) error
Enabled() bool Enabled() bool
GetImage(c *models.ReqContext) GetImage(c *models.ReqContext)
GetDashboardPreviewsSetupSettings(c *models.ReqContext) dashboardPreviewsSetupConfig
// from dashboard page // from dashboard page
SetImage(c *models.ReqContext) // form post SetImage(c *models.ReqContext) // form post
@ -41,6 +42,7 @@ type Service interface {
type thumbService struct { type thumbService struct {
scheduleOptions crawlerScheduleOptions scheduleOptions crawlerScheduleOptions
renderer dashRenderer renderer dashRenderer
renderingService rendering.Service
thumbnailRepo thumbnailRepo thumbnailRepo thumbnailRepo
lockService *serverlock.ServerLockService lockService *serverlock.ServerLockService
features featuremgmt.FeatureToggles features featuremgmt.FeatureToggles
@ -71,6 +73,7 @@ func ProvideService(cfg *setting.Cfg, features featuremgmt.FeatureToggles, lockS
OrgRole: models.ROLE_ADMIN, OrgRole: models.ROLE_ADMIN,
} }
return &thumbService{ return &thumbService{
renderingService: renderService,
renderer: newSimpleCrawler(renderService, gl, thumbnailRepo), renderer: newSimpleCrawler(renderService, gl, thumbnailRepo),
thumbnailRepo: thumbnailRepo, thumbnailRepo: thumbnailRepo,
features: features, features: features,
@ -196,6 +199,44 @@ func (hs *thumbService) GetImage(c *models.ReqContext) {
} }
} }
func (hs *thumbService) GetDashboardPreviewsSetupSettings(c *models.ReqContext) dashboardPreviewsSetupConfig {
systemRequirements := hs.getSystemRequirements()
thumbnailsExist, err := hs.thumbnailRepo.doThumbnailsExist(c.Req.Context())
if err != nil {
return dashboardPreviewsSetupConfig{
SystemRequirements: systemRequirements,
ThumbnailsExist: false,
}
}
return dashboardPreviewsSetupConfig{
SystemRequirements: systemRequirements,
ThumbnailsExist: thumbnailsExist,
}
}
func (hs *thumbService) getSystemRequirements() dashboardPreviewsSystemRequirements {
res, err := hs.renderingService.HasCapability(rendering.ScalingDownImages)
if err != nil {
hs.log.Error("Error when verifying dashboard previews system requirements thumbnail", "err", err.Error())
return dashboardPreviewsSystemRequirements{
Met: false,
}
}
if !res.IsSupported {
return dashboardPreviewsSystemRequirements{
Met: false,
RequiredImageRendererPluginVersion: res.SemverConstraint,
}
}
return dashboardPreviewsSystemRequirements{
Met: true,
}
}
// Hack for now -- lets you upload images explicitly // Hack for now -- lets you upload images explicitly
func (hs *thumbService) SetImage(c *models.ReqContext) { func (hs *thumbService) SetImage(c *models.ReqContext) {
req := hs.parseImageReq(c, false) req := hs.parseImageReq(c, false)

View File

@ -17,7 +17,7 @@ const searchSrv = new SearchSrv();
interface Props { interface Props {
onLayoutChange: (layout: SearchLayout) => void; onLayoutChange: (layout: SearchLayout) => void;
onShowPreviewsChange: (event: ChangeEvent<HTMLInputElement>) => void; setShowPreviews: (newValue: boolean) => void;
onSortChange: (value: SelectableValue) => void; onSortChange: (value: SelectableValue) => void;
onStarredFilterChange?: (event: FormEvent<HTMLInputElement>) => void; onStarredFilterChange?: (event: FormEvent<HTMLInputElement>) => void;
onTagFilterChange: (tags: string[]) => void; onTagFilterChange: (tags: string[]) => void;
@ -29,7 +29,7 @@ interface Props {
export const ActionRow: FC<Props> = ({ export const ActionRow: FC<Props> = ({
onLayoutChange, onLayoutChange,
onShowPreviewsChange, setShowPreviews,
onSortChange, onSortChange,
onStarredFilterChange = () => {}, onStarredFilterChange = () => {},
onTagFilterChange, onTagFilterChange,
@ -56,7 +56,7 @@ export const ActionRow: FC<Props> = ({
label="Show previews" label="Show previews"
showLabel showLabel
value={showPreviews} value={showPreviews}
onChange={onShowPreviewsChange} onChange={(ev: ChangeEvent<HTMLInputElement>) => setShowPreviews(ev.target.checked)}
transparent transparent
/> />
)} )}

View File

@ -7,6 +7,7 @@ import { useDashboardSearch } from '../hooks/useDashboardSearch';
import { SearchField } from './SearchField'; import { SearchField } from './SearchField';
import { SearchResults } from './SearchResults'; import { SearchResults } from './SearchResults';
import { ActionRow } from './ActionRow'; import { ActionRow } from './ActionRow';
import { PreviewsSystemRequirements } from './PreviewsSystemRequirements';
export interface Props { export interface Props {
onCloseSearch: () => void; onCloseSearch: () => void;
@ -14,7 +15,7 @@ export interface Props {
export const DashboardSearch: FC<Props> = memo(({ onCloseSearch }) => { export const DashboardSearch: FC<Props> = memo(({ onCloseSearch }) => {
const { query, onQueryChange, onTagFilterChange, onTagAdd, onSortChange, onLayoutChange } = useSearchQuery({}); const { query, onQueryChange, onTagFilterChange, onTagAdd, onSortChange, onLayoutChange } = useSearchQuery({});
const { results, loading, onToggleSection, onKeyDown, showPreviews, onShowPreviewsChange } = useDashboardSearch( const { results, loading, onToggleSection, onKeyDown, showPreviews, setShowPreviews } = useDashboardSearch(
query, query,
onCloseSearch onCloseSearch
); );
@ -34,13 +35,18 @@ export const DashboardSearch: FC<Props> = memo(({ onCloseSearch }) => {
<ActionRow <ActionRow
{...{ {...{
onLayoutChange, onLayoutChange,
onShowPreviewsChange: (ev) => onShowPreviewsChange(ev.target.checked), setShowPreviews,
onSortChange, onSortChange,
onTagFilterChange, onTagFilterChange,
query, query,
showPreviews, showPreviews,
}} }}
/> />
<PreviewsSystemRequirements
bottomSpacing={3}
showPreviews={showPreviews}
onRemove={() => setShowPreviews(false)}
/>
<CustomScrollbar> <CustomScrollbar>
<SearchResults <SearchResults
results={results} results={results}

View File

@ -62,7 +62,7 @@ export const ManageDashboards: FC<Props> = memo(({ folder }) => {
onMoveItems, onMoveItems,
noFolders, noFolders,
showPreviews, showPreviews,
onShowPreviewsChange, setShowPreviews,
} = useManageDashboards(query, {}, folder); } = useManageDashboards(query, {}, folder);
const onMoveTo = () => { const onMoveTo = () => {
@ -108,7 +108,7 @@ export const ManageDashboards: FC<Props> = memo(({ folder }) => {
canMove={hasEditPermissionInFolders && canMove} canMove={hasEditPermissionInFolders && canMove}
deleteItem={onItemDelete} deleteItem={onItemDelete}
moveTo={onMoveTo} moveTo={onMoveTo}
onShowPreviewsChange={(ev) => onShowPreviewsChange(ev.target.checked)} setShowPreviews={setShowPreviews}
onToggleAllChecked={onToggleAllChecked} onToggleAllChecked={onToggleAllChecked}
onStarredFilterChange={onStarredFilterChange} onStarredFilterChange={onStarredFilterChange}
onSortChange={onSortChange} onSortChange={onSortChange}

View File

@ -0,0 +1,93 @@
import { Alert, useStyles2 } from '@grafana/ui';
import React from 'react';
import { config } from '@grafana/runtime/src';
import { css } from '@emotion/css';
export interface Props {
showPreviews?: boolean;
/** On click handler for alert button, mostly used for dismissing the alert */
onRemove?: (event: React.MouseEvent) => void;
topSpacing?: number;
bottomSpacing?: number;
}
const MessageLink = ({ text }: { text: string }) => (
<a
href="https://grafana.com/grafana/plugins/grafana-image-renderer"
target="_blank"
rel="noopener noreferrer"
className="external-link"
>
{text}
</a>
);
const Message = ({ requiredImageRendererPluginVersion }: { requiredImageRendererPluginVersion?: string }) => {
if (requiredImageRendererPluginVersion) {
return (
<>
You must update the <MessageLink text="Grafana image renderer plugin" /> to version{' '}
{requiredImageRendererPluginVersion} to enable dashboard previews. Please contact your Grafana administrator to
update the plugin.
</>
);
}
return (
<>
You must install the <MessageLink text="Grafana image renderer plugin" /> to enable dashboard previews. Please
contact your Grafana administrator to install the plugin.
</>
);
};
export const PreviewsSystemRequirements = ({ showPreviews, onRemove, topSpacing, bottomSpacing }: Props) => {
const styles = useStyles2(getStyles);
const previewsEnabled = config.featureToggles.dashboardPreviews;
const rendererAvailable = config.rendererAvailable;
const {
systemRequirements: { met: systemRequirementsMet, requiredImageRendererPluginVersion },
thumbnailsExist,
} = config.dashboardPreviews;
const arePreviewsEnabled = previewsEnabled && showPreviews;
const areRequirementsMet = (rendererAvailable && systemRequirementsMet) || thumbnailsExist;
const shouldDisplayRequirements = arePreviewsEnabled && !areRequirementsMet;
const title = requiredImageRendererPluginVersion
? 'Image renderer plugin needs to be updated'
: 'Image renderer plugin not installed';
return (
<>
{shouldDisplayRequirements && (
<div className={styles.wrapper}>
<Alert
className={styles.alert}
topSpacing={topSpacing}
bottomSpacing={bottomSpacing}
severity="info"
title={title}
onRemove={onRemove}
>
<Message requiredImageRendererPluginVersion={requiredImageRendererPluginVersion} />
</Alert>
</div>
)}
</>
);
};
const getStyles = () => {
return {
wrapper: css`
display: flex;
justify-content: center;
`,
alert: css`
max-width: 800px;
`,
};
};

View File

@ -37,7 +37,7 @@ const setup = (propOverrides?: Partial<Props>) => {
onLayoutChange: noop, onLayoutChange: noop,
query: searchQuery, query: searchQuery,
onSortChange: noop, onSortChange: noop,
onShowPreviewsChange: noop, setShowPreviews: noop,
editable: true, editable: true,
}; };

View File

@ -1,9 +1,10 @@
import React, { FC, ChangeEvent, FormEvent } from 'react'; import React, { FC, FormEvent } from 'react';
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { Button, Checkbox, stylesFactory, useTheme, HorizontalGroup } from '@grafana/ui'; import { Button, Checkbox, stylesFactory, useTheme, HorizontalGroup } from '@grafana/ui';
import { GrafanaTheme, SelectableValue } from '@grafana/data'; import { GrafanaTheme, SelectableValue } from '@grafana/data';
import { DashboardQuery, SearchLayout } from '../types'; import { DashboardQuery, SearchLayout } from '../types';
import { ActionRow } from './ActionRow'; import { ActionRow } from './ActionRow';
import { PreviewsSystemRequirements } from './PreviewsSystemRequirements';
export interface Props { export interface Props {
allChecked?: boolean; allChecked?: boolean;
@ -13,7 +14,7 @@ export interface Props {
hideLayout?: boolean; hideLayout?: boolean;
moveTo: () => void; moveTo: () => void;
onLayoutChange: (layout: SearchLayout) => void; onLayoutChange: (layout: SearchLayout) => void;
onShowPreviewsChange: (event: ChangeEvent<HTMLInputElement>) => void; setShowPreviews: (newValue: boolean) => void;
onSortChange: (value: SelectableValue) => void; onSortChange: (value: SelectableValue) => void;
onStarredFilterChange: (event: FormEvent<HTMLInputElement>) => void; onStarredFilterChange: (event: FormEvent<HTMLInputElement>) => void;
onTagFilterChange: (tags: string[]) => void; onTagFilterChange: (tags: string[]) => void;
@ -31,7 +32,7 @@ export const SearchResultsFilter: FC<Props> = ({
hideLayout, hideLayout,
moveTo, moveTo,
onLayoutChange, onLayoutChange,
onShowPreviewsChange, setShowPreviews,
onSortChange, onSortChange,
onStarredFilterChange, onStarredFilterChange,
onTagFilterChange, onTagFilterChange,
@ -46,35 +47,43 @@ export const SearchResultsFilter: FC<Props> = ({
return ( return (
<div className={styles.wrapper}> <div className={styles.wrapper}>
{editable && ( <div className={styles.rowWrapper}>
<div className={styles.checkboxWrapper}> {editable && (
<Checkbox aria-label="Select all" value={allChecked} onChange={onToggleAllChecked} /> <div className={styles.checkboxWrapper}>
</div> <Checkbox aria-label="Select all" value={allChecked} onChange={onToggleAllChecked} />
)} </div>
{showActions ? ( )}
<HorizontalGroup spacing="md"> {showActions ? (
<Button disabled={!canMove} onClick={moveTo} icon="exchange-alt" variant="secondary"> <HorizontalGroup spacing="md">
Move <Button disabled={!canMove} onClick={moveTo} icon="exchange-alt" variant="secondary">
</Button> Move
<Button disabled={!canDelete} onClick={deleteItem} icon="trash-alt" variant="destructive"> </Button>
Delete <Button disabled={!canDelete} onClick={deleteItem} icon="trash-alt" variant="destructive">
</Button> Delete
</HorizontalGroup> </Button>
) : ( </HorizontalGroup>
<ActionRow ) : (
{...{ <ActionRow
hideLayout, {...{
onLayoutChange, hideLayout,
onShowPreviewsChange, onLayoutChange,
onSortChange, setShowPreviews,
onStarredFilterChange, onSortChange,
onTagFilterChange, onStarredFilterChange,
query, onTagFilterChange,
showPreviews, query,
}} showPreviews,
showStarredFilter }}
/> showStarredFilter
)} />
)}
</div>
<PreviewsSystemRequirements
topSpacing={2}
bottomSpacing={3}
showPreviews={showPreviews}
onRemove={() => setShowPreviews(false)}
/>
</div> </div>
); );
}; };
@ -83,6 +92,10 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
const { sm, md } = theme.spacing; const { sm, md } = theme.spacing;
return { return {
wrapper: css` wrapper: css`
display: flex;
flex-direction: column;
`,
rowWrapper: css`
height: ${theme.height.md}px; height: ${theme.height.md}px;
display: flex; display: flex;
justify-content: flex-start; justify-content: flex-start;

View File

@ -12,7 +12,7 @@ import { useDebounce } from 'react-use';
export const useDashboardSearch = (query: DashboardQuery, onCloseSearch: () => void) => { export const useDashboardSearch = (query: DashboardQuery, onCloseSearch: () => void) => {
const reducer = useReducer(searchReducer, dashboardsSearchState); const reducer = useReducer(searchReducer, dashboardsSearchState);
const { showPreviews, onShowPreviewsChange, previewFeatureEnabled } = useShowDashboardPreviews(); const { showPreviews, setShowPreviews, previewFeatureEnabled } = useShowDashboardPreviews();
const { const {
state: { results, loading }, state: { results, loading },
onToggleSection, onToggleSection,
@ -72,6 +72,6 @@ export const useDashboardSearch = (query: DashboardQuery, onCloseSearch: () => v
onToggleSection, onToggleSection,
onKeyDown, onKeyDown,
showPreviews, showPreviews,
onShowPreviewsChange, setShowPreviews,
}; };
}; };

View File

@ -47,7 +47,7 @@ export const useManageDashboards = (
...state, ...state,
}); });
const { showPreviews, onShowPreviewsChange, previewFeatureEnabled } = useShowDashboardPreviews(); const { showPreviews, setShowPreviews, previewFeatureEnabled } = useShowDashboardPreviews();
useDebounce( useDebounce(
() => { () => {
reportDashboardListViewed('manage_dashboards', showPreviews, previewFeatureEnabled, { reportDashboardListViewed('manage_dashboards', showPreviews, previewFeatureEnabled, {
@ -123,6 +123,6 @@ export const useManageDashboards = (
onMoveItems, onMoveItems,
noFolders, noFolders,
showPreviews, showPreviews,
onShowPreviewsChange, setShowPreviews,
}; };
}; };

View File

@ -5,9 +5,6 @@ import { useLocalStorage } from 'react-use';
export const useShowDashboardPreviews = () => { export const useShowDashboardPreviews = () => {
const previewFeatureEnabled = Boolean(config.featureToggles.dashboardPreviews); const previewFeatureEnabled = Boolean(config.featureToggles.dashboardPreviews);
const [showPreviews, setShowPreviews] = useLocalStorage<boolean>(PREVIEWS_LOCAL_STORAGE_KEY, previewFeatureEnabled); const [showPreviews, setShowPreviews] = useLocalStorage<boolean>(PREVIEWS_LOCAL_STORAGE_KEY, previewFeatureEnabled);
const onShowPreviewsChange = (showPreviews: boolean) => {
setShowPreviews(showPreviews);
};
return { showPreviews: Boolean(showPreviews && previewFeatureEnabled), previewFeatureEnabled, onShowPreviewsChange }; return { showPreviews: Boolean(showPreviews && previewFeatureEnabled), previewFeatureEnabled, setShowPreviews };
}; };