Previews: remove dashboard previews UI (#66146)

* remove dashboard previews ui

* remove dashboard previews ui

* remove layout prop

* remove layout prop
This commit is contained in:
Artur Wierzbicki 2023-04-07 19:15:46 +04:00 committed by GitHub
parent 4a2d86750e
commit d9b4aa07f7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 7 additions and 368 deletions

View File

@ -1,94 +0,0 @@
import { css } from '@emotion/css';
import React from 'react';
import { config } from '@grafana/runtime/src';
import { Alert, useStyles2 } from '@grafana/ui';
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

@ -3,7 +3,7 @@ import React, { FormEvent } from 'react';
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
import { config } from '@grafana/runtime';
import { HorizontalGroup, RadioButtonGroup, useStyles2, Checkbox, Button } from '@grafana/ui';
import { Button, Checkbox, HorizontalGroup, RadioButtonGroup, useStyles2 } from '@grafana/ui';
import { SortPicker } from 'app/core/components/Select/SortPicker';
import { TagFilter, TermCount } from 'app/core/components/TagFilter/TagFilter';
import { t, Trans } from 'app/core/internationalization';
@ -11,20 +11,10 @@ import { t, Trans } from 'app/core/internationalization';
import { SearchLayout, SearchState } from '../../types';
function getLayoutOptions() {
const layoutOptions = [
return [
{ value: SearchLayout.Folders, icon: 'folder', ariaLabel: t('search.actions.view-as-folders', 'View by folders') },
{ value: SearchLayout.List, icon: 'list-ul', ariaLabel: t('search.actions.view-as-list', 'View as list') },
];
if (config.featureToggles.dashboardPreviews) {
layoutOptions.push({
value: SearchLayout.Grid,
icon: 'apps',
ariaLabel: t('search.actions.view-as-grid', 'Grid view'),
});
}
return layoutOptions;
}
interface Props {
@ -54,9 +44,6 @@ export function getValidQueryLayout(q: SearchState): SearchLayout {
}
}
if (layout === SearchLayout.Grid && !config.featureToggles.dashboardPreviews) {
return SearchLayout.List;
}
return layout;
}

View File

@ -1,66 +0,0 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import React, { KeyboardEvent } from 'react';
import { Observable } from 'rxjs';
import { ArrayVector, DataFrame, DataFrameView, FieldType } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { DashboardQueryResult, QueryResponse } from '../../service';
import { DashboardSearchItemType } from '../../types';
import { SearchResultsGrid } from './SearchResultsGrid';
describe('SearchResultsGrid', () => {
const dashboardsData: DataFrame = {
fields: [
{
name: 'kind',
type: FieldType.string,
config: {},
values: new ArrayVector([DashboardSearchItemType.DashDB]),
},
{ name: 'name', type: FieldType.string, config: {}, values: new ArrayVector(['My dashboard 1', 'dash2']) },
{ name: 'uid', type: FieldType.string, config: {}, values: new ArrayVector(['my-dashboard-1', 'dash-2']) },
{ name: 'url', type: FieldType.string, config: {}, values: new ArrayVector(['/my-dashbaord-1', '/dash-2']) },
],
length: 2,
};
const mockSearchResult: QueryResponse = {
isItemLoaded: jest.fn(),
loadMoreItems: jest.fn(),
totalRows: dashboardsData.length,
view: new DataFrameView<DashboardQueryResult>(dashboardsData),
};
const baseProps = {
response: mockSearchResult,
width: 800,
height: 600,
clearSelection: jest.fn(),
onTagSelected: jest.fn(),
keyboardEvents: new Observable<KeyboardEvent<Element>>(),
};
it('should render grid of dashboards', () => {
render(<SearchResultsGrid {...baseProps} />);
expect(screen.getByTestId(selectors.components.Search.dashboardCard('My dashboard 1'))).toBeInTheDocument();
expect(screen.getByTestId(selectors.components.Search.dashboardCard('dash2'))).toBeInTheDocument();
});
it('should not render checkboxes for non-editable results', async () => {
render(<SearchResultsGrid {...baseProps} />);
expect(screen.queryByRole('checkbox')).toBeNull();
await waitFor(() => expect(screen.queryAllByRole('checkbox')).toHaveLength(0));
});
it('should render checkboxes for editable results ', async () => {
const mockSelectionToggle = jest.fn();
const mockSelection = jest.fn();
render(<SearchResultsGrid {...baseProps} selection={mockSelection} selectionToggle={mockSelectionToggle} />);
await waitFor(() => expect(screen.queryAllByRole('checkbox')).toHaveLength(2));
fireEvent.click(await screen.findByRole('checkbox', { name: /Select dashboard dash2/i }));
expect(mockSelectionToggle).toHaveBeenCalledWith('dashboard', 'dash-2');
expect(mockSelectionToggle).toHaveBeenCalledTimes(1);
});
});

View File

@ -1,134 +0,0 @@
import { css } from '@emotion/css';
import React from 'react';
import { FixedSizeGrid } from 'react-window';
import InfiniteLoader from 'react-window-infinite-loader';
import { GrafanaTheme2 } from '@grafana/data';
import { config } from '@grafana/runtime';
import { useStyles2 } from '@grafana/ui';
import { SearchCard } from '../../components/SearchCard';
import { useSearchKeyboardNavigation } from '../../hooks/useSearchKeyboardSelection';
import { queryResultToViewItem } from '../../service/utils';
import { DashboardViewItem } from '../../types';
import { SearchResultsProps } from './SearchResultsTable';
export const SearchResultsGrid = ({
response,
width,
height,
selection,
selectionToggle,
onTagSelected,
onClickItem,
keyboardEvents,
}: SearchResultsProps) => {
const styles = useStyles2(getStyles);
// Hacked to reuse existing SearchCard (and old DashboardSectionItem)
const itemProps = {
editable: selection != null,
onToggleChecked: (item: DashboardViewItem) => {
if (selectionToggle) {
selectionToggle(item.kind, item.uid);
}
},
onTagSelected,
onClick: onClickItem,
};
const itemCount = response.totalRows ?? response.view.length;
const view = response.view;
const numColumns = Math.ceil(width / 320);
const cellWidth = width / numColumns;
const cellHeight = (cellWidth - 64) * 0.75 + 56 + 8;
const numRows = Math.ceil(itemCount / numColumns);
const highlightIndex = useSearchKeyboardNavigation(keyboardEvents, numColumns, response);
return (
<InfiniteLoader isItemLoaded={response.isItemLoaded} itemCount={itemCount} loadMoreItems={response.loadMoreItems}>
{({ onItemsRendered, ref }) => (
<FixedSizeGrid
ref={ref}
onItemsRendered={(v) => {
onItemsRendered({
visibleStartIndex: v.visibleRowStartIndex * numColumns,
visibleStopIndex: v.visibleRowStopIndex * numColumns,
overscanStartIndex: v.overscanRowStartIndex * numColumns,
overscanStopIndex: v.overscanColumnStopIndex * numColumns,
});
}}
columnCount={numColumns}
columnWidth={cellWidth}
rowCount={numRows}
rowHeight={cellHeight}
className={styles.wrapper}
innerElementType="ul"
height={height}
width={width - 2}
>
{({ columnIndex, rowIndex, style }) => {
const index = rowIndex * numColumns + columnIndex;
if (index >= view.length) {
return null;
}
const item = view.get(index);
const kind = item.kind ?? 'dashboard';
const facade = queryResultToViewItem(item, view);
if (kind === 'panel') {
const type = item.panel_type;
facade.icon = 'public/img/icons/unicons/graph-bar.svg';
if (type) {
const info = config.panels[type];
if (info?.name) {
const v = info.info?.logos.small;
if (v && v.endsWith('.svg')) {
facade.icon = v;
}
}
}
}
let className = styles.virtualizedGridItemWrapper;
if (rowIndex === highlightIndex.y && columnIndex === highlightIndex.x) {
className += ' ' + styles.selectedItem;
}
// The wrapper div is needed as the inner SearchItem has margin-bottom spacing
// And without this wrapper there is no room for that margin
return item ? (
<li style={style} className={className}>
<SearchCard
key={item.uid}
{...itemProps}
item={facade}
isSelected={selection ? selection(facade.kind, facade.uid) : false}
/>
</li>
) : null;
}}
</FixedSizeGrid>
)}
</InfiniteLoader>
);
};
const getStyles = (theme: GrafanaTheme2) => ({
virtualizedGridItemWrapper: css`
padding: 4px;
`,
wrapper: css`
display: flex;
flex-direction: column;
> ul {
list-style: none;
}
`,
selectedItem: css`
box-shadow: inset 1px 1px 3px 3px ${theme.colors.primary.border};
`,
});

View File

@ -10,7 +10,6 @@ import { useStyles2, Spinner, Button } from '@grafana/ui';
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
import { FolderDTO } from 'app/types';
import { PreviewsSystemRequirements } from '../../components/PreviewsSystemRequirements';
import { getGrafanaSearcher } from '../../service';
import { getSearchStateManager } from '../../state/SearchStateManager';
import { SearchLayout, DashboardViewItem } from '../../types';
@ -21,7 +20,6 @@ import { FolderSection } from './FolderSection';
import { ManageActions } from './ManageActions';
import { RootFolderView } from './RootFolderView';
import { SearchResultsCards } from './SearchResultsCards';
import { SearchResultsGrid } from './SearchResultsGrid';
import { SearchResultsTable, SearchResultsProps } from './SearchResultsTable';
export type SearchViewProps = {
@ -133,10 +131,6 @@ export const SearchView = ({ showManage, folderDTO, hidePseudoFolders, keyboardE
onClickItem: stateManager.onSearchItemClicked,
};
if (layout === SearchLayout.Grid) {
return <SearchResultsGrid {...props} />;
}
if (width < 800) {
return <SearchResultsCards {...props} />;
}
@ -193,13 +187,6 @@ export const SearchView = ({ showManage, folderDTO, hidePseudoFolders, keyboardE
/>
)}
{layout === SearchLayout.Grid && (
<PreviewsSystemRequirements
bottomSpacing={3}
showPreviews={true}
onRemove={() => stateManager.onLayoutChange(SearchLayout.List)}
/>
)}
{renderResults()}
</>
);

View File

@ -1,4 +1,4 @@
import { config, reportInteraction } from '@grafana/runtime';
import { reportInteraction } from '@grafana/runtime';
import { InspectTab } from 'app/features/inspector/types';
import { EventTrackingNamespace, SearchLayout } from '../types';
@ -40,12 +40,7 @@ export const reportPanelInspectInteraction = (
};
const getQuerySearchContext = (query: QueryProps) => {
const showPreviews = query.layout === SearchLayout.Grid;
const previewsEnabled = Boolean(config.featureToggles.panelTitleSearch);
const previews = previewsEnabled ? (showPreviews ? 'on' : 'off') : 'feature_disabled';
return {
previews,
layout: query.layout,
starredFilter: query.starred ?? false,
sort: query.sortValue ?? '',

View File

@ -102,7 +102,6 @@ export type OnToggleChecked = (item: DashboardViewItem) => void;
export enum SearchLayout {
List = 'list',
Folders = 'folders',
Grid = 'grid', // preview
}
export interface SearchQueryParams {

View File

@ -20,12 +20,11 @@ import { getBackendSrv } from 'app/core/services/backend_srv';
import impressionSrv from 'app/core/services/impression_srv';
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { SearchCard } from 'app/features/search/components/SearchCard';
import { DashboardSearchItem } from 'app/features/search/types';
import { getVariablesUrlParams } from 'app/features/variables/getAllVariableValuesForUrl';
import { useDispatch } from 'app/types';
import { PanelLayout, PanelOptions } from './panelcfg.gen';
import { PanelOptions } from './panelcfg.gen';
import { getStyles } from './styles';
type Dashboard = DashboardSearchItem & { id?: number; isSearchResult?: boolean; isRecent?: boolean };
@ -130,7 +129,7 @@ export function DashList(props: PanelProps<PanelOptions>) {
];
}, [dashboards]);
const { showStarred, showRecentlyViewed, showHeadings, showSearch, layout } = props.options;
const { showStarred, showRecentlyViewed, showHeadings, showSearch } = props.options;
const dashboardGroups: DashboardGroup[] = [
{
@ -198,16 +197,6 @@ export function DashList(props: PanelProps<PanelOptions>) {
</ul>
);
const renderPreviews = (dashboards: Dashboard[]) => (
<ul className={css.gridContainer}>
{dashboards.map((dash) => (
<li key={dash.uid}>
<SearchCard item={{ ...dash, kind: 'dashboard' }} />
</li>
))}
</ul>
);
return (
<CustomScrollbar autoHeightMin="100%" autoHeightMax="100%">
{dashboardGroups.map(
@ -215,7 +204,7 @@ export function DashList(props: PanelProps<PanelOptions>) {
show && (
<div className={css.dashlistSection} key={`dash-group-${i}`}>
{showHeadings && <h6 className={css.dashlistSectionHeader}>{header}</h6>}
{layout === PanelLayout.Previews ? renderPreviews(dashboards) : renderList(dashboards)}
{renderList(dashboards)}
</div>
)
)}

View File

@ -1,7 +1,6 @@
import React from 'react';
import { PanelModel, PanelPlugin } from '@grafana/data';
import { config } from '@grafana/runtime';
import { TagsInput } from '@grafana/ui';
import {
@ -11,24 +10,10 @@ import {
} from '../../../core/components/Select/ReadonlyFolderPicker/ReadonlyFolderPicker';
import { DashList } from './DashList';
import { defaultPanelOptions, PanelLayout, PanelOptions } from './panelcfg.gen';
import { defaultPanelOptions, PanelOptions } from './panelcfg.gen';
export const plugin = new PanelPlugin<PanelOptions>(DashList)
.setPanelOptions((builder) => {
if (config.featureToggles.dashboardPreviews) {
builder.addRadio({
path: 'layout',
name: 'Layout',
defaultValue: PanelLayout.List,
settings: {
options: [
{ value: PanelLayout.List, label: 'List' },
{ value: PanelLayout.Previews, label: 'Preview' },
],
},
});
}
builder
.addBooleanSwitch({
path: 'keepTime',

View File

@ -22,9 +22,7 @@ composableKinds: PanelCfg: {
{
schemas: [
{
PanelLayout: "list" | "previews" @cuetsy(kind="enum")
PanelOptions: {
layout?: PanelLayout | *"list"
keepTime: bool | *false
includeVars: bool | *false
showStarred: bool | *true

View File

@ -10,16 +10,10 @@
export const PanelCfgModelVersion = Object.freeze([0, 0]);
export enum PanelLayout {
List = 'list',
Previews = 'previews',
}
export interface PanelOptions {
folderId?: number;
includeVars: boolean;
keepTime: boolean;
layout?: PanelLayout;
maxItems: number;
query: string;
showHeadings: boolean;
@ -32,7 +26,6 @@ export interface PanelOptions {
export const defaultPanelOptions: Partial<PanelOptions> = {
includeVars: false,
keepTime: false,
layout: PanelLayout.List,
maxItems: 10,
query: '',
showHeadings: true,