Commit 3bd9e1dd by Dominik Prokop Committed by GitHub

NewPanelEditor: vis picker UI update (#23894)

* Fix storybook

* Add deprecated/alpha badge to panels in viz picker, fix long title display

* Move getFocusCss to mixins

* Updated hover/active state of vis picker item

* try fixing e2e

* Add removed label for e2e to be happy happy happy
parent 6dfad3b3
...@@ -3,7 +3,7 @@ import { GrafanaTheme } from '@grafana/data'; ...@@ -3,7 +3,7 @@ import { GrafanaTheme } from '@grafana/data';
import { getLabelStyles } from './Label'; import { getLabelStyles } from './Label';
import { useTheme, stylesFactory } from '../../themes'; import { useTheme, stylesFactory } from '../../themes';
import { css, cx } from 'emotion'; import { css, cx } from 'emotion';
import { getFocusCss } from './commonStyles'; import { focusCss } from '../../themes/mixins';
export interface CheckboxProps extends Omit<HTMLProps<HTMLInputElement>, 'value'> { export interface CheckboxProps extends Omit<HTMLProps<HTMLInputElement>, 'value'> {
label?: string; label?: string;
...@@ -41,7 +41,7 @@ export const getCheckboxStyles = stylesFactory((theme: GrafanaTheme) => { ...@@ -41,7 +41,7 @@ export const getCheckboxStyles = stylesFactory((theme: GrafanaTheme) => {
height: 100%; height: 100%;
opacity: 0; opacity: 0;
&:focus + span { &:focus + span {
${getFocusCss(theme)} ${focusCss(theme)}
} }
/** /**
......
...@@ -2,7 +2,8 @@ import React from 'react'; ...@@ -2,7 +2,8 @@ import React from 'react';
import { useTheme, stylesFactory } from '../../../themes'; import { useTheme, stylesFactory } from '../../../themes';
import { GrafanaTheme } from '@grafana/data'; import { GrafanaTheme } from '@grafana/data';
import { css, cx } from 'emotion'; import { css, cx } from 'emotion';
import { getFocusCss, getPropertiesForButtonSize } from '../commonStyles'; import { getPropertiesForButtonSize } from '../commonStyles';
import { focusCss } from '../../../themes/mixins';
export type RadioButtonSize = 'sm' | 'md'; export type RadioButtonSize = 'sm' | 'md';
...@@ -56,7 +57,7 @@ const getRadioButtonStyles = stylesFactory((theme: GrafanaTheme, size: RadioButt ...@@ -56,7 +57,7 @@ const getRadioButtonStyles = stylesFactory((theme: GrafanaTheme, size: RadioButt
} }
&:focus + label { &:focus + label {
${getFocusCss(theme)}; ${focusCss(theme)};
z-index: 3; z-index: 3;
} }
......
import { css } from 'emotion'; import { css } from 'emotion';
import { GrafanaTheme } from '@grafana/data'; import { GrafanaTheme } from '@grafana/data';
import { StyleProps } from '../Button'; import { StyleProps } from '../Button';
import { focusCss } from '../../themes/mixins';
export const getFocusCss = (theme: GrafanaTheme) => `
outline: 2px dotted transparent;
outline-offset: 2px;
box-shadow: 0 0 0 2px ${theme.colors.bodyBg}, 0 0 0px 4px ${theme.colors.formFocusOutline};
transition: all 0.2s cubic-bezier(0.19, 1, 0.22, 1);
`;
export const getFocusStyle = (theme: GrafanaTheme) => css` export const getFocusStyle = (theme: GrafanaTheme) => css`
&:focus { &:focus {
${getFocusCss(theme)} ${focusCss(theme)}
} }
`; `;
......
...@@ -10,7 +10,7 @@ import { useTheme, selectThemeVariant } from '../../themes'; ...@@ -10,7 +10,7 @@ import { useTheme, selectThemeVariant } from '../../themes';
import mdx from './Icon.mdx'; import mdx from './Icon.mdx';
export default { export default {
title: 'Docs Overview//Icon', title: 'Docs Overview/Icon',
component: Icon, component: Icon,
decorators: [withCenteredStory], decorators: [withCenteredStory],
parameters: { parameters: {
...@@ -60,7 +60,7 @@ const IconWrapper: React.FC<{ name: IconName }> = ({ name }) => { ...@@ -60,7 +60,7 @@ const IconWrapper: React.FC<{ name: IconName }> = ({ name }) => {
const icons = getAvailableIcons().sort((a, b) => a.localeCompare(b)); const icons = getAvailableIcons().sort((a, b) => a.localeCompare(b));
export const simple = () => { export const iconsOverview = () => {
const [filter, setFilter] = useState(''); const [filter, setFilter] = useState('');
const searchIcon = (event: ChangeEvent<HTMLInputElement>) => { const searchIcon = (event: ChangeEvent<HTMLInputElement>) => {
......
import React from 'react'; import React from 'react';
import { useTheme } from '../../themes/ThemeContext'; import { useTheme } from '../../themes/ThemeContext';
import { getFocusCss, sharedInputStyle } from '../Forms/commonStyles'; import { sharedInputStyle } from '../Forms/commonStyles';
import { getInputStyles } from '../Input/Input'; import { getInputStyles } from '../Input/Input';
import { cx, css } from 'emotion'; import { css, cx } from 'emotion';
import { stylesFactory } from '../../themes'; import { stylesFactory } from '../../themes';
import { GrafanaTheme } from '@grafana/data'; import { GrafanaTheme } from '@grafana/data';
import { focusCss } from '../../themes/mixins';
interface InputControlProps { interface InputControlProps {
/** Show an icon as a prefix in the input */ /** Show an icon as a prefix in the input */
...@@ -25,7 +26,7 @@ const getInputControlStyles = stylesFactory( ...@@ -25,7 +26,7 @@ const getInputControlStyles = stylesFactory(
sharedInputStyle(theme, invalid), sharedInputStyle(theme, invalid),
focused && focused &&
css` css`
${getFocusCss(theme)} ${focusCss(theme)}
`, `,
disabled && styles.inputDisabled, disabled && styles.inputDisabled,
css` css`
......
...@@ -3,7 +3,7 @@ import { css, cx } from 'emotion'; ...@@ -3,7 +3,7 @@ import { css, cx } from 'emotion';
import uniqueId from 'lodash/uniqueId'; import uniqueId from 'lodash/uniqueId';
import { GrafanaTheme } from '@grafana/data'; import { GrafanaTheme } from '@grafana/data';
import { stylesFactory, useTheme } from '../../themes'; import { stylesFactory, useTheme } from '../../themes';
import { getFocusCss } from '../Forms/commonStyles'; import { focusCss } from '../../themes/mixins';
export interface SwitchProps extends Omit<HTMLProps<HTMLInputElement>, 'value'> { export interface SwitchProps extends Omit<HTMLProps<HTMLInputElement>, 'value'> {
value?: boolean; value?: boolean;
...@@ -40,7 +40,7 @@ export const getSwitchStyles = stylesFactory((theme: GrafanaTheme) => { ...@@ -40,7 +40,7 @@ export const getSwitchStyles = stylesFactory((theme: GrafanaTheme) => {
} }
&:focus + label { &:focus + label {
${getFocusCss(theme)}; ${focusCss(theme)};
} }
} }
......
...@@ -4,8 +4,9 @@ import { css, cx } from 'emotion'; ...@@ -4,8 +4,9 @@ import { css, cx } from 'emotion';
import { dateTime, DateTime, dateTimeAsMoment, GrafanaTheme } from '@grafana/data'; import { dateTime, DateTime, dateTimeAsMoment, GrafanaTheme } from '@grafana/data';
import { useTheme, Icon } from '../../index'; import { useTheme, Icon } from '../../index';
import { stylesFactory } from '../../themes'; import { stylesFactory } from '../../themes';
import { inputSizes, getFocusCss } from '../Forms/commonStyles'; import { inputSizes } from '../Forms/commonStyles';
import { FormInputSize } from '../Forms/types'; import { FormInputSize } from '../Forms/types';
import { focusCss } from '../../themes/mixins';
interface Props { interface Props {
onChange: (value: DateTime) => void; onChange: (value: DateTime) => void;
...@@ -76,7 +77,7 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => { ...@@ -76,7 +77,7 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
height: ${theme.spacing.formInputHeight}px; height: ${theme.spacing.formInputHeight}px;
&:focus { &:focus {
${getFocusCss(theme)} ${focusCss(theme)}
} }
} }
`, `,
......
...@@ -39,3 +39,10 @@ export function listItemSelected(theme: GrafanaTheme): string { ...@@ -39,3 +39,10 @@ export function listItemSelected(theme: GrafanaTheme): string {
color: ${theme.colors.textStrong}; color: ${theme.colors.textStrong};
`; `;
} }
export const focusCss = (theme: GrafanaTheme) => `
outline: 2px dotted transparent;
outline-offset: 2px;
box-shadow: 0 0 0 2px ${theme.colors.bodyBg}, 0 0 0px 4px ${theme.colors.formFocusOutline};
transition: all 0.2s cubic-bezier(0.19, 1, 0.22, 1);
`;
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import { css } from 'emotion'; import { css } from 'emotion';
import { GrafanaTheme, PanelPlugin, PanelPluginMeta } from '@grafana/data'; import { GrafanaTheme, PanelPlugin, PanelPluginMeta } from '@grafana/data';
import { CustomScrollbar, useTheme, stylesFactory, Icon, Input } from '@grafana/ui'; import { useTheme, stylesFactory, Icon, Input } from '@grafana/ui';
import { changePanelPlugin } from '../../state/actions'; import { changePanelPlugin } from '../../state/actions';
import { StoreState } from 'app/types'; import { StoreState } from 'app/types';
import { PanelModel } from '../../state/PanelModel'; import { PanelModel } from '../../state/PanelModel';
...@@ -61,29 +61,24 @@ export const VisualizationTabUnconnected = React.forwardRef<HTMLInputElement, Pr ...@@ -61,29 +61,24 @@ export const VisualizationTabUnconnected = React.forwardRef<HTMLInputElement, Pr
return ( return (
<div className={styles.wrapper}> <div className={styles.wrapper}>
<div className={styles.search}> <Field>
<Field> <Input
<Input value={searchQuery}
value={searchQuery} onChange={e => setSearchQuery(e.currentTarget.value)}
onChange={e => setSearchQuery(e.currentTarget.value)} onKeyPress={onKeyPress}
onKeyPress={onKeyPress} prefix={<Icon name="filter" className={styles.icon} />}
prefix={<Icon name="filter" className={styles.icon} />} suffix={suffix}
suffix={suffix} placeholder="Filter visualisations"
placeholder="Filter visualisations" ref={ref}
ref={ref} />
/> </Field>
</Field>
</div> <VizTypePicker
<div className={styles.visList}> current={plugin.meta}
<CustomScrollbar> onTypeChange={onPluginTypeChange}
<VizTypePicker searchQuery={searchQuery}
current={plugin.meta} onClose={() => {}}
onTypeChange={onPluginTypeChange} />
searchQuery={searchQuery}
onClose={() => {}}
/>
</CustomScrollbar>
</div>
</div> </div>
); );
} }
...@@ -96,22 +91,11 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => { ...@@ -96,22 +91,11 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
wrapper: css` wrapper: css`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex-grow: 1;
max-height: 100%;
`,
search: css`
flex-grow: 0;
flex-shrink: 1;
`, `,
searchClear: css` searchClear: css`
color: ${theme.palette.gray60}; color: ${theme.palette.gray60};
cursor: pointer; cursor: pointer;
`, `,
visList: css`
flex-grow: 1;
height: 100%;
overflow: hidden;
`,
}; };
}); });
......
...@@ -95,6 +95,8 @@ export const VizTypePicker: React.FC<Props> = ({ searchQuery, onTypeChange, curr ...@@ -95,6 +95,8 @@ export const VizTypePicker: React.FC<Props> = ({ searchQuery, onTypeChange, curr
); );
}; };
VizTypePicker.displayName = 'VizTypePicker';
const getStyles = stylesFactory((theme: GrafanaTheme) => { const getStyles = stylesFactory((theme: GrafanaTheme) => {
return { return {
grid: css` grid: css`
......
import React from 'react'; import React from 'react';
import { GrafanaTheme, PanelPluginMeta } from '@grafana/data'; import { GrafanaTheme, PanelPluginMeta } from '@grafana/data';
import { stylesFactory, useTheme } from '@grafana/ui'; import { stylesFactory, useTheme, styleMixins } from '@grafana/ui';
import { css, cx } from 'emotion'; import { css, cx } from 'emotion';
import { e2e } from '@grafana/e2e'; import { e2e } from '@grafana/e2e';
import { PanelPluginBadge } from '../../plugins/PluginSignatureBadge';
interface Props { interface Props {
isCurrent: boolean; isCurrent: boolean;
...@@ -16,32 +17,50 @@ const VizTypePickerPlugin: React.FC<Props> = ({ isCurrent, plugin, onClick, disa ...@@ -16,32 +17,50 @@ const VizTypePickerPlugin: React.FC<Props> = ({ isCurrent, plugin, onClick, disa
const styles = getStyles(theme); const styles = getStyles(theme);
const cssClass = cx({ const cssClass = cx({
[styles.item]: true, [styles.item]: true,
[styles.current]: isCurrent,
[styles.disabled]: disabled, [styles.disabled]: disabled,
[styles.current]: isCurrent,
}); });
return ( return (
<div <div className={styles.wrapper} aria-label={e2e.components.PluginVisualization.selectors.item(plugin.name)}>
className={cssClass} <div className={cssClass} onClick={disabled ? () => {} : onClick} title={plugin.name}>
onClick={disabled ? () => {} : onClick} <div className={styles.bg} />
title={plugin.name} <div className={styles.itemContent}>
aria-label={e2e.components.PluginVisualization.selectors.item(plugin.name)} <div className={styles.name} title={plugin.name}>
> {plugin.name}
<div className={styles.name}>{plugin.name}</div> </div>
<img className={styles.img} src={plugin.info.logos.small} /> <img className={styles.img} src={plugin.info.logos.small} />
</div>
</div>
<div className={styles.badge}>
<PanelPluginBadge plugin={plugin} />
</div>
</div> </div>
); );
}; };
VizTypePickerPlugin.displayName = 'VizTypePickerPlugin';
const getStyles = stylesFactory((theme: GrafanaTheme) => { const getStyles = stylesFactory((theme: GrafanaTheme) => {
return { return {
item: css` wrapper: css`
position: relative;
`,
bg: css`
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: ${theme.colors.bg2}; background: ${theme.colors.bg2};
border: 1px solid ${theme.colors.border2}; border: 1px solid ${theme.colors.border2};
border-radius: 3px; border-radius: 3px;
height: 100px; transform: scale(1);
width: 100%; transform-origin: center;
max-width: 200px; transition: all 0.1s ease-in;
z-index: 0;
`,
item: css`
flex-shrink: 0; flex-shrink: 0;
flex-direction: column; flex-direction: column;
text-align: center; text-align: center;
...@@ -51,23 +70,35 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => { ...@@ -51,23 +70,35 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding-bottom: 6px; padding-bottom: 6px;
height: 100px;
width: 100%;
max-width: 200px;
position: relative;
&:hover { &:hover {
box-shadow: 0 0 4px ${theme.palette.blue95}; > div:first-child {
border: 1px solid ${theme.palette.blue95}; transform: scale(1.05);
border-color: ${theme.colors.formFocusOutline};
}
} }
`, `,
itemContent: css`
position: relative;
z-index: 1;
`,
current: css` current: css`
label: currentVisualizationItem; label: currentVisualizationItem;
box-shadow: 0 0 6px ${theme.palette.orange} !important; pointer-events: none;
border: 1px solid ${theme.palette.orange} !important; > div:first-child {
${styleMixins.focusCss(theme)};
}
`, `,
disabled: css` disabled: css`
opacity: 0.2; opacity: 0.2;
filter: grayscale(1); filter: grayscale(1);
cursor: default; cursor: default;
pointer-events: none;
&:hover { &:hover {
box-shadow: none;
border: 1px solid ${theme.colors.border2}; border: 1px solid ${theme.colors.border2};
} }
`, `,
...@@ -76,15 +107,20 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => { ...@@ -76,15 +107,20 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
font-size: ${theme.typography.size.sm}; font-size: ${theme.typography.size.sm};
display: flex; text-align: center;
flex-direction: column;
align-self: center;
height: 23px; height: 23px;
font-weight: ${theme.typography.weight.semibold}; font-weight: ${theme.typography.weight.semibold};
padding: 0 10px;
width: 100%;
`, `,
img: css` img: css`
height: 55px; height: 55px;
`, `,
badge: css`
position: absolute;
bottom: ${theme.spacing.xs};
right: ${theme.spacing.xs};
`,
}; };
}); });
......
import React from 'react'; import React from 'react';
import { Icon, stylesFactory, useTheme, IconName, Tooltip } from '@grafana/ui'; import { Icon, IconName, stylesFactory, Tooltip, useTheme } from '@grafana/ui';
import { GrafanaTheme, PluginSignatureStatus, getColorFromHexRgbOrName } from '@grafana/data'; import {
getColorFromHexRgbOrName,
GrafanaTheme,
PanelPluginMeta,
PluginSignatureStatus,
PluginState,
} from '@grafana/data';
import { css } from 'emotion'; import { css } from 'emotion';
import tinycolor from 'tinycolor2'; import tinycolor from 'tinycolor2';
...@@ -23,6 +29,25 @@ export const PluginSignatureBadge: React.FC<Props> = ({ status }) => { ...@@ -23,6 +29,25 @@ export const PluginSignatureBadge: React.FC<Props> = ({ status }) => {
); );
}; };
interface PanelPluginBadgeProps {
plugin: PanelPluginMeta;
}
export const PanelPluginBadge: React.FC<PanelPluginBadgeProps> = ({ plugin }) => {
const theme = useTheme();
const display = getPanelStateBadgeDisplayModel(plugin);
const styles = getStyles(theme, display);
if (plugin.state !== PluginState.deprecated && plugin.state !== PluginState.alpha) {
return null;
}
return (
<div className={styles.wrapper}>
<Icon name={display.icon} size="sm" />
<span>{display.text}</span>
</div>
);
};
interface DisplayModel { interface DisplayModel {
text: string; text: string;
icon: IconName; icon: IconName;
...@@ -55,6 +80,25 @@ function getSignatureDisplayModel(signature: PluginSignatureStatus): DisplayMode ...@@ -55,6 +80,25 @@ function getSignatureDisplayModel(signature: PluginSignatureStatus): DisplayMode
return { text: 'Unsigned', icon: 'exclamation-triangle', color: 'red', tooltip: 'Unsigned external plugin' }; return { text: 'Unsigned', icon: 'exclamation-triangle', color: 'red', tooltip: 'Unsigned external plugin' };
} }
function getPanelStateBadgeDisplayModel(panel: PanelPluginMeta): DisplayModel {
switch (panel.state) {
case PluginState.deprecated:
return {
text: 'Deprecated',
icon: 'exclamation-triangle',
color: 'red',
tooltip: `${panel.name} panel is deprecated`,
};
}
return {
text: 'Alpha',
icon: 'rocket',
color: 'blue',
tooltip: `${panel.name} panel is experimental`,
};
}
const getStyles = stylesFactory((theme: GrafanaTheme, model: DisplayModel) => { const getStyles = stylesFactory((theme: GrafanaTheme, model: DisplayModel) => {
let sourceColor = getColorFromHexRgbOrName(model.color); let sourceColor = getColorFromHexRgbOrName(model.color);
let borderColor = ''; let borderColor = '';
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment