mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
AnnotationList: Adds spacing to UI (#31888)
* AnnotationList: Adds spacing to UI * Tests: updates snapshots * Chore: updates after PR comments
This commit is contained in:
parent
d59574ec1e
commit
9f92eb6859
@ -32,6 +32,7 @@ exports[`DashboardPage Dashboard init completed Should render dashboard grid 1`
|
||||
"_eventsCount": 0,
|
||||
},
|
||||
},
|
||||
"formatDate": [Function],
|
||||
"getVariables": [Function],
|
||||
"getVariablesFromState": [Function],
|
||||
"gnetId": null,
|
||||
@ -158,6 +159,7 @@ exports[`DashboardPage Dashboard init completed Should render dashboard grid 1`
|
||||
"_eventsCount": 0,
|
||||
},
|
||||
},
|
||||
"formatDate": [Function],
|
||||
"getVariables": [Function],
|
||||
"getVariablesFromState": [Function],
|
||||
"gnetId": null,
|
||||
@ -255,6 +257,7 @@ exports[`DashboardPage Dashboard init completed Should render dashboard grid 1`
|
||||
"_eventsCount": 0,
|
||||
},
|
||||
},
|
||||
"formatDate": [Function],
|
||||
"getVariables": [Function],
|
||||
"getVariablesFromState": [Function],
|
||||
"gnetId": null,
|
||||
@ -408,6 +411,7 @@ exports[`DashboardPage When dashboard has editview url state should render setti
|
||||
"_eventsCount": 0,
|
||||
},
|
||||
},
|
||||
"formatDate": [Function],
|
||||
"getVariables": [Function],
|
||||
"getVariablesFromState": [Function],
|
||||
"gnetId": null,
|
||||
@ -534,6 +538,7 @@ exports[`DashboardPage When dashboard has editview url state should render setti
|
||||
"_eventsCount": 0,
|
||||
},
|
||||
},
|
||||
"formatDate": [Function],
|
||||
"getVariables": [Function],
|
||||
"getVariablesFromState": [Function],
|
||||
"gnetId": null,
|
||||
@ -631,6 +636,7 @@ exports[`DashboardPage When dashboard has editview url state should render setti
|
||||
"_eventsCount": 0,
|
||||
},
|
||||
},
|
||||
"formatDate": [Function],
|
||||
"getVariables": [Function],
|
||||
"getVariablesFromState": [Function],
|
||||
"gnetId": null,
|
||||
@ -732,6 +738,7 @@ exports[`DashboardPage When dashboard has editview url state should render setti
|
||||
"_eventsCount": 0,
|
||||
},
|
||||
},
|
||||
"formatDate": [Function],
|
||||
"getVariables": [Function],
|
||||
"getVariablesFromState": [Function],
|
||||
"gnetId": null,
|
||||
|
@ -79,6 +79,7 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
|
||||
"_eventsCount": 1,
|
||||
},
|
||||
},
|
||||
"formatDate": [Function],
|
||||
"getVariables": [Function],
|
||||
"getVariablesFromState": [Function],
|
||||
"gnetId": null,
|
||||
@ -302,6 +303,7 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
|
||||
"_eventsCount": 1,
|
||||
},
|
||||
},
|
||||
"formatDate": [Function],
|
||||
"getVariables": [Function],
|
||||
"getVariablesFromState": [Function],
|
||||
"gnetId": null,
|
||||
@ -525,6 +527,7 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
|
||||
"_eventsCount": 1,
|
||||
},
|
||||
},
|
||||
"formatDate": [Function],
|
||||
"getVariables": [Function],
|
||||
"getVariablesFromState": [Function],
|
||||
"gnetId": null,
|
||||
@ -748,6 +751,7 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
|
||||
"_eventsCount": 1,
|
||||
},
|
||||
},
|
||||
"formatDate": [Function],
|
||||
"getVariables": [Function],
|
||||
"getVariablesFromState": [Function],
|
||||
"gnetId": null,
|
||||
|
@ -98,6 +98,7 @@ export class DashboardModel {
|
||||
panelInEdit: true,
|
||||
panelInView: true,
|
||||
getVariablesFromState: true,
|
||||
formatDate: true,
|
||||
};
|
||||
|
||||
constructor(data: any, meta?: DashboardMeta, private getVariablesFromState: GetVariables = getVariables) {
|
||||
@ -128,6 +129,7 @@ export class DashboardModel {
|
||||
this.links = data.links || [];
|
||||
this.gnetId = data.gnetId || null;
|
||||
this.panels = _.map(data.panels || [], (panelData: any) => new PanelModel(panelData));
|
||||
this.formatDate = this.formatDate.bind(this);
|
||||
|
||||
this.resetOriginalVariables(true);
|
||||
this.resetOriginalTime();
|
||||
|
264
public/app/plugins/panel/annolist/AnnoListPanel.test.tsx
Normal file
264
public/app/plugins/panel/annolist/AnnoListPanel.test.tsx
Normal file
@ -0,0 +1,264 @@
|
||||
import React from 'react';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
|
||||
import { AnnoListPanel, Props } from './AnnoListPanel';
|
||||
import { AnnotationEvent, FieldConfigSource, getDefaultTimeRange, LoadingState } from '@grafana/data';
|
||||
import { AnnoOptions } from './types';
|
||||
import { backendSrv } from '../../../core/services/backend_srv';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { silenceConsoleOutput } from '../../../../test/core/utils/silenceConsoleOutput';
|
||||
import { setDashboardSrv } from '../../../features/dashboard/services/DashboardSrv';
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...((jest.requireActual('@grafana/runtime') as unknown) as object),
|
||||
getBackendSrv: () => backendSrv,
|
||||
}));
|
||||
|
||||
const defaultOptions: AnnoOptions = {
|
||||
limit: 10,
|
||||
navigateAfter: '10m',
|
||||
navigateBefore: '20m',
|
||||
navigateToPanel: true,
|
||||
onlyFromThisDashboard: true,
|
||||
onlyInTimeRange: false,
|
||||
showTags: true,
|
||||
showTime: true,
|
||||
showUser: true,
|
||||
tags: ['tag A', 'tag B'],
|
||||
};
|
||||
|
||||
const defaultResult: AnnotationEvent = {
|
||||
text: 'Result text',
|
||||
userId: 1,
|
||||
login: 'Result login',
|
||||
email: 'Result email',
|
||||
avatarUrl: 'Result avatarUrl',
|
||||
tags: ['Result tag A', 'Result tag B'],
|
||||
time: Date.UTC(2021, 0, 1, 0, 0, 0, 0),
|
||||
panelId: 13,
|
||||
dashboardId: 14, // deliberately different from panelId
|
||||
};
|
||||
|
||||
async function setupTestContext({
|
||||
options = defaultOptions,
|
||||
results = [defaultResult],
|
||||
}: { options?: AnnoOptions; results?: AnnotationEvent[] } = {}) {
|
||||
jest.clearAllMocks();
|
||||
|
||||
const getMock = jest.spyOn(backendSrv, 'get');
|
||||
getMock.mockResolvedValue(results);
|
||||
|
||||
const dash: any = { id: 1, formatDate: (time: number) => new Date(time).toISOString() };
|
||||
const dashSrv: any = { getCurrent: () => dash };
|
||||
setDashboardSrv(dashSrv);
|
||||
|
||||
const props: Props = {
|
||||
data: { state: LoadingState.Done, timeRange: getDefaultTimeRange(), series: [] },
|
||||
eventBus: {
|
||||
getStream: jest.fn(),
|
||||
publish: jest.fn(),
|
||||
removeAllListeners: jest.fn(),
|
||||
subscribe: jest.fn(),
|
||||
},
|
||||
fieldConfig: ({} as unknown) as FieldConfigSource,
|
||||
height: 400,
|
||||
id: 1,
|
||||
onChangeTimeRange: jest.fn(),
|
||||
onFieldConfigChange: jest.fn(),
|
||||
onOptionsChange: jest.fn(),
|
||||
options,
|
||||
renderCounter: 1,
|
||||
replaceVariables: jest.fn(),
|
||||
timeRange: getDefaultTimeRange(),
|
||||
timeZone: 'utc',
|
||||
title: 'Test Title',
|
||||
transparent: false,
|
||||
width: 320,
|
||||
};
|
||||
const { rerender } = render(<AnnoListPanel {...props} />);
|
||||
await waitFor(() => expect(getMock).toHaveBeenCalledTimes(1));
|
||||
|
||||
return { props, rerender, getMock };
|
||||
}
|
||||
|
||||
describe('AnnoListPanel', () => {
|
||||
describe('when mounted', () => {
|
||||
it('then it should fetch annotations', async () => {
|
||||
const { getMock } = await setupTestContext();
|
||||
|
||||
expect(getMock).toHaveBeenCalledWith(
|
||||
'/api/annotations',
|
||||
{
|
||||
dashboardId: 1,
|
||||
limit: 10,
|
||||
tags: ['tag A', 'tag B'],
|
||||
type: 'annotation',
|
||||
},
|
||||
'anno-list-panel-1'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when there are no annotations', () => {
|
||||
it('then it should show a no annotations message', async () => {
|
||||
await setupTestContext({ results: [] });
|
||||
|
||||
expect(screen.getByText(/no annotations found/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when there are annotations', () => {
|
||||
it('then it renders the annotations correctly', async () => {
|
||||
await setupTestContext();
|
||||
|
||||
expect(screen.queryByText(/no annotations found/i)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(/result email/i)).not.toBeInTheDocument();
|
||||
expect(screen.getByText(/result text/i)).toBeInTheDocument();
|
||||
expect(screen.getByRole('img')).toBeInTheDocument();
|
||||
expect(screen.getByText('Result tag A')).toBeInTheDocument();
|
||||
expect(screen.getByText('Result tag B')).toBeInTheDocument();
|
||||
expect(screen.getByText(/2021-01-01T00:00:00.000Z/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('and login property is missing in annotation', () => {
|
||||
it('then it renders the annotations correctly', async () => {
|
||||
await setupTestContext({ results: [{ ...defaultResult, login: undefined }] });
|
||||
|
||||
expect(screen.queryByRole('img')).not.toBeInTheDocument();
|
||||
expect(screen.getByText(/result text/i)).toBeInTheDocument();
|
||||
expect(screen.getByText('Result tag A')).toBeInTheDocument();
|
||||
expect(screen.getByText('Result tag B')).toBeInTheDocument();
|
||||
expect(screen.getByText(/2021-01-01T00:00:00.000Z/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('and property is missing in annotation', () => {
|
||||
it('then it renders the annotations correctly', async () => {
|
||||
await setupTestContext({ results: [{ ...defaultResult, time: undefined }] });
|
||||
|
||||
expect(screen.queryByText(/2021-01-01T00:00:00.000Z/i)).not.toBeInTheDocument();
|
||||
expect(screen.getByText(/result text/i)).toBeInTheDocument();
|
||||
expect(screen.getByRole('img')).toBeInTheDocument();
|
||||
expect(screen.getByText('Result tag A')).toBeInTheDocument();
|
||||
expect(screen.getByText('Result tag B')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('and show user option is off', () => {
|
||||
it('then it renders the annotations correctly', async () => {
|
||||
await setupTestContext({
|
||||
options: { ...defaultOptions, showUser: false },
|
||||
});
|
||||
|
||||
expect(screen.queryByRole('img')).not.toBeInTheDocument();
|
||||
expect(screen.getByText(/result text/i)).toBeInTheDocument();
|
||||
expect(screen.getByText('Result tag A')).toBeInTheDocument();
|
||||
expect(screen.getByText('Result tag B')).toBeInTheDocument();
|
||||
expect(screen.getByText(/2021-01-01T00:00:00.000Z/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('and show time option is off', () => {
|
||||
it('then it renders the annotations correctly', async () => {
|
||||
await setupTestContext({
|
||||
options: { ...defaultOptions, showTime: false },
|
||||
});
|
||||
|
||||
expect(screen.queryByText(/2021-01-01T00:00:00.000Z/i)).not.toBeInTheDocument();
|
||||
expect(screen.getByText(/result text/i)).toBeInTheDocument();
|
||||
expect(screen.getByRole('img')).toBeInTheDocument();
|
||||
expect(screen.getByText('Result tag A')).toBeInTheDocument();
|
||||
expect(screen.getByText('Result tag B')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('and show tags option is off', () => {
|
||||
it('then it renders the annotations correctly', async () => {
|
||||
await setupTestContext({
|
||||
options: { ...defaultOptions, showTags: false },
|
||||
});
|
||||
|
||||
expect(screen.queryByText('Result tag A')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Result tag B')).not.toBeInTheDocument();
|
||||
expect(screen.getByText(/result text/i)).toBeInTheDocument();
|
||||
expect(screen.getByRole('img')).toBeInTheDocument();
|
||||
expect(screen.getByText(/2021-01-01T00:00:00.000Z/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('and the user clicks on the annotation', () => {
|
||||
it('then it should navigate to the dashboard connected to the annotation', async () => {
|
||||
const { getMock } = await setupTestContext();
|
||||
|
||||
getMock.mockClear();
|
||||
expect(screen.getByText(/result text/i)).toBeInTheDocument();
|
||||
userEvent.click(screen.getByText(/result text/i));
|
||||
|
||||
expect(getMock).toHaveBeenCalledTimes(1);
|
||||
expect(getMock).toHaveBeenCalledWith('/api/search', { dashboardIds: 14 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('and the user clicks on a tag', () => {
|
||||
it('then it should navigate to the dashboard connected to the annotation', async () => {
|
||||
const { getMock } = await setupTestContext();
|
||||
|
||||
getMock.mockClear();
|
||||
expect(screen.getByText('Result tag B')).toBeInTheDocument();
|
||||
userEvent.click(screen.getByText('Result tag B'));
|
||||
|
||||
expect(getMock).toHaveBeenCalledTimes(1);
|
||||
expect(getMock).toHaveBeenCalledWith(
|
||||
'/api/annotations',
|
||||
{
|
||||
dashboardId: 1,
|
||||
limit: 10,
|
||||
tags: ['tag A', 'tag B', 'Result tag B'],
|
||||
type: 'annotation',
|
||||
},
|
||||
'anno-list-panel-1'
|
||||
);
|
||||
expect(screen.getByText(/filter:/i)).toBeInTheDocument();
|
||||
expect(screen.getAllByText(/result tag b/i)).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('and the user clicks on the user avatar', () => {
|
||||
it('then it should filter annotations by login and the filter should show', async () => {
|
||||
const { getMock } = await setupTestContext();
|
||||
|
||||
getMock.mockClear();
|
||||
expect(screen.getByRole('img')).toBeInTheDocument();
|
||||
userEvent.click(screen.getByRole('img'));
|
||||
|
||||
expect(getMock).toHaveBeenCalledTimes(1);
|
||||
expect(getMock).toHaveBeenCalledWith(
|
||||
'/api/annotations',
|
||||
{
|
||||
dashboardId: 1,
|
||||
limit: 10,
|
||||
tags: ['tag A', 'tag B'],
|
||||
type: 'annotation',
|
||||
userId: 1,
|
||||
},
|
||||
'anno-list-panel-1'
|
||||
);
|
||||
expect(screen.getByText(/filter:/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/result email/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('and the user hovers over the user avatar', () => {
|
||||
silenceConsoleOutput(); // Popper throws an act error, but if we add act around the hover here it doesn't matter
|
||||
it('then it should filter annotations by login', async () => {
|
||||
const { getMock } = await setupTestContext();
|
||||
|
||||
getMock.mockClear();
|
||||
expect(screen.getByRole('img')).toBeInTheDocument();
|
||||
userEvent.hover(screen.getByRole('img'));
|
||||
|
||||
expect(screen.getByText(/result email/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -3,13 +3,12 @@ import React, { PureComponent } from 'react';
|
||||
// Types
|
||||
import { AnnoOptions } from './types';
|
||||
import { AnnotationEvent, AppEvents, dateTime, DurationUnit, PanelProps } from '@grafana/data';
|
||||
import { Tooltip } from '@grafana/ui';
|
||||
import { getBackendSrv, getLocationSrv } from '@grafana/runtime';
|
||||
import { AbstractList } from '@grafana/ui/src/components/List/AbstractList';
|
||||
import { TagBadge } from 'app/core/components/TagFilter/TagBadge';
|
||||
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { css, cx } from 'emotion';
|
||||
import { AnnotationListItem } from './AnnotationListItem';
|
||||
import { AnnotationListItemTags } from './AnnotationListItemTags';
|
||||
|
||||
interface UserInfo {
|
||||
id?: number;
|
||||
@ -17,7 +16,7 @@ interface UserInfo {
|
||||
email?: string;
|
||||
}
|
||||
|
||||
interface Props extends PanelProps<AnnoOptions> {}
|
||||
export interface Props extends PanelProps<AnnoOptions> {}
|
||||
interface State {
|
||||
annotations: AnnotationEvent[];
|
||||
timeInfo: string;
|
||||
@ -103,8 +102,7 @@ export class AnnoListPanel extends PureComponent<Props, State> {
|
||||
});
|
||||
}
|
||||
|
||||
onAnnoClick = (e: React.SyntheticEvent, anno: AnnotationEvent) => {
|
||||
e.stopPropagation();
|
||||
onAnnoClick = (anno: AnnotationEvent) => {
|
||||
if (!anno.time) {
|
||||
return;
|
||||
}
|
||||
@ -161,15 +159,13 @@ export class AnnoListPanel extends PureComponent<Props, State> {
|
||||
return t.add(incr, unit as DurationUnit).valueOf();
|
||||
}
|
||||
|
||||
onTagClick = (e: React.SyntheticEvent, tag: string, remove?: boolean) => {
|
||||
e.stopPropagation();
|
||||
onTagClick = (tag: string, remove?: boolean) => {
|
||||
const queryTags = remove ? this.state.queryTags.filter((item) => item !== tag) : [...this.state.queryTags, tag];
|
||||
|
||||
this.setState({ queryTags });
|
||||
};
|
||||
|
||||
onUserClick = (e: React.SyntheticEvent, anno: AnnotationEvent) => {
|
||||
e.stopPropagation();
|
||||
onUserClick = (anno: AnnotationEvent) => {
|
||||
this.setState({
|
||||
queryUser: {
|
||||
id: anno.userId,
|
||||
@ -186,73 +182,22 @@ export class AnnoListPanel extends PureComponent<Props, State> {
|
||||
};
|
||||
|
||||
renderTags = (tags?: string[], remove?: boolean): JSX.Element | null => {
|
||||
if (!tags || !tags.length) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{tags.map((tag) => {
|
||||
return (
|
||||
<span key={tag} onClick={(e) => this.onTagClick(e, tag, remove)} className="pointer">
|
||||
<TagBadge label={tag} removeIcon={!!remove} count={0} />
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
return <AnnotationListItemTags tags={tags} remove={remove} onClick={this.onTagClick} />;
|
||||
};
|
||||
|
||||
renderItem = (anno: AnnotationEvent, index: number): JSX.Element => {
|
||||
const { options } = this.props;
|
||||
const { showUser, showTags, showTime } = options;
|
||||
const dashboard = getDashboardSrv().getCurrent();
|
||||
|
||||
return (
|
||||
<div className="dashlist-item">
|
||||
<span
|
||||
className="dashlist-link pointer"
|
||||
onClick={(e) => {
|
||||
this.onAnnoClick(e, anno);
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className={cx([
|
||||
'dashlist-title',
|
||||
css`
|
||||
margin-right: 8px;
|
||||
`,
|
||||
])}
|
||||
>
|
||||
{anno.text}
|
||||
</span>
|
||||
|
||||
<span className="pluginlist-message">
|
||||
{anno.login && showUser && (
|
||||
<span className="graph-annotation">
|
||||
<Tooltip
|
||||
content={
|
||||
<span>
|
||||
Created by:
|
||||
<br /> {anno.email}
|
||||
</span>
|
||||
}
|
||||
theme="info"
|
||||
placement="top"
|
||||
>
|
||||
<span onClick={(e) => this.onUserClick(e, anno)} className="graph-annotation__user">
|
||||
<img src={anno.avatarUrl} />
|
||||
</span>
|
||||
</Tooltip>
|
||||
</span>
|
||||
)}
|
||||
{showTags && this.renderTags(anno.tags, false)}
|
||||
</span>
|
||||
|
||||
<span className="pluginlist-version">
|
||||
{showTime && anno.time && <span>{dashboard.formatDate(anno.time)}</span>}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<AnnotationListItem
|
||||
annotation={anno}
|
||||
formatDate={dashboard.formatDate}
|
||||
onClick={this.onAnnoClick}
|
||||
onAvatarClick={this.onUserClick}
|
||||
onTagClick={this.onTagClick}
|
||||
options={options}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
151
public/app/plugins/panel/annolist/AnnotationListItem.tsx
Normal file
151
public/app/plugins/panel/annolist/AnnotationListItem.tsx
Normal file
@ -0,0 +1,151 @@
|
||||
import React, { FC, MouseEvent } from 'react';
|
||||
import { css, cx } from 'emotion';
|
||||
import { AnnotationEvent, DateTimeInput, GrafanaTheme, PanelProps } from '@grafana/data';
|
||||
import { styleMixins, Tooltip, useStyles } from '@grafana/ui';
|
||||
import { AnnoOptions } from './types';
|
||||
import { AnnotationListItemTags } from './AnnotationListItemTags';
|
||||
|
||||
interface Props extends Pick<PanelProps<AnnoOptions>, 'options'> {
|
||||
annotation: AnnotationEvent;
|
||||
formatDate: (date: DateTimeInput, format?: string) => string;
|
||||
onClick: (annotation: AnnotationEvent) => void;
|
||||
onAvatarClick: (annotation: AnnotationEvent) => void;
|
||||
onTagClick: (tag: string, remove?: boolean) => void;
|
||||
}
|
||||
|
||||
export const AnnotationListItem: FC<Props> = ({
|
||||
options,
|
||||
annotation,
|
||||
formatDate,
|
||||
onClick,
|
||||
onAvatarClick,
|
||||
onTagClick,
|
||||
}) => {
|
||||
const styles = useStyles(getStyles);
|
||||
const { showUser, showTags, showTime } = options;
|
||||
const { text, login, email, avatarUrl, tags, time } = annotation;
|
||||
const onItemClick = (e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
onClick(annotation);
|
||||
};
|
||||
const onLoginClick = () => {
|
||||
onAvatarClick(annotation);
|
||||
};
|
||||
const showAvatar = login && showUser;
|
||||
const showTimeStamp = time && showTime;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span className={cx(styles.item, styles.link, styles.pointer)} onClick={onItemClick}>
|
||||
<div className={styles.title}>
|
||||
<span>{text}</span>
|
||||
{showTimeStamp ? <TimeStamp formatDate={formatDate} time={time!} /> : null}
|
||||
</div>
|
||||
<div className={styles.login}>
|
||||
{showAvatar ? <Avatar email={email} login={login!} avatarUrl={avatarUrl} onClick={onLoginClick} /> : null}
|
||||
{showTags ? <AnnotationListItemTags tags={tags} remove={false} onClick={onTagClick} /> : null}
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface AvatarProps {
|
||||
login: string;
|
||||
onClick: () => void;
|
||||
avatarUrl?: string;
|
||||
email?: string;
|
||||
}
|
||||
|
||||
const Avatar: FC<AvatarProps> = ({ onClick, avatarUrl, login, email }) => {
|
||||
const styles = useStyles(getStyles);
|
||||
const onAvatarClick = (e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
onClick();
|
||||
};
|
||||
const tooltipContent = (
|
||||
<span>
|
||||
Created by:
|
||||
<br /> {email}
|
||||
</span>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Tooltip content={tooltipContent} theme="info" placement="top">
|
||||
<span onClick={onAvatarClick} className={styles.avatar}>
|
||||
<img src={avatarUrl} alt="avatar icon" />
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface TimeStampProps {
|
||||
time: number;
|
||||
formatDate: (date: DateTimeInput, format?: string) => string;
|
||||
}
|
||||
|
||||
const TimeStamp: FC<TimeStampProps> = ({ time, formatDate }) => {
|
||||
const styles = useStyles(getStyles);
|
||||
|
||||
return (
|
||||
<span className={styles.time}>
|
||||
<span>{formatDate(time)}</span>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
function getStyles(theme: GrafanaTheme) {
|
||||
return {
|
||||
pointer: css`
|
||||
label: pointer;
|
||||
cursor: pointer;
|
||||
`,
|
||||
item: css`
|
||||
label: labelItem;
|
||||
margin: ${theme.spacing.xs};
|
||||
padding: ${theme.spacing.sm};
|
||||
${styleMixins.listItem(theme)}// display: flex;
|
||||
`,
|
||||
title: css`
|
||||
label: title;
|
||||
flex-basis: 80%;
|
||||
`,
|
||||
link: css`
|
||||
label: link;
|
||||
display: flex;
|
||||
|
||||
.fa {
|
||||
padding-top: ${theme.spacing.xs};
|
||||
}
|
||||
|
||||
.fa-star {
|
||||
color: ${theme.palette.orange};
|
||||
}
|
||||
`,
|
||||
login: css`
|
||||
label: login;
|
||||
align-self: center;
|
||||
flex: auto;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
font-size: ${theme.typography.size.sm};
|
||||
`,
|
||||
time: css`
|
||||
label: time;
|
||||
margin-left: ${theme.spacing.sm};
|
||||
font-size: ${theme.typography.size.sm};
|
||||
color: ${theme.colors.textWeak};
|
||||
`,
|
||||
avatar: css`
|
||||
label: avatar;
|
||||
padding: ${theme.spacing.xs};
|
||||
img {
|
||||
border-radius: 50%;
|
||||
width: ${theme.spacing.md};
|
||||
height: ${theme.spacing.md};
|
||||
}
|
||||
`,
|
||||
};
|
||||
}
|
50
public/app/plugins/panel/annolist/AnnotationListItemTags.tsx
Normal file
50
public/app/plugins/panel/annolist/AnnotationListItemTags.tsx
Normal file
@ -0,0 +1,50 @@
|
||||
import React, { FC, MouseEvent, useCallback } from 'react';
|
||||
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
import { css } from 'emotion';
|
||||
import { useStyles } from '@grafana/ui';
|
||||
|
||||
import { TagBadge } from '../../../core/components/TagFilter/TagBadge';
|
||||
|
||||
interface Props {
|
||||
tags?: string[];
|
||||
remove?: boolean;
|
||||
onClick: (tag: string, remove?: boolean) => void;
|
||||
}
|
||||
|
||||
export const AnnotationListItemTags: FC<Props> = ({ tags, remove, onClick }) => {
|
||||
const styles = useStyles(getStyles);
|
||||
const onTagClicked = useCallback(
|
||||
(e: MouseEvent, tag: string) => {
|
||||
e.stopPropagation();
|
||||
onClick(tag, remove);
|
||||
},
|
||||
[remove]
|
||||
);
|
||||
|
||||
if (!tags || !tags.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{tags.map((tag) => {
|
||||
return (
|
||||
<span key={tag} onClick={(e) => onTagClicked(e, tag)} className={styles.pointer}>
|
||||
<TagBadge label={tag} removeIcon={Boolean(remove)} count={0} />
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function getStyles(theme: GrafanaTheme) {
|
||||
return {
|
||||
pointer: css`
|
||||
label: pointer;
|
||||
cursor: pointer;
|
||||
padding: ${theme.spacing.xxs};
|
||||
`,
|
||||
};
|
||||
}
|
Loading…
Reference in New Issue
Block a user