mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Search/virtualize list (#23710)
* Search: Add FixedSizeList for result items * Search: Move SectionHeader to a separate file * Search: Add useListHeight hook * Search: Fix horizontal scrollbar * Search: Remove custom scrollbar * Search: Do not fetch dashboard folder on route change * Search: Update tests * Search: Remove extra checkbox renders * Search: Move wrapper ref outside search results * Search: Fix param type * Search: Fix merge conflicts * Search: Virtualize dashboard list * Search: Update layout * Search: Pass wrapper to search results * Search: Update dashboard redirect * Search: Remove unused css * Search: Revert config * Search: Use AutoSizer * Search: Remove redundant appEvents call * Search: Use List layout in folder view
This commit is contained in:
parent
cf23f15a08
commit
fb8a555f19
@ -124,7 +124,7 @@ export class CustomScrollbar extends Component<Props> {
|
|||||||
autoHideTimeout={autoHideTimeout}
|
autoHideTimeout={autoHideTimeout}
|
||||||
hideTracksWhenNotNeeded={hideTracksWhenNotNeeded}
|
hideTracksWhenNotNeeded={hideTracksWhenNotNeeded}
|
||||||
// These autoHeightMin & autoHeightMax options affect firefox and chrome differently.
|
// These autoHeightMin & autoHeightMax options affect firefox and chrome differently.
|
||||||
// Before these where set to inhert but that caused problems with cut of legends in firefox
|
// Before these where set to inherit but that caused problems with cut of legends in firefox
|
||||||
autoHeightMax={autoHeightMax}
|
autoHeightMax={autoHeightMax}
|
||||||
autoHeightMin={autoHeightMin}
|
autoHeightMin={autoHeightMin}
|
||||||
renderTrackHorizontal={this.renderTrackHorizontal}
|
renderTrackHorizontal={this.renderTrackHorizontal}
|
||||||
|
@ -4,3 +4,4 @@ export const getRouteParamsId = (state: LocationState) => state.routeParams.id;
|
|||||||
export const getRouteParamsPage = (state: LocationState) => state.routeParams.page;
|
export const getRouteParamsPage = (state: LocationState) => state.routeParams.page;
|
||||||
export const getRouteParams = (state: LocationState) => state.routeParams;
|
export const getRouteParams = (state: LocationState) => state.routeParams;
|
||||||
export const getLocationQuery = (state: LocationState) => state.query;
|
export const getLocationQuery = (state: LocationState) => state.query;
|
||||||
|
export const getUrl = (state: LocationState) => state.url;
|
||||||
|
@ -5,7 +5,7 @@ import { NavModel, locationUtil } from '@grafana/data';
|
|||||||
import { getLocationSrv } from '@grafana/runtime';
|
import { getLocationSrv } from '@grafana/runtime';
|
||||||
import { StoreState } from 'app/types';
|
import { StoreState } from 'app/types';
|
||||||
import { getNavModel } from 'app/core/selectors/navModel';
|
import { getNavModel } from 'app/core/selectors/navModel';
|
||||||
import { getRouteParams } from 'app/core/selectors/location';
|
import { getRouteParams, getUrl } from 'app/core/selectors/location';
|
||||||
import Page from 'app/core/components/Page/Page';
|
import Page from 'app/core/components/Page/Page';
|
||||||
import { loadFolderPage } from '../loaders';
|
import { loadFolderPage } from '../loaders';
|
||||||
import { ManageDashboards } from './ManageDashboards';
|
import { ManageDashboards } from './ManageDashboards';
|
||||||
@ -13,18 +13,19 @@ import { ManageDashboards } from './ManageDashboards';
|
|||||||
interface Props {
|
interface Props {
|
||||||
navModel: NavModel;
|
navModel: NavModel;
|
||||||
uid?: string;
|
uid?: string;
|
||||||
|
url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DashboardListPage: FC<Props> = memo(({ navModel, uid }) => {
|
export const DashboardListPage: FC<Props> = memo(({ navModel, uid, url }) => {
|
||||||
const { loading, value } = useAsync(() => {
|
const { loading, value } = useAsync(() => {
|
||||||
if (!uid) {
|
if (!uid || !url.startsWith('/dashboards')) {
|
||||||
return Promise.resolve({ pageNavModel: navModel });
|
return Promise.resolve({ pageNavModel: navModel });
|
||||||
}
|
}
|
||||||
return loadFolderPage(uid, 'manage-folder-dashboards').then(({ folder, model }) => {
|
return loadFolderPage(uid!, 'manage-folder-dashboards').then(({ folder, model }) => {
|
||||||
const url = locationUtil.stripBaseFromUrl(folder.url);
|
const path = locationUtil.stripBaseFromUrl(folder.url);
|
||||||
|
|
||||||
if (url !== location.pathname) {
|
if (path !== location.pathname) {
|
||||||
getLocationSrv().update({ path: url });
|
getLocationSrv().update({ path });
|
||||||
}
|
}
|
||||||
|
|
||||||
return { id: folder.id, pageNavModel: { ...navModel, ...model } };
|
return { id: folder.id, pageNavModel: { ...navModel, ...model } };
|
||||||
@ -40,9 +41,12 @@ export const DashboardListPage: FC<Props> = memo(({ navModel, uid }) => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapStateToProps: MapStateToProps<Props, {}, StoreState> = state => ({
|
const mapStateToProps: MapStateToProps<Props, {}, StoreState> = state => {
|
||||||
navModel: getNavModel(state.navIndex, 'manage-dashboards'),
|
return {
|
||||||
uid: getRouteParams(state.location).uid as string | undefined,
|
navModel: getNavModel(state.navIndex, 'manage-dashboards'),
|
||||||
});
|
uid: getRouteParams(state.location).uid as string | undefined,
|
||||||
|
url: getUrl(state.location),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export default connect(mapStateToProps)(DashboardListPage);
|
export default connect(mapStateToProps)(DashboardListPage);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { FC } from 'react';
|
import React, { FC, memo } from 'react';
|
||||||
import { css } from 'emotion';
|
import { css } from 'emotion';
|
||||||
import { useTheme, CustomScrollbar, stylesFactory, Button } from '@grafana/ui';
|
import { useTheme, CustomScrollbar, stylesFactory, Button } from '@grafana/ui';
|
||||||
import { GrafanaTheme } from '@grafana/data';
|
import { GrafanaTheme } from '@grafana/data';
|
||||||
@ -14,7 +14,7 @@ export interface Props {
|
|||||||
folder?: string;
|
folder?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DashboardSearch: FC<Props> = ({ onCloseSearch, folder }) => {
|
export const DashboardSearch: FC<Props> = memo(({ onCloseSearch, folder }) => {
|
||||||
const payload = folder ? { query: `folder:${folder}` } : {};
|
const payload = folder ? { query: `folder:${folder}` } : {};
|
||||||
const { query, onQueryChange, onTagFilterChange, onTagAdd, onSortChange } = useSearchQuery(payload);
|
const { query, onQueryChange, onTagFilterChange, onTagAdd, onSortChange } = useSearchQuery(payload);
|
||||||
const { results, loading, onToggleSection, onKeyDown } = useDashboardSearch(query, onCloseSearch);
|
const { results, loading, onToggleSection, onKeyDown } = useDashboardSearch(query, onCloseSearch);
|
||||||
@ -74,7 +74,7 @@ export const DashboardSearch: FC<Props> = ({ onCloseSearch, folder }) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||||
return {
|
return {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { FC, useState, memo } from 'react';
|
import React, { FC, memo, useState } from 'react';
|
||||||
import { css } from 'emotion';
|
import { css } from 'emotion';
|
||||||
import { Icon, TagList, HorizontalGroup, stylesFactory, useTheme } from '@grafana/ui';
|
import { HorizontalGroup, Icon, stylesFactory, TagList, useTheme } from '@grafana/ui';
|
||||||
import { GrafanaTheme } from '@grafana/data';
|
import { GrafanaTheme } from '@grafana/data';
|
||||||
import { contextSrv } from 'app/core/services/context_srv';
|
import { contextSrv } from 'app/core/services/context_srv';
|
||||||
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
|
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
|
||||||
@ -56,7 +56,8 @@ export const ManageDashboards: FC<Props> = memo(({ folderId, folderUid }) => {
|
|||||||
onMoveItems,
|
onMoveItems,
|
||||||
} = useManageDashboards(query, { hasEditPermissionInFolders: contextSrv.hasEditPermissionInFolders }, folderUid);
|
} = useManageDashboards(query, { hasEditPermissionInFolders: contextSrv.hasEditPermissionInFolders }, folderUid);
|
||||||
|
|
||||||
const { layout, setLayout } = useSearchLayout(query);
|
const defaultLayout = folderId ? SearchLayout.List : SearchLayout.Folders;
|
||||||
|
const { layout, setLayout } = useSearchLayout(query, defaultLayout);
|
||||||
|
|
||||||
const onMoveTo = () => {
|
const onMoveTo = () => {
|
||||||
setIsMoveModalOpen(true);
|
setIsMoveModalOpen(true);
|
||||||
@ -89,59 +90,61 @@ export const ManageDashboards: FC<Props> = memo(({ folderId, folderUid }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="dashboard-list">
|
<div className={styles.container}>
|
||||||
<HorizontalGroup justify="space-between">
|
<div>
|
||||||
<SearchField query={query} onChange={onQueryChange} className={styles.searchField} />
|
<HorizontalGroup justify="space-between">
|
||||||
<DashboardActions isEditor={isEditor} canEdit={hasEditPermissionInFolders || canSave} folderId={folderId} />
|
<SearchField query={query} onChange={onQueryChange} className={styles.searchField} />
|
||||||
</HorizontalGroup>
|
<DashboardActions isEditor={isEditor} canEdit={hasEditPermissionInFolders || canSave} folderId={folderId} />
|
||||||
|
|
||||||
{hasFilters && (
|
|
||||||
<HorizontalGroup>
|
|
||||||
<div className="gf-form-inline">
|
|
||||||
{query.tag.length > 0 && (
|
|
||||||
<div className="gf-form">
|
|
||||||
<label className="gf-form-label width-4">Tags</label>
|
|
||||||
<TagList tags={query.tag} onClick={onTagRemove} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{query.starred && (
|
|
||||||
<div className="gf-form">
|
|
||||||
<label className="gf-form-label">
|
|
||||||
<a className="pointer" onClick={onRemoveStarred}>
|
|
||||||
<Icon name="check" />
|
|
||||||
Starred
|
|
||||||
</a>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{query.sort && (
|
|
||||||
<div className="gf-form">
|
|
||||||
<label className="gf-form-label">
|
|
||||||
<a className="pointer" onClick={() => onSortChange(null)}>
|
|
||||||
Sort: {query.sort.label}
|
|
||||||
</a>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="gf-form">
|
|
||||||
<label className="gf-form-label">
|
|
||||||
<a
|
|
||||||
className="pointer"
|
|
||||||
onClick={() => {
|
|
||||||
onClearFilters();
|
|
||||||
setLayout(SearchLayout.Folders);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon name="times" />
|
|
||||||
Clear
|
|
||||||
</a>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</HorizontalGroup>
|
</HorizontalGroup>
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="search-results">
|
{hasFilters && (
|
||||||
|
<HorizontalGroup>
|
||||||
|
<div className="gf-form-inline">
|
||||||
|
{query.tag.length > 0 && (
|
||||||
|
<div className="gf-form">
|
||||||
|
<label className="gf-form-label width-4">Tags</label>
|
||||||
|
<TagList tags={query.tag} onClick={onTagRemove} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{query.starred && (
|
||||||
|
<div className="gf-form">
|
||||||
|
<label className="gf-form-label">
|
||||||
|
<a className="pointer" onClick={onRemoveStarred}>
|
||||||
|
<Icon name="check" />
|
||||||
|
Starred
|
||||||
|
</a>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{query.sort && (
|
||||||
|
<div className="gf-form">
|
||||||
|
<label className="gf-form-label">
|
||||||
|
<a className="pointer" onClick={() => onSortChange(null)}>
|
||||||
|
Sort: {query.sort.label}
|
||||||
|
</a>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="gf-form">
|
||||||
|
<label className="gf-form-label">
|
||||||
|
<a
|
||||||
|
className="pointer"
|
||||||
|
onClick={() => {
|
||||||
|
onClearFilters();
|
||||||
|
setLayout(SearchLayout.Folders);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon name="times" />
|
||||||
|
Clear
|
||||||
|
</a>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</HorizontalGroup>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.results}>
|
||||||
{results?.length > 0 && (
|
{results?.length > 0 && (
|
||||||
<SearchResultsFilter
|
<SearchResultsFilter
|
||||||
allChecked={allChecked}
|
allChecked={allChecked}
|
||||||
@ -187,6 +190,13 @@ export const ManageDashboards: FC<Props> = memo(({ folderId, folderUid }) => {
|
|||||||
|
|
||||||
const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||||
return {
|
return {
|
||||||
|
container: css`
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.results-container {
|
||||||
|
padding: 5px 0 0;
|
||||||
|
}
|
||||||
|
`,
|
||||||
searchField: css`
|
searchField: css`
|
||||||
height: auto;
|
height: auto;
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
@ -196,5 +206,12 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
|||||||
width: 400px;
|
width: 400px;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
results: css`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
height: 100%;
|
||||||
|
margin-top: ${theme.spacing.xl};
|
||||||
|
`,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { FC, useCallback, useRef, useEffect } from 'react';
|
import React, { FC, useCallback, useRef, useEffect, CSSProperties } from 'react';
|
||||||
import { css, cx } from 'emotion';
|
import { css, cx } from 'emotion';
|
||||||
import { GrafanaTheme } from '@grafana/data';
|
import { GrafanaTheme } from '@grafana/data';
|
||||||
import { e2e } from '@grafana/e2e';
|
import { e2e } from '@grafana/e2e';
|
||||||
@ -12,11 +12,12 @@ export interface Props {
|
|||||||
editable?: boolean;
|
editable?: boolean;
|
||||||
onTagSelected: (name: string) => any;
|
onTagSelected: (name: string) => any;
|
||||||
onToggleChecked?: OnToggleChecked;
|
onToggleChecked?: OnToggleChecked;
|
||||||
|
style?: CSSProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { selectors } = e2e.pages.Dashboards;
|
const { selectors } = e2e.pages.Dashboards;
|
||||||
|
|
||||||
export const SearchItem: FC<Props> = ({ item, editable, onToggleChecked, onTagSelected }) => {
|
export const SearchItem: FC<Props> = ({ item, editable, onToggleChecked, onTagSelected, style }) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const styles = getResultsItemStyles(theme);
|
const styles = getResultsItemStyles(theme);
|
||||||
const inputEl = useRef<HTMLInputElement>(null);
|
const inputEl = useRef<HTMLInputElement>(null);
|
||||||
@ -60,6 +61,7 @@ export const SearchItem: FC<Props> = ({ item, editable, onToggleChecked, onTagSe
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
|
style={style}
|
||||||
aria-label={selectors.dashboards(item.title)}
|
aria-label={selectors.dashboards(item.title)}
|
||||||
className={cx(styles.wrapper, { [styles.selected]: item.selected })}
|
className={cx(styles.wrapper, { [styles.selected]: item.selected })}
|
||||||
>
|
>
|
||||||
@ -83,7 +85,6 @@ const getResultsItemStyles = stylesFactory((theme: GrafanaTheme) => ({
|
|||||||
${styleMixins.listItem(theme)};
|
${styleMixins.listItem(theme)};
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin: ${theme.spacing.xxs};
|
|
||||||
padding: 0 ${theme.spacing.sm};
|
padding: 0 ${theme.spacing.sm};
|
||||||
min-height: 37px;
|
min-height: 37px;
|
||||||
|
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
import React, { FC } from 'react';
|
import React, { FC, MutableRefObject } from 'react';
|
||||||
import { css, cx } from 'emotion';
|
import { css, cx } from 'emotion';
|
||||||
|
import { FixedSizeList } from 'react-window';
|
||||||
|
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||||
import { GrafanaTheme } from '@grafana/data';
|
import { GrafanaTheme } from '@grafana/data';
|
||||||
import { Icon, stylesFactory, useTheme, IconName, IconButton, Spinner } from '@grafana/ui';
|
import { stylesFactory, useTheme, Spinner } from '@grafana/ui';
|
||||||
import appEvents from 'app/core/app_events';
|
|
||||||
import { CoreEvents } from 'app/types';
|
|
||||||
import { DashboardSection, OnToggleChecked, SearchLayout } from '../types';
|
import { DashboardSection, OnToggleChecked, SearchLayout } from '../types';
|
||||||
|
import { getVisibleItems } from '../utils';
|
||||||
|
import { ITEM_HEIGHT } from '../constants';
|
||||||
import { SearchItem } from './SearchItem';
|
import { SearchItem } from './SearchItem';
|
||||||
import { SearchCheckbox } from './SearchCheckbox';
|
import { SectionHeader } from './SectionHeader';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
editable?: boolean;
|
editable?: boolean;
|
||||||
@ -14,8 +16,9 @@ export interface Props {
|
|||||||
onTagSelected: (name: string) => any;
|
onTagSelected: (name: string) => any;
|
||||||
onToggleChecked?: OnToggleChecked;
|
onToggleChecked?: OnToggleChecked;
|
||||||
onToggleSection: (section: DashboardSection) => void;
|
onToggleSection: (section: DashboardSection) => void;
|
||||||
results: DashboardSection[] | undefined;
|
results: DashboardSection[];
|
||||||
layout?: string;
|
layout?: string;
|
||||||
|
wrapperRef?: MutableRefObject<HTMLDivElement | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SearchResults: FC<Props> = ({
|
export const SearchResults: FC<Props> = ({
|
||||||
@ -25,19 +28,53 @@ export const SearchResults: FC<Props> = ({
|
|||||||
onToggleChecked,
|
onToggleChecked,
|
||||||
onToggleSection,
|
onToggleSection,
|
||||||
results,
|
results,
|
||||||
|
wrapperRef,
|
||||||
layout,
|
layout,
|
||||||
}) => {
|
}) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const styles = getSectionStyles(theme);
|
const styles = getSectionStyles(theme);
|
||||||
|
const itemProps = { editable, onToggleChecked, onTagSelected };
|
||||||
|
|
||||||
const renderItems = (section: DashboardSection) => {
|
const renderFolders = () => {
|
||||||
if (!section.expanded && layout !== SearchLayout.List) {
|
return (
|
||||||
return null;
|
<ul className={styles.wrapper}>
|
||||||
}
|
{results.map(section => {
|
||||||
|
return (
|
||||||
|
<li aria-label="Search section" className={styles.section} key={section.title}>
|
||||||
|
<SectionHeader onSectionClick={onToggleSection} {...{ onToggleChecked, editable, section }} />
|
||||||
|
<ul aria-label="Search items">
|
||||||
|
{section.expanded && section.items.map(item => <SearchItem key={item.id} {...itemProps} item={item} />)}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return section.items.map(item => (
|
const items = getVisibleItems(results);
|
||||||
<SearchItem key={item.id} {...{ item, editable, onToggleChecked, onTagSelected }} />
|
|
||||||
));
|
const renderDashboards = () => {
|
||||||
|
return (
|
||||||
|
<AutoSizer disableWidth>
|
||||||
|
{({ height }) => (
|
||||||
|
<FixedSizeList
|
||||||
|
aria-label="Search items"
|
||||||
|
className={styles.wrapper}
|
||||||
|
innerElementType="ul"
|
||||||
|
itemSize={ITEM_HEIGHT}
|
||||||
|
height={height}
|
||||||
|
itemCount={items.length}
|
||||||
|
width="100%"
|
||||||
|
>
|
||||||
|
{({ index, style }) => {
|
||||||
|
const item = items[index];
|
||||||
|
return <SearchItem key={item.id} {...itemProps} item={item} style={style} />;
|
||||||
|
}}
|
||||||
|
</FixedSizeList>
|
||||||
|
)}
|
||||||
|
</AutoSizer>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@ -45,28 +82,15 @@ export const SearchResults: FC<Props> = ({
|
|||||||
} else if (!results || !results.length) {
|
} else if (!results || !results.length) {
|
||||||
return <h6>No dashboards matching your query were found.</h6>;
|
return <h6>No dashboards matching your query were found.</h6>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="search-results-container">
|
<div className={cx('results-container', styles.resultsContainer)}>
|
||||||
<ul className={styles.wrapper}>
|
{layout !== SearchLayout.List ? renderFolders() : renderDashboards()}
|
||||||
{results.map(section =>
|
|
||||||
layout !== SearchLayout.List ? (
|
|
||||||
<li aria-label="Search section" className={styles.section} key={section.title}>
|
|
||||||
<SectionHeader onSectionClick={onToggleSection} {...{ onToggleChecked, editable, section, layout }} />
|
|
||||||
<ul aria-label="Search items" className={styles.wrapper}>
|
|
||||||
{renderItems(section)}
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
) : (
|
|
||||||
renderItems(section)
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSectionStyles = stylesFactory((theme: GrafanaTheme) => {
|
const getSectionStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||||
|
const { xs, sm, md } = theme.spacing;
|
||||||
return {
|
return {
|
||||||
wrapper: css`
|
wrapper: css`
|
||||||
list-style: none;
|
list-style: none;
|
||||||
@ -74,7 +98,7 @@ const getSectionStyles = stylesFactory((theme: GrafanaTheme) => {
|
|||||||
section: css`
|
section: css`
|
||||||
background: ${theme.colors.panelBg};
|
background: ${theme.colors.panelBg};
|
||||||
border-bottom: solid 1px ${theme.isLight ? theme.palette.gray95 : theme.palette.gray25};
|
border-bottom: solid 1px ${theme.isLight ? theme.palette.gray95 : theme.palette.gray25};
|
||||||
padding: 0px 4px 4px 4px;
|
padding: 0px ${xs} ${xs};
|
||||||
margin-bottom: 3px;
|
margin-bottom: 3px;
|
||||||
`,
|
`,
|
||||||
spinner: css`
|
spinner: css`
|
||||||
@ -83,94 +107,15 @@ const getSectionStyles = stylesFactory((theme: GrafanaTheme) => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
min-height: 100px;
|
min-height: 100px;
|
||||||
`,
|
`,
|
||||||
};
|
resultsContainer: css`
|
||||||
});
|
padding: ${sm};
|
||||||
|
position: relative;
|
||||||
interface SectionHeaderProps {
|
flex-grow: 10;
|
||||||
editable?: boolean;
|
margin-bottom: ${md};
|
||||||
onSectionClick: (section: DashboardSection) => void;
|
background: ${theme.palette.gray10};
|
||||||
onToggleChecked?: OnToggleChecked;
|
border: 1px solid ${theme.palette.gray15};
|
||||||
section: DashboardSection;
|
border-radius: 3px;
|
||||||
}
|
height: 100%;
|
||||||
|
|
||||||
const SectionHeader: FC<SectionHeaderProps> = ({ section, onSectionClick, onToggleChecked, editable = false }) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
const styles = getSectionHeaderStyles(theme, section.selected);
|
|
||||||
|
|
||||||
const onSectionExpand = () => {
|
|
||||||
onSectionClick(section);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSectionChecked = (e: React.MouseEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
if (onToggleChecked) {
|
|
||||||
onToggleChecked(section);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return !section.hideHeader ? (
|
|
||||||
<div className={styles.wrapper} onClick={onSectionExpand}>
|
|
||||||
<SearchCheckbox editable={editable} checked={section.checked} onClick={onSectionChecked} />
|
|
||||||
<Icon className={styles.icon} name={section.icon as IconName} />
|
|
||||||
|
|
||||||
<span className={styles.text}>{section.title}</span>
|
|
||||||
{section.url && (
|
|
||||||
<a
|
|
||||||
href={section.url}
|
|
||||||
className={styles.link}
|
|
||||||
onClick={() => appEvents.emit(CoreEvents.hideDashSearch, { target: 'search-item' })}
|
|
||||||
>
|
|
||||||
<IconButton name="cog" className={styles.button} />
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
<Icon name={section.expanded ? 'angle-down' : 'angle-right'} />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className={styles.wrapper} />
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getSectionHeaderStyles = stylesFactory((theme: GrafanaTheme, selected = false) => {
|
|
||||||
const { sm, xs } = theme.spacing;
|
|
||||||
return {
|
|
||||||
wrapper: cx(
|
|
||||||
css`
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
font-size: ${theme.typography.size.base};
|
|
||||||
padding: ${sm} ${xs} ${xs};
|
|
||||||
color: ${theme.colors.textWeak};
|
|
||||||
|
|
||||||
&:hover,
|
|
||||||
&.selected {
|
|
||||||
color: ${theme.colors.text};
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
a {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
'pointer',
|
|
||||||
{ selected }
|
|
||||||
),
|
|
||||||
icon: css`
|
|
||||||
width: 43px;
|
|
||||||
`,
|
|
||||||
text: css`
|
|
||||||
flex-grow: 1;
|
|
||||||
line-height: 24px;
|
|
||||||
`,
|
|
||||||
link: css`
|
|
||||||
padding: 2px 10px 0;
|
|
||||||
color: ${theme.colors.textWeak};
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 150ms ease-in-out;
|
|
||||||
`,
|
|
||||||
button: css`
|
|
||||||
margin-top: 3px;
|
|
||||||
`,
|
`,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
99
public/app/features/search/components/SectionHeader.tsx
Normal file
99
public/app/features/search/components/SectionHeader.tsx
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import React, { FC, useCallback } from 'react';
|
||||||
|
import { css, cx } from 'emotion';
|
||||||
|
import { GrafanaTheme } from '@grafana/data';
|
||||||
|
import { Icon, IconButton, IconName, stylesFactory, useTheme } from '@grafana/ui';
|
||||||
|
import { DashboardSection, OnToggleChecked } from '../types';
|
||||||
|
import { SearchCheckbox } from './SearchCheckbox';
|
||||||
|
|
||||||
|
interface SectionHeaderProps {
|
||||||
|
editable?: boolean;
|
||||||
|
onSectionClick: (section: DashboardSection) => void;
|
||||||
|
onToggleChecked?: OnToggleChecked;
|
||||||
|
section: DashboardSection;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SectionHeader: FC<SectionHeaderProps> = ({
|
||||||
|
section,
|
||||||
|
onSectionClick,
|
||||||
|
onToggleChecked,
|
||||||
|
editable = false,
|
||||||
|
}) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const styles = getSectionHeaderStyles(theme, section.selected);
|
||||||
|
|
||||||
|
const onSectionExpand = () => {
|
||||||
|
onSectionClick(section);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSectionChecked = useCallback(
|
||||||
|
(e: React.MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
if (onToggleChecked) {
|
||||||
|
onToggleChecked(section);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[section]
|
||||||
|
);
|
||||||
|
|
||||||
|
return !section.hideHeader ? (
|
||||||
|
<div className={styles.wrapper} onClick={onSectionExpand}>
|
||||||
|
<SearchCheckbox editable={editable} checked={section.checked} onClick={onSectionChecked} />
|
||||||
|
<Icon className={styles.icon} name={section.icon as IconName} />
|
||||||
|
|
||||||
|
<span className={styles.text}>{section.title}</span>
|
||||||
|
{section.url && (
|
||||||
|
<a href={section.url} className={styles.link}>
|
||||||
|
<IconButton name="cog" className={styles.button} />
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
<Icon name={section.expanded ? 'angle-down' : 'angle-right'} />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className={styles.wrapper} />
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSectionHeaderStyles = stylesFactory((theme: GrafanaTheme, selected = false) => {
|
||||||
|
const { sm, xs } = theme.spacing;
|
||||||
|
return {
|
||||||
|
wrapper: cx(
|
||||||
|
css`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: ${theme.typography.size.base};
|
||||||
|
padding: ${sm} ${xs} ${xs};
|
||||||
|
color: ${theme.colors.textWeak};
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&.selected {
|
||||||
|
color: ${theme.colors.text};
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
a {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
'pointer',
|
||||||
|
{ selected }
|
||||||
|
),
|
||||||
|
icon: css`
|
||||||
|
width: 43px;
|
||||||
|
`,
|
||||||
|
text: css`
|
||||||
|
flex-grow: 1;
|
||||||
|
line-height: 24px;
|
||||||
|
`,
|
||||||
|
link: css`
|
||||||
|
padding: 2px 10px 0;
|
||||||
|
color: ${theme.colors.textWeak};
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 150ms ease-in-out;
|
||||||
|
`,
|
||||||
|
button: css`
|
||||||
|
margin-top: 3px;
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
});
|
@ -1,2 +1,4 @@
|
|||||||
export const NO_ID_SECTIONS = ['Recent', 'Starred'];
|
export const NO_ID_SECTIONS = ['Recent', 'Starred'];
|
||||||
|
// Height of the search result item
|
||||||
|
export const ITEM_HEIGHT = 40;
|
||||||
export const DEFAULT_SORT = { label: 'A-Z', value: 'alpha-asc' };
|
export const DEFAULT_SORT = { label: 'A-Z', value: 'alpha-asc' };
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { SearchLayout } from '../types';
|
import { DashboardQuery, SearchLayout } from '../types';
|
||||||
|
|
||||||
export const layoutOptions = [
|
export const layoutOptions = [
|
||||||
{ label: 'Folders', value: SearchLayout.Folders, icon: 'folder' },
|
{ label: 'Folders', value: SearchLayout.Folders, icon: 'folder' },
|
||||||
{ label: 'List', value: SearchLayout.List, icon: 'list-ul' },
|
{ label: 'List', value: SearchLayout.List, icon: 'list-ul' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const useSearchLayout = (query: any) => {
|
export const useSearchLayout = (query: DashboardQuery, defaultLayout = SearchLayout.Folders) => {
|
||||||
const [layout, setLayout] = useState<string>(layoutOptions[0].value);
|
const [layout, setLayout] = useState<string>(defaultLayout);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (query.sort) {
|
if (query.sort) {
|
||||||
|
@ -28,6 +28,18 @@ export const getFlattenedSections = (sections: DashboardSection[]): string[] =>
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all items for currently expanded sections
|
||||||
|
* @param sections
|
||||||
|
*/
|
||||||
|
export const getVisibleItems = (sections: DashboardSection[]) => {
|
||||||
|
return sections.flatMap(section => {
|
||||||
|
if (section.expanded) {
|
||||||
|
return section.items;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
};
|
||||||
/**
|
/**
|
||||||
* Since Recent and Starred folders don't have id, title field is used as id
|
* Since Recent and Starred folders don't have id, title field is used as id
|
||||||
* @param title - title field of the section
|
* @param title - title field of the section
|
||||||
|
Loading…
Reference in New Issue
Block a user