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