mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
TagsInput: Design update and component refactor (#31163)
* TagsInput: Design update and component refactor * Update packages/grafana-ui/src/components/TagsInput/TagsInput.tsx Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com> * Update packages/grafana-ui/src/components/TagsInput/TagsInput.tsx Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com> * Update packages/grafana-ui/src/components/TagsInput/TagsInput.tsx Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com> * Update packages/grafana-ui/src/components/TagsInput/TagsInput.tsx Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com> * Updated Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com>
This commit is contained in:
@@ -13,21 +13,24 @@ interface Props {
|
|||||||
|
|
||||||
const getStyles = stylesFactory(({ theme, name }: { theme: GrafanaTheme; name: string }) => {
|
const getStyles = stylesFactory(({ theme, name }: { theme: GrafanaTheme; name: string }) => {
|
||||||
const { color, borderColor } = getTagColorsFromName(name);
|
const { color, borderColor } = getTagColorsFromName(name);
|
||||||
|
const height = theme.spacing.formInputHeight - 8;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
itemStyle: css`
|
itemStyle: css`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: ${height}px;
|
||||||
|
line-height: ${height - 2}px;
|
||||||
background-color: ${color};
|
background-color: ${color};
|
||||||
color: ${theme.palette.white};
|
color: ${theme.palette.white};
|
||||||
border: 1px solid ${borderColor};
|
border: 1px solid ${borderColor};
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
padding: 3px 6px;
|
padding: 0 ${theme.spacing.xs};
|
||||||
margin: 3px;
|
margin-right: 3px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-shadow: none;
|
text-shadow: none;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: ${theme.typography.size.sm};
|
font-size: ${theme.typography.size.sm};
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
`,
|
`,
|
||||||
|
|
||||||
nameStyle: css`
|
nameStyle: css`
|
||||||
@@ -36,6 +39,10 @@ const getStyles = stylesFactory(({ theme, name }: { theme: GrafanaTheme; name: s
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
* Only used internally by TagsInput
|
||||||
|
* */
|
||||||
export const TagItem: FC<Props> = ({ name, onRemove }) => {
|
export const TagItem: FC<Props> = ({ name, onRemove }) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const styles = getStyles({ theme, name });
|
const styles = getStyles({ theme, name });
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import { action } from '@storybook/addon-actions';
|
|
||||||
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
|
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
|
||||||
import { UseState } from '../../utils/storybook/UseState';
|
|
||||||
import { TagsInput } from '@grafana/ui';
|
import { TagsInput } from '@grafana/ui';
|
||||||
import mdx from './TagsInput.mdx';
|
import mdx from './TagsInput.mdx';
|
||||||
|
import { StoryExample } from '../../utils/storybook/StoryExample';
|
||||||
const mockTags = ['Some', 'Tags', 'With', 'This', 'New', 'Component'];
|
import { VerticalGroup } from '../Layout/Layout';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: 'Forms/TagsInput',
|
title: 'Forms/TagsInput',
|
||||||
@@ -18,16 +16,18 @@ export default {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const basic = () => {
|
export const Basic = () => {
|
||||||
return <TagsInput tags={[]} onChange={(tags) => action('tags updated')(tags)} />;
|
const [tags, setTags] = useState<string[]>([]);
|
||||||
|
return <TagsInput tags={tags} onChange={setTags} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const withMockTags = () => {
|
export const WithManyTags = () => {
|
||||||
|
const [tags, setTags] = useState<string[]>(['dashboard', 'prod', 'server', 'frontend', 'game', 'kubernetes']);
|
||||||
return (
|
return (
|
||||||
<UseState initialState={mockTags}>
|
<VerticalGroup>
|
||||||
{(tags) => {
|
<StoryExample name="With many tags">
|
||||||
return <TagsInput tags={tags} onChange={(tags) => action('tags updated')(tags)} />;
|
<TagsInput tags={tags} onChange={setTags} />
|
||||||
}}
|
</StoryExample>
|
||||||
</UseState>
|
</VerticalGroup>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,122 +1,87 @@
|
|||||||
import React, { ChangeEvent, KeyboardEvent, PureComponent } from 'react';
|
import React, { ChangeEvent, KeyboardEvent, FC, useState } from 'react';
|
||||||
import { css, cx } from 'emotion';
|
import { css } from 'emotion';
|
||||||
import { stylesFactory } from '../../themes/stylesFactory';
|
|
||||||
import { Button } from '../Button';
|
import { Button } from '../Button';
|
||||||
import { Input } from '../Forms/Legacy/Input/Input';
|
|
||||||
import { TagItem } from './TagItem';
|
import { TagItem } from './TagItem';
|
||||||
|
import { useStyles } from '../../themes/ThemeContext';
|
||||||
|
import { GrafanaTheme } from '@grafana/data';
|
||||||
|
import { Input } from '../Input/Input';
|
||||||
|
|
||||||
interface Props {
|
export interface Props {
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
|
|
||||||
onChange: (tags: string[]) => void;
|
onChange: (tags: string[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
export const TagsInput: FC<Props> = ({ placeholder = 'New tag (enter key to add)', tags = [], onChange }) => {
|
||||||
newTag: string;
|
const [newTagName, setNewName] = useState('');
|
||||||
tags: string[];
|
const styles = useStyles(getStyles);
|
||||||
}
|
|
||||||
|
|
||||||
export class TagsInput extends PureComponent<Props, State> {
|
const onNameChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
constructor(props: Props) {
|
setNewName(event.target.value);
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
newTag: '',
|
|
||||||
tags: this.props.tags || [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
onNameChange = (event: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
this.setState({
|
|
||||||
newTag: event.target.value,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onRemove = (tagToRemove: string) => {
|
const onRemove = (tagToRemove: string) => {
|
||||||
this.setState(
|
onChange(tags?.filter((x) => x !== tagToRemove));
|
||||||
(prevState: State) => ({
|
|
||||||
...prevState,
|
|
||||||
tags: prevState.tags.filter((tag) => tagToRemove !== tag),
|
|
||||||
}),
|
|
||||||
() => this.onChange()
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Using React.MouseEvent to avoid tslint error
|
const onAdd = (event: React.MouseEvent) => {
|
||||||
onAdd = (event: React.MouseEvent) => {
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (this.state.newTag !== '') {
|
onChange(tags.concat(newTagName));
|
||||||
this.setNewTags();
|
setNewName('');
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onKeyboardAdd = (event: KeyboardEvent) => {
|
const onKeyboardAdd = (event: KeyboardEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (event.key === 'Enter' && this.state.newTag !== '') {
|
if (event.key === 'Enter' && newTagName !== '') {
|
||||||
this.setNewTags();
|
onChange(tags.concat(newTagName));
|
||||||
|
setNewName('');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
setNewTags = () => {
|
return (
|
||||||
// We don't want to duplicate tags, clearing the input if
|
<div className={styles.wrapper}>
|
||||||
// the user is trying to add the same tag.
|
<div className={styles.tags}>
|
||||||
if (!this.state.tags.includes(this.state.newTag)) {
|
{tags?.map((tag: string, index: number) => {
|
||||||
this.setState(
|
return <TagItem key={`${tag}-${index}`} name={tag} onRemove={onRemove} />;
|
||||||
(prevState: State) => ({
|
})}
|
||||||
...prevState,
|
</div>
|
||||||
tags: [...prevState.tags, prevState.newTag],
|
<div>
|
||||||
newTag: '',
|
<Input
|
||||||
}),
|
placeholder={placeholder}
|
||||||
() => this.onChange()
|
onChange={onNameChange}
|
||||||
|
value={newTagName}
|
||||||
|
onKeyUp={onKeyboardAdd}
|
||||||
|
suffix={
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
className={styles.addButtonStyle}
|
||||||
|
onClick={onAdd}
|
||||||
|
size="md"
|
||||||
|
disabled={newTagName.length === 0}
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
} else {
|
};
|
||||||
this.setState({ newTag: '' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onChange = () => {
|
const getStyles = (theme: GrafanaTheme) => ({
|
||||||
this.props.onChange(this.state.tags);
|
wrapper: css`
|
||||||
};
|
height: ${theme.spacing.formInputHeight}px;
|
||||||
|
align-items: center;
|
||||||
render() {
|
display: flex;
|
||||||
const { placeholder = 'Add name' } = this.props;
|
flex-wrap: wrap;
|
||||||
const { tags, newTag } = this.state;
|
`,
|
||||||
|
tags: css`
|
||||||
const getStyles = stylesFactory(() => ({
|
|
||||||
tagsCloudStyle: css`
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
margin-right: ${theme.spacing.xs};
|
||||||
`,
|
`,
|
||||||
|
|
||||||
addButtonStyle: css`
|
addButtonStyle: css`
|
||||||
margin-left: 8px;
|
margin: 0 -${theme.spacing.sm};
|
||||||
`,
|
`,
|
||||||
}));
|
});
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="width-20">
|
|
||||||
<div
|
|
||||||
className={cx(
|
|
||||||
['gf-form-inline'],
|
|
||||||
css`
|
|
||||||
margin-bottom: 4px;
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Input placeholder={placeholder} onChange={this.onNameChange} value={newTag} onKeyUp={this.onKeyboardAdd} />
|
|
||||||
<Button className={getStyles().addButtonStyle} onClick={this.onAdd} variant="secondary" size="md">
|
|
||||||
Add
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className={getStyles().tagsCloudStyle}>
|
|
||||||
{tags &&
|
|
||||||
tags.map((tag: string, index: number) => {
|
|
||||||
return <TagItem key={`${tag}-${index}`} name={tag} onRemove={this.onRemove} />;
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export const GeneralSettings: React.FC<Props> = ({ dashboard }) => {
|
|||||||
<Field label="Description">
|
<Field label="Description">
|
||||||
<Input name="description" onBlur={onBlur} defaultValue={dashboard.description} />
|
<Input name="description" onBlur={onBlur} defaultValue={dashboard.description} />
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="Tags" description="Press enter to add a tag">
|
<Field label="Tags">
|
||||||
<TagsInput tags={dashboard.tags} onChange={onTagsChange} />
|
<TagsInput tags={dashboard.tags} onChange={onTagsChange} />
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="Folder">
|
<Field label="Folder">
|
||||||
|
|||||||
Reference in New Issue
Block a user