mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
4a2d86750e
commit
d9b4aa07f7
@ -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;
|
||||
`,
|
||||
};
|
||||
};
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
@ -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};
|
||||
`,
|
||||
});
|
@ -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()}
|
||||
</>
|
||||
);
|
||||
|
@ -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 ?? '',
|
||||
|
@ -102,7 +102,6 @@ export type OnToggleChecked = (item: DashboardViewItem) => void;
|
||||
export enum SearchLayout {
|
||||
List = 'list',
|
||||
Folders = 'folders',
|
||||
Grid = 'grid', // preview
|
||||
}
|
||||
|
||||
export interface SearchQueryParams {
|
||||
|
@ -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>
|
||||
)
|
||||
)}
|
||||
|
@ -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',
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user