mirror of https://github.com/grafana/grafana
NewPanelEdit: Add unified UI to queries and transformations (#23478)
* Do not use pointer cursor on icon by default * Allow items alignment in the HorizontalGroup layout * Add util for rendering components based on their type (element or function) * Components for rendering query and transformation rows in a unified way * Apply new UI fo query and transformation rows * Add some tests * Minor fix for scroll area Co-authored-by: Torkel Ödegaard <torkel@grafana.com>pull/23484/head
parent
76827d2152
commit
712564f66a
@ -0,0 +1,22 @@ |
|||||||
|
import React from 'react'; |
||||||
|
|
||||||
|
/** |
||||||
|
* Given react node or function returns element accordingly |
||||||
|
* |
||||||
|
* @param itemToRender |
||||||
|
* @param props props to be passed to the function if item provided as such |
||||||
|
*/ |
||||||
|
export function renderOrCallToRender<TProps = any>( |
||||||
|
itemToRender: ((props?: TProps) => React.ReactNode) | React.ReactNode, |
||||||
|
props?: TProps |
||||||
|
): React.ReactNode { |
||||||
|
if (React.isValidElement(itemToRender) || typeof itemToRender === 'string' || typeof itemToRender === 'number') { |
||||||
|
return itemToRender; |
||||||
|
} |
||||||
|
|
||||||
|
if (typeof itemToRender === 'function') { |
||||||
|
return itemToRender(props); |
||||||
|
} |
||||||
|
|
||||||
|
throw new Error(`${itemToRender} is not a React element nor a function that returns React element`); |
||||||
|
} |
@ -0,0 +1,23 @@ |
|||||||
|
import React from 'react'; |
||||||
|
import { QueryOperationAction } from './QueryOperationAction'; |
||||||
|
import { shallow } from 'enzyme'; |
||||||
|
|
||||||
|
describe('QueryOperationAction', () => { |
||||||
|
it('renders', () => { |
||||||
|
expect(() => shallow(<QueryOperationAction icon="add-panel" onClick={() => {}} />)).not.toThrow(); |
||||||
|
}); |
||||||
|
describe('when disabled', () => { |
||||||
|
it('does not call onClick handler', () => { |
||||||
|
const clickSpy = jest.fn(); |
||||||
|
const wrapper = shallow(<QueryOperationAction icon="add-panel" onClick={clickSpy} title="Test action" />); |
||||||
|
const actionEl = wrapper.find({ 'aria-label': 'Test action query operation action' }); |
||||||
|
|
||||||
|
expect(actionEl).toHaveLength(1); |
||||||
|
expect(clickSpy).not.toBeCalled(); |
||||||
|
|
||||||
|
actionEl.first().simulate('click'); |
||||||
|
|
||||||
|
expect(clickSpy).toBeCalledTimes(1); |
||||||
|
}); |
||||||
|
}); |
||||||
|
}); |
@ -0,0 +1,45 @@ |
|||||||
|
import { Icon, IconName, stylesFactory, useTheme } from '@grafana/ui'; |
||||||
|
import React from 'react'; |
||||||
|
import { css, cx } from 'emotion'; |
||||||
|
import { GrafanaTheme } from '@grafana/data'; |
||||||
|
|
||||||
|
interface QueryOperationActionProps { |
||||||
|
icon: IconName; |
||||||
|
title?: string; |
||||||
|
onClick: (e: React.MouseEvent) => void; |
||||||
|
disabled?: boolean; |
||||||
|
} |
||||||
|
|
||||||
|
export const QueryOperationAction: React.FC<QueryOperationActionProps> = ({ icon, disabled, title, ...otherProps }) => { |
||||||
|
const theme = useTheme(); |
||||||
|
const styles = getQueryOperationActionStyles(theme, !!disabled); |
||||||
|
const onClick = (e: React.MouseEvent) => { |
||||||
|
if (!disabled) { |
||||||
|
otherProps.onClick(e); |
||||||
|
} |
||||||
|
}; |
||||||
|
return ( |
||||||
|
<div title={title}> |
||||||
|
<Icon name={icon} className={styles.icon} onClick={onClick} aria-label={`${title} query operation action`} /> |
||||||
|
</div> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
const getQueryOperationActionStyles = stylesFactory((theme: GrafanaTheme, disabled: boolean) => { |
||||||
|
return { |
||||||
|
icon: cx( |
||||||
|
!disabled && |
||||||
|
css` |
||||||
|
cursor: pointer; |
||||||
|
color: ${theme.colors.textWeak}; |
||||||
|
`,
|
||||||
|
disabled && |
||||||
|
css` |
||||||
|
color: ${theme.colors.gray25}; |
||||||
|
cursor: disabled; |
||||||
|
` |
||||||
|
), |
||||||
|
}; |
||||||
|
}); |
||||||
|
|
||||||
|
QueryOperationAction.displayName = 'QueryOperationAction'; |
@ -0,0 +1,136 @@ |
|||||||
|
import React from 'react'; |
||||||
|
import { QueryOperationRow } from './QueryOperationRow'; |
||||||
|
import { shallow, mount } from 'enzyme'; |
||||||
|
import { act } from 'react-dom/test-utils'; |
||||||
|
|
||||||
|
describe('QueryOperationRow', () => { |
||||||
|
it('renders', () => { |
||||||
|
expect(() => |
||||||
|
shallow( |
||||||
|
<QueryOperationRow> |
||||||
|
<div>Test</div> |
||||||
|
</QueryOperationRow> |
||||||
|
) |
||||||
|
).not.toThrow(); |
||||||
|
}); |
||||||
|
|
||||||
|
describe('callbacks', () => { |
||||||
|
it('should not call onOpen when component is shallowed', async () => { |
||||||
|
const onOpenSpy = jest.fn(); |
||||||
|
await act(async () => { |
||||||
|
shallow( |
||||||
|
<QueryOperationRow onOpen={onOpenSpy}> |
||||||
|
<div>Test</div> |
||||||
|
</QueryOperationRow> |
||||||
|
); |
||||||
|
}); |
||||||
|
expect(onOpenSpy).not.toBeCalled(); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should call onOpen when row is opened and onClose when row is collapsed', async () => { |
||||||
|
const onOpenSpy = jest.fn(); |
||||||
|
const onCloseSpy = jest.fn(); |
||||||
|
const wrapper = mount( |
||||||
|
<QueryOperationRow onOpen={onOpenSpy} onClose={onCloseSpy} isOpen={false}> |
||||||
|
<div>Test</div> |
||||||
|
</QueryOperationRow> |
||||||
|
); |
||||||
|
const titleEl = wrapper.find({ 'aria-label': 'Query operation row title' }); |
||||||
|
expect(titleEl).toHaveLength(1); |
||||||
|
|
||||||
|
await act(async () => { |
||||||
|
// open
|
||||||
|
titleEl.first().simulate('click'); |
||||||
|
}); |
||||||
|
await act(async () => { |
||||||
|
// close
|
||||||
|
titleEl.first().simulate('click'); |
||||||
|
}); |
||||||
|
|
||||||
|
expect(onOpenSpy).toBeCalledTimes(1); |
||||||
|
expect(onCloseSpy).toBeCalledTimes(1); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
describe('title rendering', () => { |
||||||
|
it('should render title provided as element', () => { |
||||||
|
const title = <div aria-label="test title">Test</div>; |
||||||
|
const wrapper = shallow( |
||||||
|
<QueryOperationRow title={title}> |
||||||
|
<div>Test</div> |
||||||
|
</QueryOperationRow> |
||||||
|
); |
||||||
|
|
||||||
|
const titleEl = wrapper.find({ 'aria-label': 'test title' }); |
||||||
|
expect(titleEl).toHaveLength(1); |
||||||
|
}); |
||||||
|
it('should render title provided as function', () => { |
||||||
|
const title = () => <div aria-label="test title">Test</div>; |
||||||
|
const wrapper = shallow( |
||||||
|
<QueryOperationRow title={title}> |
||||||
|
<div>Test</div> |
||||||
|
</QueryOperationRow> |
||||||
|
); |
||||||
|
|
||||||
|
const titleEl = wrapper.find({ 'aria-label': 'test title' }); |
||||||
|
expect(titleEl).toHaveLength(1); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should expose api to title rendered as function', () => { |
||||||
|
const propsSpy = jest.fn(); |
||||||
|
const title = (props: any) => { |
||||||
|
propsSpy(props); |
||||||
|
return <div aria-label="test title">Test</div>; |
||||||
|
}; |
||||||
|
shallow( |
||||||
|
<QueryOperationRow title={title}> |
||||||
|
<div>Test</div> |
||||||
|
</QueryOperationRow> |
||||||
|
); |
||||||
|
|
||||||
|
expect(Object.keys(propsSpy.mock.calls[0][0])).toContain('isOpen'); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
describe('actions rendering', () => { |
||||||
|
it('should render actions provided as element', () => { |
||||||
|
const actions = <div aria-label="test actions">Test</div>; |
||||||
|
const wrapper = shallow( |
||||||
|
<QueryOperationRow actions={actions}> |
||||||
|
<div>Test</div> |
||||||
|
</QueryOperationRow> |
||||||
|
); |
||||||
|
|
||||||
|
const actionsEl = wrapper.find({ 'aria-label': 'test actions' }); |
||||||
|
expect(actionsEl).toHaveLength(1); |
||||||
|
}); |
||||||
|
it('should render actions provided as function', () => { |
||||||
|
const actions = () => <div aria-label="test actions">Test</div>; |
||||||
|
const wrapper = shallow( |
||||||
|
<QueryOperationRow actions={actions}> |
||||||
|
<div>Test</div> |
||||||
|
</QueryOperationRow> |
||||||
|
); |
||||||
|
|
||||||
|
const actionsEl = wrapper.find({ 'aria-label': 'test actions' }); |
||||||
|
expect(actionsEl).toHaveLength(1); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should expose api to title rendered as function', () => { |
||||||
|
const propsSpy = jest.fn(); |
||||||
|
const actions = (props: any) => { |
||||||
|
propsSpy(props); |
||||||
|
return <div aria-label="test actions">Test</div>; |
||||||
|
}; |
||||||
|
shallow( |
||||||
|
<QueryOperationRow actions={actions}> |
||||||
|
<div>Test</div> |
||||||
|
</QueryOperationRow> |
||||||
|
); |
||||||
|
|
||||||
|
expect(Object.keys(propsSpy.mock.calls[0][0])).toContainEqual('isOpen'); |
||||||
|
expect(Object.keys(propsSpy.mock.calls[0][0])).toContainEqual('openRow'); |
||||||
|
expect(Object.keys(propsSpy.mock.calls[0][0])).toContainEqual('closeRow'); |
||||||
|
}); |
||||||
|
}); |
||||||
|
}); |
@ -0,0 +1,115 @@ |
|||||||
|
import React, { useState } from 'react'; |
||||||
|
import { renderOrCallToRender, HorizontalGroup, Icon, stylesFactory, useTheme } from '@grafana/ui'; |
||||||
|
import { GrafanaTheme } from '@grafana/data'; |
||||||
|
import { css } from 'emotion'; |
||||||
|
import { useUpdateEffect } from 'react-use'; |
||||||
|
|
||||||
|
interface QueryOperationRowProps { |
||||||
|
title?: ((props: { isOpen: boolean }) => React.ReactNode) | React.ReactNode; |
||||||
|
actions?: |
||||||
|
| ((props: { isOpen: boolean; openRow: () => void; closeRow: () => void }) => React.ReactNode) |
||||||
|
| React.ReactNode; |
||||||
|
onOpen?: () => void; |
||||||
|
onClose?: () => void; |
||||||
|
children: React.ReactNode; |
||||||
|
isOpen?: boolean; |
||||||
|
} |
||||||
|
|
||||||
|
export const QueryOperationRow: React.FC<QueryOperationRowProps> = ({ |
||||||
|
children, |
||||||
|
actions, |
||||||
|
title, |
||||||
|
onClose, |
||||||
|
onOpen, |
||||||
|
isOpen, |
||||||
|
}: QueryOperationRowProps) => { |
||||||
|
const [isContentVisible, setIsContentVisible] = useState(isOpen !== undefined ? isOpen : true); |
||||||
|
const theme = useTheme(); |
||||||
|
const styles = getQueryOperationRowStyles(theme); |
||||||
|
useUpdateEffect(() => { |
||||||
|
if (isContentVisible) { |
||||||
|
if (onOpen) { |
||||||
|
onOpen(); |
||||||
|
} |
||||||
|
} else { |
||||||
|
if (onClose) { |
||||||
|
onClose(); |
||||||
|
} |
||||||
|
} |
||||||
|
}, [isContentVisible]); |
||||||
|
|
||||||
|
const titleElement = title && renderOrCallToRender(title, { isOpen: isContentVisible }); |
||||||
|
const actionsElement = |
||||||
|
actions && |
||||||
|
renderOrCallToRender(actions, { |
||||||
|
isOpen: isContentVisible, |
||||||
|
openRow: () => { |
||||||
|
setIsContentVisible(true); |
||||||
|
}, |
||||||
|
closeRow: () => { |
||||||
|
setIsContentVisible(false); |
||||||
|
}, |
||||||
|
}); |
||||||
|
|
||||||
|
return ( |
||||||
|
<div className={styles.wrapper}> |
||||||
|
<div className={styles.header}> |
||||||
|
<HorizontalGroup justify="space-between"> |
||||||
|
<div |
||||||
|
className={styles.titleWrapper} |
||||||
|
onClick={() => { |
||||||
|
setIsContentVisible(!isContentVisible); |
||||||
|
}} |
||||||
|
aria-label="Query operation row title" |
||||||
|
> |
||||||
|
<Icon name={isContentVisible ? 'angle-down' : 'angle-right'} className={styles.collapseIcon} /> |
||||||
|
{title && <span className={styles.title}>{titleElement}</span>} |
||||||
|
</div> |
||||||
|
{actions && <div>{actionsElement}</div>} |
||||||
|
</HorizontalGroup> |
||||||
|
</div> |
||||||
|
{isContentVisible && <div className={styles.content}>{children}</div>} |
||||||
|
</div> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
const getQueryOperationRowStyles = stylesFactory((theme: GrafanaTheme) => { |
||||||
|
const borderColor = theme.isLight ? theme.colors.gray85 : theme.colors.gray25; |
||||||
|
|
||||||
|
return { |
||||||
|
wrapper: css` |
||||||
|
margin-bottom: ${theme.spacing.formSpacingBase * 2}px; |
||||||
|
`,
|
||||||
|
header: css` |
||||||
|
padding: ${theme.spacing.sm}; |
||||||
|
border-radius: ${theme.border.radius.sm}; |
||||||
|
border: 1px solid ${borderColor}; |
||||||
|
background: ${theme.colors.pageBg}; |
||||||
|
`,
|
||||||
|
collapseIcon: css` |
||||||
|
color: ${theme.colors.textWeak}; |
||||||
|
`,
|
||||||
|
titleWrapper: css` |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
cursor: pointer; |
||||||
|
`,
|
||||||
|
|
||||||
|
title: css` |
||||||
|
font-weight: ${theme.typography.weight.semibold}; |
||||||
|
color: ${theme.colors.blue95}; |
||||||
|
margin-left: ${theme.spacing.sm}; |
||||||
|
`,
|
||||||
|
content: css` |
||||||
|
border: 1px solid ${borderColor}; |
||||||
|
margin-top: -1px; |
||||||
|
background: ${theme.colors.pageBg}; |
||||||
|
margin-left: ${theme.spacing.xl}; |
||||||
|
border-top: 1px solid ${theme.colors.pageBg}; |
||||||
|
border-radis: 0 ${theme.border.radius.sm}; |
||||||
|
padding: 0 ${theme.spacing.sm} ${theme.spacing.sm} ${theme.spacing.lg}; |
||||||
|
`,
|
||||||
|
}; |
||||||
|
}); |
||||||
|
|
||||||
|
QueryOperationRow.displayName = 'QueryOperationRow'; |
@ -0,0 +1,132 @@ |
|||||||
|
import React, { useContext } from 'react'; |
||||||
|
import { css } from 'emotion'; |
||||||
|
import { CustomScrollbar, Icon, JSONFormatter, ThemeContext } from '@grafana/ui'; |
||||||
|
import { GrafanaTheme, DataFrame } from '@grafana/data'; |
||||||
|
|
||||||
|
interface TransformationEditorProps { |
||||||
|
name: string; |
||||||
|
description: string; |
||||||
|
editor?: JSX.Element; |
||||||
|
input: DataFrame[]; |
||||||
|
output?: DataFrame[]; |
||||||
|
debugMode?: boolean; |
||||||
|
} |
||||||
|
|
||||||
|
export const TransformationEditor = ({ editor, input, output, debugMode }: TransformationEditorProps) => { |
||||||
|
const theme = useContext(ThemeContext); |
||||||
|
const styles = getStyles(theme); |
||||||
|
|
||||||
|
return ( |
||||||
|
<div> |
||||||
|
<div className={styles.editor}> |
||||||
|
{editor} |
||||||
|
{debugMode && ( |
||||||
|
<div className={styles.debugWrapper}> |
||||||
|
<div className={styles.debug}> |
||||||
|
<div className={styles.debugTitle}>Input</div> |
||||||
|
<div className={styles.debugJson}> |
||||||
|
<CustomScrollbar |
||||||
|
className={css` |
||||||
|
height: 100%; |
||||||
|
`}
|
||||||
|
> |
||||||
|
<JSONFormatter json={input} /> |
||||||
|
</CustomScrollbar> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div className={styles.debugSeparator}> |
||||||
|
<Icon name="arrow-right" /> |
||||||
|
</div> |
||||||
|
<div className={styles.debug}> |
||||||
|
<div className={styles.debugTitle}>Output</div> |
||||||
|
|
||||||
|
<div className={styles.debugJson}> |
||||||
|
<CustomScrollbar |
||||||
|
className={css` |
||||||
|
height: 100%; |
||||||
|
`}
|
||||||
|
> |
||||||
|
<JSONFormatter json={output} /> |
||||||
|
</CustomScrollbar> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
)} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme) => ({ |
||||||
|
title: css` |
||||||
|
display: flex; |
||||||
|
padding: 4px 8px 4px 8px; |
||||||
|
position: relative; |
||||||
|
height: 35px; |
||||||
|
background: ${theme.colors.textFaint}; |
||||||
|
border-radius: 4px 4px 0 0; |
||||||
|
flex-wrap: nowrap; |
||||||
|
justify-content: space-between; |
||||||
|
align-items: center; |
||||||
|
`,
|
||||||
|
name: css` |
||||||
|
font-weight: ${theme.typography.weight.semibold}; |
||||||
|
color: ${theme.colors.blue}; |
||||||
|
`,
|
||||||
|
iconRow: css` |
||||||
|
display: flex; |
||||||
|
`,
|
||||||
|
icon: css` |
||||||
|
background: transparent; |
||||||
|
border: none; |
||||||
|
box-shadow: none; |
||||||
|
cursor: pointer; |
||||||
|
color: ${theme.colors.textWeak}; |
||||||
|
margin-left: ${theme.spacing.sm}; |
||||||
|
&:hover { |
||||||
|
color: ${theme.colors.text}; |
||||||
|
} |
||||||
|
`,
|
||||||
|
editor: css` |
||||||
|
padding-top: ${theme.spacing.sm}; |
||||||
|
`,
|
||||||
|
debugWrapper: css` |
||||||
|
display: flex; |
||||||
|
flex-direction: row; |
||||||
|
`,
|
||||||
|
debugSeparator: css` |
||||||
|
width: 48px; |
||||||
|
height: 300px; |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
justify-content: center; |
||||||
|
margin: 0 ${theme.spacing.xs}; |
||||||
|
`,
|
||||||
|
debugTitle: css` |
||||||
|
padding: ${theme.spacing.xxs}; |
||||||
|
text-align: center; |
||||||
|
font-family: ${theme.typography.fontFamily.monospace}; |
||||||
|
font-size: ${theme.typography.size.sm}; |
||||||
|
color: ${theme.colors.blueBase}; |
||||||
|
border-bottom: 1px dashed ${theme.colors.gray15}; |
||||||
|
flex-grow: 0; |
||||||
|
flex-shrink: 1; |
||||||
|
`,
|
||||||
|
|
||||||
|
debug: css` |
||||||
|
margin-top: ${theme.spacing.md}; |
||||||
|
padding: 0 ${theme.spacing.sm} ${theme.spacing.sm} ${theme.spacing.sm}; |
||||||
|
border: 1px dashed ${theme.colors.gray15}; |
||||||
|
background: ${theme.colors.gray05}; |
||||||
|
border-radius: ${theme.border.radius.sm}; |
||||||
|
width: 100%; |
||||||
|
height: 300px; |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
`,
|
||||||
|
debugJson: css` |
||||||
|
flex-grow: 1; |
||||||
|
height: 100%; |
||||||
|
overflow: hidden; |
||||||
|
`,
|
||||||
|
}); |
@ -0,0 +1,44 @@ |
|||||||
|
import { DataFrame } from '@grafana/data'; |
||||||
|
import React, { useState } from 'react'; |
||||||
|
import { HorizontalGroup } from '@grafana/ui'; |
||||||
|
import { TransformationEditor } from './TransformationEditor'; |
||||||
|
import { QueryOperationRow } from 'app/core/components/QueryOperationRow/QueryOperationRow'; |
||||||
|
import { QueryOperationAction } from 'app/core/components/QueryOperationRow/QueryOperationAction'; |
||||||
|
|
||||||
|
interface TransformationOperationRowProps { |
||||||
|
name: string; |
||||||
|
description: string; |
||||||
|
editor?: JSX.Element; |
||||||
|
onRemove: () => void; |
||||||
|
input: DataFrame[]; |
||||||
|
output: DataFrame[]; |
||||||
|
} |
||||||
|
|
||||||
|
export const TransformationOperationRow: React.FC<TransformationOperationRowProps> = ({ |
||||||
|
children, |
||||||
|
onRemove, |
||||||
|
...props |
||||||
|
}) => { |
||||||
|
const [showDebug, setShowDebug] = useState(false); |
||||||
|
|
||||||
|
const renderActions = ({ isOpen }: { isOpen: boolean }) => { |
||||||
|
return ( |
||||||
|
<HorizontalGroup> |
||||||
|
<QueryOperationAction |
||||||
|
disabled={!isOpen} |
||||||
|
icon="bug" |
||||||
|
onClick={() => { |
||||||
|
setShowDebug(!showDebug); |
||||||
|
}} |
||||||
|
/> |
||||||
|
<QueryOperationAction icon="trash-alt" onClick={onRemove} /> |
||||||
|
</HorizontalGroup> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
return ( |
||||||
|
<QueryOperationRow title={props.name} actions={renderActions}> |
||||||
|
<TransformationEditor {...props} debugMode={showDebug} /> |
||||||
|
</QueryOperationRow> |
||||||
|
); |
||||||
|
}; |
@ -1,83 +0,0 @@ |
|||||||
import React, { useContext, useState } from 'react'; |
|
||||||
import { css } from 'emotion'; |
|
||||||
import { JSONFormatter, ThemeContext } from '@grafana/ui'; |
|
||||||
import { GrafanaTheme, DataFrame } from '@grafana/data'; |
|
||||||
|
|
||||||
interface TransformationRowProps { |
|
||||||
name: string; |
|
||||||
description: string; |
|
||||||
editor?: JSX.Element; |
|
||||||
onRemove: () => void; |
|
||||||
input: DataFrame[]; |
|
||||||
} |
|
||||||
|
|
||||||
export const TransformationRow = ({ onRemove, editor, name, input }: TransformationRowProps) => { |
|
||||||
const theme = useContext(ThemeContext); |
|
||||||
const [viewDebug, setViewDebug] = useState(false); |
|
||||||
const styles = getStyles(theme); |
|
||||||
return ( |
|
||||||
<div |
|
||||||
className={css` |
|
||||||
margin-bottom: 10px; |
|
||||||
`}
|
|
||||||
> |
|
||||||
<div className={styles.title}> |
|
||||||
<div className={styles.name}>{name}</div> |
|
||||||
<div className={styles.iconRow}> |
|
||||||
<div onClick={() => setViewDebug(!viewDebug)} className={styles.icon}> |
|
||||||
<i className="fa fa-fw fa-bug" /> |
|
||||||
</div> |
|
||||||
<div onClick={onRemove} className={styles.icon}> |
|
||||||
<i className="fa fa-fw fa-trash" /> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
<div className={styles.editor}> |
|
||||||
{editor} |
|
||||||
{viewDebug && ( |
|
||||||
<div> |
|
||||||
<JSONFormatter json={input} /> |
|
||||||
</div> |
|
||||||
)} |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
); |
|
||||||
}; |
|
||||||
|
|
||||||
const getStyles = (theme: GrafanaTheme) => ({ |
|
||||||
title: css` |
|
||||||
display: flex; |
|
||||||
padding: 4px 8px 4px 8px; |
|
||||||
position: relative; |
|
||||||
height: 35px; |
|
||||||
background: ${theme.colors.textFaint}; |
|
||||||
border-radius: 4px 4px 0 0; |
|
||||||
flex-wrap: nowrap; |
|
||||||
justify-content: space-between; |
|
||||||
align-items: center; |
|
||||||
`,
|
|
||||||
name: css` |
|
||||||
font-weight: ${theme.typography.weight.semibold}; |
|
||||||
color: ${theme.colors.blue}; |
|
||||||
`,
|
|
||||||
iconRow: css` |
|
||||||
display: flex; |
|
||||||
`,
|
|
||||||
icon: css` |
|
||||||
background: transparent; |
|
||||||
border: none; |
|
||||||
box-shadow: none; |
|
||||||
cursor: pointer; |
|
||||||
color: ${theme.colors.textWeak}; |
|
||||||
margin-left: ${theme.spacing.sm}; |
|
||||||
&:hover { |
|
||||||
color: ${theme.colors.text}; |
|
||||||
} |
|
||||||
`,
|
|
||||||
editor: css` |
|
||||||
border: 2px dashed ${theme.colors.textFaint}; |
|
||||||
border-top: none; |
|
||||||
border-radius: 0 0 4px 4px; |
|
||||||
padding: 8px; |
|
||||||
`,
|
|
||||||
}); |
|
@ -0,0 +1,72 @@ |
|||||||
|
import React from 'react'; |
||||||
|
import { css } from 'emotion'; |
||||||
|
import { DataQuery, DataSourceApi, GrafanaTheme } from '@grafana/data'; |
||||||
|
import { HorizontalGroup, stylesFactory, useTheme } from '@grafana/ui'; |
||||||
|
|
||||||
|
interface QueryEditorRowTitleProps { |
||||||
|
query: DataQuery; |
||||||
|
datasource: DataSourceApi; |
||||||
|
inMixedMode: boolean; |
||||||
|
disabled: boolean; |
||||||
|
onClick: (e: React.MouseEvent) => void; |
||||||
|
collapsedText: string; |
||||||
|
} |
||||||
|
|
||||||
|
export const QueryEditorRowTitle: React.FC<QueryEditorRowTitleProps> = ({ |
||||||
|
datasource, |
||||||
|
inMixedMode, |
||||||
|
disabled, |
||||||
|
query, |
||||||
|
onClick, |
||||||
|
collapsedText, |
||||||
|
}) => { |
||||||
|
const theme = useTheme(); |
||||||
|
const styles = getQueryEditorRowTitleStyles(theme); |
||||||
|
return ( |
||||||
|
<HorizontalGroup align="center"> |
||||||
|
<div className={styles.refId}> |
||||||
|
<span>{query.refId}</span> |
||||||
|
{inMixedMode && <em className={styles.contextInfo}> ({datasource.name})</em>} |
||||||
|
{disabled && <em className={styles.contextInfo}> Disabled</em>} |
||||||
|
</div> |
||||||
|
{collapsedText && ( |
||||||
|
<div className={styles.collapsedText} onClick={onClick}> |
||||||
|
{collapsedText} |
||||||
|
</div> |
||||||
|
)} |
||||||
|
</HorizontalGroup> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
const getQueryEditorRowTitleStyles = stylesFactory((theme: GrafanaTheme) => { |
||||||
|
return { |
||||||
|
refId: css` |
||||||
|
font-weight: ${theme.typography.weight.semibold}; |
||||||
|
color: ${theme.colors.blue95}; |
||||||
|
cursor: pointer; |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
`,
|
||||||
|
collapsedText: css` |
||||||
|
font-weight: ${theme.typography.weight.regular}; |
||||||
|
font-size: ${theme.typography.size.sm}; |
||||||
|
color: ${theme.colors.textWeak}; |
||||||
|
padding: 0 10px; |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
flex-grow: 1; |
||||||
|
overflow: hidden; |
||||||
|
font-style: italic; |
||||||
|
overflow: hidden; |
||||||
|
white-space: nowrap; |
||||||
|
text-overflow: ellipsis; |
||||||
|
min-width: 0; |
||||||
|
`,
|
||||||
|
contextInfo: css` |
||||||
|
font-size: ${theme.typography.size.sm}; |
||||||
|
font-style: italic; |
||||||
|
color: ${theme.colors.textWeak}; |
||||||
|
padding-left: 10px; |
||||||
|
`,
|
||||||
|
}; |
||||||
|
}); |
Loading…
Reference in new issue