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