1import React, { FC, useCallback, useEffect, useRef, useState } from 'react'; 2import { css } from '@emotion/css'; 3import { GrafanaTheme, PanelData, SelectableValue } from '@grafana/data'; 4import { Button, CustomScrollbar, FilterInput, RadioButtonGroup, useStyles } from '@grafana/ui'; 5import { changePanelPlugin } from '../../../panel/state/actions'; 6import { PanelModel } from '../../state/PanelModel'; 7import { useDispatch, useSelector } from 'react-redux'; 8import { VizTypePicker } from '../../../panel/components/VizTypePicker/VizTypePicker'; 9import { Field } from '@grafana/ui/src/components/Forms/Field'; 10import { PanelLibraryOptionsGroup } from 'app/features/library-panels/components/PanelLibraryOptionsGroup/PanelLibraryOptionsGroup'; 11import { toggleVizPicker } from './state/reducers'; 12import { selectors } from '@grafana/e2e-selectors'; 13import { getPanelPluginWithFallback } from '../../state/selectors'; 14import { VizTypeChangeDetails } from 'app/features/panel/components/VizTypePicker/types'; 15import { VisualizationSuggestions } from 'app/features/panel/components/VizTypePicker/VisualizationSuggestions'; 16import { useLocalStorage } from 'react-use'; 17import { VisualizationSelectPaneTab } from './types'; 18import { LS_VISUALIZATION_SELECT_TAB_KEY } from 'app/core/constants'; 19 20interface Props { 21 panel: PanelModel; 22 data?: PanelData; 23} 24 25export const VisualizationSelectPane: FC<Props> = ({ panel, data }) => { 26 const plugin = useSelector(getPanelPluginWithFallback(panel.type)); 27 const [searchQuery, setSearchQuery] = useState(''); 28 const [listMode, setListMode] = useLocalStorage( 29 LS_VISUALIZATION_SELECT_TAB_KEY, 30 VisualizationSelectPaneTab.Visualizations 31 ); 32 33 const dispatch = useDispatch(); 34 const styles = useStyles(getStyles); 35 const searchRef = useRef<HTMLInputElement | null>(null); 36 37 const onVizChange = useCallback( 38 (pluginChange: VizTypeChangeDetails) => { 39 dispatch(changePanelPlugin({ panel: panel, ...pluginChange })); 40 41 // close viz picker unless a mod key is pressed while clicking 42 if (!pluginChange.withModKey) { 43 dispatch(toggleVizPicker(false)); 44 } 45 }, 46 [dispatch, panel] 47 ); 48 49 // Give Search input focus when using radio button switch list mode 50 useEffect(() => { 51 if (searchRef.current) { 52 searchRef.current.focus(); 53 } 54 }, [listMode]); 55 56 const onCloseVizPicker = () => { 57 dispatch(toggleVizPicker(false)); 58 }; 59 60 if (!plugin) { 61 return null; 62 } 63 64 const radioOptions: Array<SelectableValue<VisualizationSelectPaneTab>> = [ 65 { label: 'Visualizations', value: VisualizationSelectPaneTab.Visualizations }, 66 { label: 'Suggestions', value: VisualizationSelectPaneTab.Suggestions }, 67 { 68 label: 'Library panels', 69 value: VisualizationSelectPaneTab.LibraryPanels, 70 description: 'Reusable panels you can share between multiple dashboards.', 71 }, 72 ]; 73 74 return ( 75 <div className={styles.openWrapper}> 76 <div className={styles.formBox}> 77 <div className={styles.searchRow}> 78 <FilterInput 79 value={searchQuery} 80 onChange={setSearchQuery} 81 ref={searchRef} 82 autoFocus={true} 83 placeholder="Search for..." 84 /> 85 <Button 86 title="Close" 87 variant="secondary" 88 icon="angle-up" 89 className={styles.closeButton} 90 aria-label={selectors.components.PanelEditor.toggleVizPicker} 91 onClick={onCloseVizPicker} 92 /> 93 </div> 94 <Field className={styles.customFieldMargin}> 95 <RadioButtonGroup options={radioOptions} value={listMode} onChange={setListMode} fullWidth /> 96 </Field> 97 </div> 98 <div className={styles.scrollWrapper}> 99 <CustomScrollbar autoHeightMin="100%"> 100 <div className={styles.scrollContent}> 101 {listMode === VisualizationSelectPaneTab.Visualizations && ( 102 <VizTypePicker 103 current={plugin.meta} 104 onChange={onVizChange} 105 searchQuery={searchQuery} 106 data={data} 107 onClose={() => {}} 108 /> 109 )} 110 {listMode === VisualizationSelectPaneTab.Suggestions && ( 111 <VisualizationSuggestions 112 current={plugin.meta} 113 onChange={onVizChange} 114 searchQuery={searchQuery} 115 panel={panel} 116 data={data} 117 onClose={() => {}} 118 /> 119 )} 120 {listMode === VisualizationSelectPaneTab.LibraryPanels && ( 121 <PanelLibraryOptionsGroup searchQuery={searchQuery} panel={panel} key="Panel Library" /> 122 )} 123 </div> 124 </CustomScrollbar> 125 </div> 126 </div> 127 ); 128}; 129 130VisualizationSelectPane.displayName = 'VisualizationSelectPane'; 131 132const getStyles = (theme: GrafanaTheme) => { 133 return { 134 icon: css` 135 color: ${theme.palette.gray33}; 136 `, 137 wrapper: css` 138 display: flex; 139 flex-direction: column; 140 flex: 1 1 0; 141 height: 100%; 142 `, 143 vizButton: css` 144 text-align: left; 145 `, 146 scrollWrapper: css` 147 flex-grow: 1; 148 min-height: 0; 149 `, 150 scrollContent: css` 151 padding: ${theme.spacing.sm}; 152 `, 153 openWrapper: css` 154 display: flex; 155 flex-direction: column; 156 flex: 1 1 100%; 157 height: 100%; 158 background: ${theme.colors.bg1}; 159 border: 1px solid ${theme.colors.border1}; 160 `, 161 searchRow: css` 162 display: flex; 163 margin-bottom: ${theme.spacing.sm}; 164 `, 165 closeButton: css` 166 margin-left: ${theme.spacing.sm}; 167 `, 168 customFieldMargin: css` 169 margin-bottom: ${theme.spacing.sm}; 170 `, 171 formBox: css` 172 padding: ${theme.spacing.sm}; 173 padding-bottom: 0; 174 `, 175 }; 176}; 177