mirror of
https://github.com/grafana/grafana.git
synced 2024-11-21 16:38:03 -06:00
Explore: Logs pinning in content outline (#88316)
* wip: working version * add delete buttons, put pinned logs on top, * Use already available onPinLine prop * cleanup * Fix alignment of pinned log * Limit to 3 pinned log lines * Format tooltip message * Lower to font size and adjust padding so pinned log title is fully visible * Add internationalization support in Explore Logs * Update json for i18n * Test remove button * Open content outline after pinning a log * Remove console.log statements * Minor changes * Conflict stuff
This commit is contained in:
parent
667fea6623
commit
2d370f3983
@ -3891,9 +3891,7 @@ exports[`better eslint`] = {
|
||||
],
|
||||
"public/app/features/explore/Logs/Logs.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "3"]
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"]
|
||||
],
|
||||
"public/app/features/explore/Logs/LogsFeedback.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
|
||||
|
@ -11,6 +11,8 @@ jest.mock('./ContentOutlineContext', () => ({
|
||||
const scrollIntoViewMock = jest.fn();
|
||||
const scrollerMock = document.createElement('div');
|
||||
|
||||
const unregisterMock = jest.fn();
|
||||
|
||||
const setup = (mergeSingleChild = false) => {
|
||||
HTMLElement.prototype.scrollIntoView = scrollIntoViewMock;
|
||||
|
||||
@ -50,6 +52,7 @@ const setup = (mergeSingleChild = false) => {
|
||||
title: 'Item 2-1',
|
||||
ref: document.createElement('div'),
|
||||
level: 'child',
|
||||
onRemove: () => unregisterMock('item-2-1'),
|
||||
},
|
||||
{
|
||||
id: 'item-2-2',
|
||||
@ -62,7 +65,7 @@ const setup = (mergeSingleChild = false) => {
|
||||
},
|
||||
],
|
||||
register: jest.fn(),
|
||||
unregister: jest.fn(),
|
||||
unregister: unregisterMock,
|
||||
});
|
||||
|
||||
return render(<ContentOutline scroller={scrollerMock} panelId="content-outline-container-1" />);
|
||||
@ -143,4 +146,16 @@ describe('<ContentOutline />', () => {
|
||||
await userEvent.click(button);
|
||||
expect(button.getAttribute('aria-controls')).toBe(sectionContent.id);
|
||||
});
|
||||
|
||||
it('deletes item on delete button click', async () => {
|
||||
setup();
|
||||
const expandSectionChevrons = screen.getAllByRole('button', { name: 'Content outline item collapse button' });
|
||||
// chevron for the second item
|
||||
const button = expandSectionChevrons[1];
|
||||
await userEvent.click(button);
|
||||
const deleteButtons = screen.getAllByTestId('content-outline-item-delete-button');
|
||||
await userEvent.click(deleteButtons[0]);
|
||||
|
||||
expect(unregisterMock).toHaveBeenCalledWith('item-2-1');
|
||||
});
|
||||
});
|
||||
|
@ -47,9 +47,11 @@ export function ContentOutline({ scroller, panelId }: { scroller: HTMLElement |
|
||||
(item) => item.children && !(item.mergeSingleChild && item.children?.length === 1) && item.children.length > 0
|
||||
);
|
||||
|
||||
const outlineItemsHaveDeleteButton = outlineItems.some((item) => item.children?.some((child) => child.onRemove));
|
||||
|
||||
const [sectionsExpanded, setSectionsExpanded] = useState(() => {
|
||||
return outlineItems.reduce((acc: { [key: string]: boolean }, item) => {
|
||||
acc[item.id] = false;
|
||||
acc[item.id] = !!item.expanded;
|
||||
return acc;
|
||||
}, {});
|
||||
});
|
||||
@ -58,6 +60,10 @@ export function ContentOutline({ scroller, panelId }: { scroller: HTMLElement |
|
||||
let scrollValue = 0;
|
||||
let el: HTMLElement | null | undefined = ref;
|
||||
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
|
||||
do {
|
||||
scrollValue += el?.offsetTop || 0;
|
||||
el = el?.offsetParent instanceof HTMLElement ? el.offsetParent : undefined;
|
||||
@ -158,7 +164,7 @@ export function ContentOutline({ scroller, panelId }: { scroller: HTMLElement |
|
||||
title={contentOutlineExpanded ? item.title : undefined}
|
||||
contentOutlineExpanded={contentOutlineExpanded}
|
||||
className={cx(styles.buttonStyles, {
|
||||
[styles.justifyCenter]: !contentOutlineExpanded,
|
||||
[styles.justifyCenter]: !contentOutlineExpanded && !outlineItemsHaveDeleteButton,
|
||||
[styles.sectionHighlighter]: isChildActive(item, activeSectionChildId) && !contentOutlineExpanded,
|
||||
})}
|
||||
indentStyle={cx({
|
||||
@ -196,7 +202,7 @@ export function ContentOutline({ scroller, panelId }: { scroller: HTMLElement |
|
||||
contentOutlineExpanded={contentOutlineExpanded}
|
||||
icon={contentOutlineExpanded ? undefined : item.icon}
|
||||
className={cx(styles.buttonStyles, {
|
||||
[styles.justifyCenter]: !contentOutlineExpanded,
|
||||
[styles.justifyCenter]: !contentOutlineExpanded && !outlineItemsHaveDeleteButton,
|
||||
[styles.sectionHighlighter]:
|
||||
isChildActive(item, activeSectionChildId) && !contentOutlineExpanded,
|
||||
})}
|
||||
@ -211,6 +217,7 @@ export function ContentOutline({ scroller, panelId }: { scroller: HTMLElement |
|
||||
isActive={shouldBeActive(child, activeSectionId, activeSectionChildId, sectionsExpanded)}
|
||||
extraHighlight={child.highlight}
|
||||
color={child.color}
|
||||
onRemove={child.onRemove ? () => child.onRemove?.(child.id) : undefined}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
@ -257,10 +264,10 @@ const getStyles = (theme: GrafanaTheme2, expanded: boolean) => {
|
||||
marginRight: expanded ? theme.spacing(0.5) : undefined,
|
||||
}),
|
||||
indentRoot: css({
|
||||
paddingLeft: theme.spacing(4),
|
||||
paddingLeft: theme.spacing(3),
|
||||
}),
|
||||
indentChild: css({
|
||||
paddingLeft: expanded ? theme.spacing(7) : theme.spacing(4),
|
||||
paddingLeft: expanded ? theme.spacing(5) : theme.spacing(2.75),
|
||||
}),
|
||||
itemWrapper: css({
|
||||
display: 'flex',
|
||||
@ -275,7 +282,7 @@ const getStyles = (theme: GrafanaTheme2, expanded: boolean) => {
|
||||
borderRight: `1px solid ${theme.colors.border.medium}`,
|
||||
content: '""',
|
||||
height: '100%',
|
||||
left: 48,
|
||||
left: theme.spacing(4.75),
|
||||
position: 'absolute',
|
||||
transform: 'translateX(50%)',
|
||||
},
|
||||
|
@ -18,6 +18,7 @@ export interface ContentOutlineContextProps {
|
||||
unregister: (id: string) => void;
|
||||
unregisterAllChildren: (parentId: string, childType: ITEM_TYPES) => void;
|
||||
updateOutlineItems: (newItems: ContentOutlineItemContextProps[]) => void;
|
||||
updateItem: (id: string, properties: Partial<Omit<ContentOutlineItemContextProps, 'id'>>) => void;
|
||||
}
|
||||
|
||||
interface ContentOutlineContextProviderProps {
|
||||
@ -141,8 +142,11 @@ export function ContentOutlineContextProvider({ children, refreshDependencies }:
|
||||
ref = parent.ref;
|
||||
}
|
||||
|
||||
const childrenUpdated = [...(parent.children || []), { ...outlineItem, id, ref }];
|
||||
childrenUpdated.sort(sortElementsByDocumentPosition);
|
||||
let childrenUpdated = [{ ...outlineItem, id, ref }, ...(parent.children || [])];
|
||||
|
||||
if (!outlineItem.childOnTop) {
|
||||
childrenUpdated = sortItems(childrenUpdated);
|
||||
}
|
||||
|
||||
newItems[parentIndex] = {
|
||||
...parent,
|
||||
@ -175,6 +179,20 @@ export function ContentOutlineContextProvider({ children, refreshDependencies }:
|
||||
setOutlineItems(newItems);
|
||||
}, []);
|
||||
|
||||
const updateItem = useCallback((id: string, properties: Partial<Omit<ContentOutlineItemContextProps, 'id'>>) => {
|
||||
setOutlineItems((prevItems) =>
|
||||
prevItems.map((item) => {
|
||||
if (item.id === id) {
|
||||
return {
|
||||
...item,
|
||||
...properties,
|
||||
};
|
||||
}
|
||||
return item;
|
||||
})
|
||||
);
|
||||
}, []);
|
||||
|
||||
const unregisterAllChildren = useCallback((parentId: string, childType: ITEM_TYPES) => {
|
||||
setOutlineItems((prevItems) =>
|
||||
prevItems.map((item) => {
|
||||
@ -190,7 +208,8 @@ export function ContentOutlineContextProvider({ children, refreshDependencies }:
|
||||
setOutlineItems((prevItems) => {
|
||||
const newItems = [...prevItems];
|
||||
for (const item of newItems) {
|
||||
item.children?.sort(sortElementsByDocumentPosition);
|
||||
const sortedItems = sortItems(item.children || []);
|
||||
item.children = sortedItems;
|
||||
}
|
||||
return newItems;
|
||||
});
|
||||
@ -198,14 +217,14 @@ export function ContentOutlineContextProvider({ children, refreshDependencies }:
|
||||
|
||||
return (
|
||||
<ContentOutlineContext.Provider
|
||||
value={{ outlineItems, register, unregister, updateOutlineItems, unregisterAllChildren }}
|
||||
value={{ outlineItems, register, unregister, updateOutlineItems, unregisterAllChildren, updateItem }}
|
||||
>
|
||||
{children}
|
||||
</ContentOutlineContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function sortElementsByDocumentPosition(a: ContentOutlineItemContextProps, b: ContentOutlineItemContextProps) {
|
||||
function sortElementsByDocumentPosition(a: ContentOutlineItemContextProps, b: ContentOutlineItemContextProps) {
|
||||
if (a.ref && b.ref) {
|
||||
const diff = a.ref.compareDocumentPosition(b.ref);
|
||||
if (diff === Node.DOCUMENT_POSITION_PRECEDING) {
|
||||
@ -217,6 +236,22 @@ export function sortElementsByDocumentPosition(a: ContentOutlineItemContextProps
|
||||
return 0;
|
||||
}
|
||||
|
||||
function sortItems(outlineItems: ContentOutlineItemContextProps[]): ContentOutlineItemContextProps[] {
|
||||
const [skipSort, sortable] = outlineItems.reduce<
|
||||
[ContentOutlineItemContextProps[], ContentOutlineItemContextProps[]]
|
||||
>(
|
||||
(acc, item) => {
|
||||
item.childOnTop ? acc[0].push(item) : acc[1].push(item);
|
||||
return acc;
|
||||
},
|
||||
[[], []]
|
||||
);
|
||||
|
||||
sortable.sort(sortElementsByDocumentPosition);
|
||||
|
||||
return [...skipSort, ...sortable];
|
||||
}
|
||||
|
||||
export function useContentOutlineContext() {
|
||||
return useContext(ContentOutlineContext);
|
||||
}
|
||||
|
@ -35,6 +35,13 @@ export interface ContentOutlineItemBaseProps {
|
||||
* Client can additionally mark filter actions as highlighted
|
||||
*/
|
||||
highlight?: boolean;
|
||||
onRemove?: (id: string) => void;
|
||||
/**
|
||||
* Child that will always be on top of the list
|
||||
* e.g. pinned log in Logs section
|
||||
*/
|
||||
childOnTop?: boolean;
|
||||
expanded?: boolean;
|
||||
}
|
||||
|
||||
interface ContentOutlineItemProps extends ContentOutlineItemBaseProps {
|
||||
|
@ -2,7 +2,7 @@ import { cx, css } from '@emotion/css';
|
||||
import React, { ButtonHTMLAttributes, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { IconName, isIconName, GrafanaTheme2 } from '@grafana/data';
|
||||
import { Icon, Tooltip, useTheme2 } from '@grafana/ui';
|
||||
import { Button, Icon, Tooltip, useTheme2 } from '@grafana/ui';
|
||||
import { TooltipPlacement } from '@grafana/ui/src/components/Tooltip';
|
||||
|
||||
type CommonProps = {
|
||||
@ -20,6 +20,7 @@ type CommonProps = {
|
||||
sectionId?: string;
|
||||
toggleCollapsed?: () => void;
|
||||
color?: string;
|
||||
onRemove?: () => void;
|
||||
};
|
||||
|
||||
export type ContentOutlineItemButtonProps = CommonProps & ButtonHTMLAttributes<HTMLButtonElement>;
|
||||
@ -39,6 +40,7 @@ export function ContentOutlineItemButton({
|
||||
sectionId,
|
||||
toggleCollapsed,
|
||||
color,
|
||||
onRemove,
|
||||
...rest
|
||||
}: ContentOutlineItemButtonProps) {
|
||||
const theme = useTheme2();
|
||||
@ -83,6 +85,15 @@ export function ContentOutlineItemButton({
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
{onRemove && (
|
||||
<Button
|
||||
variant="destructive"
|
||||
className={styles.deleteButton}
|
||||
icon="times"
|
||||
onClick={() => onRemove()}
|
||||
data-testid="content-outline-item-delete-button"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -117,20 +128,20 @@ const getStyles = (theme: GrafanaTheme2, color?: string) => {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexGrow: 1,
|
||||
gap: theme.spacing(1),
|
||||
overflow: 'hidden',
|
||||
gap: theme.spacing(0.25),
|
||||
width: '100%',
|
||||
overflow: 'hidden',
|
||||
}),
|
||||
button: css({
|
||||
label: 'content-outline-item-button',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
height: theme.spacing(theme.components.height.md),
|
||||
padding: theme.spacing(0, 1),
|
||||
gap: theme.spacing(1),
|
||||
gap: theme.spacing(0.5),
|
||||
color: theme.colors.text.secondary,
|
||||
width: '100%',
|
||||
background: 'transparent',
|
||||
overflow: 'hidden',
|
||||
border: 'none',
|
||||
}),
|
||||
collapseButton: css({
|
||||
@ -143,6 +154,7 @@ const getStyles = (theme: GrafanaTheme2, color?: string) => {
|
||||
color: theme.colors.text.secondary,
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
overflow: 'hidden',
|
||||
|
||||
'&:hover': {
|
||||
color: theme.colors.text.primary,
|
||||
@ -153,6 +165,8 @@ const getStyles = (theme: GrafanaTheme2, color?: string) => {
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
fontSize: theme.typography.bodySmall.fontSize,
|
||||
marginLeft: theme.spacing(0.5),
|
||||
}),
|
||||
active: css({
|
||||
backgroundColor: theme.colors.background.secondary,
|
||||
@ -193,5 +207,11 @@ const getStyles = (theme: GrafanaTheme2, color?: string) => {
|
||||
left: '2px',
|
||||
},
|
||||
}),
|
||||
deleteButton: css({
|
||||
width: theme.spacing(1),
|
||||
height: theme.spacing(1),
|
||||
padding: theme.spacing(0.75, 0.75),
|
||||
marginRight: theme.spacing(0.5),
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
@ -438,6 +438,9 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
|
||||
isFilterLabelActive={this.isFilterLabelActive}
|
||||
onClickFilterString={this.onClickFilterString}
|
||||
onClickFilterOutString={this.onClickFilterOutString}
|
||||
onPinLineCallback={() => {
|
||||
this.setState({ contentOutlineVisible: true });
|
||||
}}
|
||||
/>
|
||||
</ContentOutlineItem>
|
||||
);
|
||||
|
@ -39,12 +39,14 @@ import {
|
||||
InlineFieldRow,
|
||||
InlineSwitch,
|
||||
PanelChrome,
|
||||
PopoverContent,
|
||||
RadioButtonGroup,
|
||||
SeriesVisibilityChangeMode,
|
||||
Themeable2,
|
||||
withTheme2,
|
||||
} from '@grafana/ui';
|
||||
import { mapMouseEventToMode } from '@grafana/ui/src/components/VizLegend/utils';
|
||||
import { Trans } from 'app/core/internationalization';
|
||||
import store from 'app/core/store';
|
||||
import { createAndCopyShortLink } from 'app/core/utils/shortLinks';
|
||||
import { InfiniteScroll } from 'app/features/logs/components/InfiniteScroll';
|
||||
@ -112,6 +114,7 @@ interface Props extends Themeable2 {
|
||||
onClickFilterString?: (value: string, refId?: string) => void;
|
||||
onClickFilterOutString?: (value: string, refId?: string) => void;
|
||||
loadMoreLogs?(range: AbsoluteTimeRange): void;
|
||||
onPinLineCallback?: () => void;
|
||||
}
|
||||
|
||||
export type LogsVisualisationType = 'table' | 'logs';
|
||||
@ -132,6 +135,7 @@ interface State {
|
||||
tableFrame?: DataFrame;
|
||||
visualisationType?: LogsVisualisationType;
|
||||
logsContainer?: HTMLDivElement;
|
||||
pinLineButtonTooltipTitle?: PopoverContent;
|
||||
}
|
||||
|
||||
// we need to define the order of these explicitly
|
||||
@ -156,6 +160,8 @@ const getDefaultVisualisationType = (): LogsVisualisationType => {
|
||||
return 'logs';
|
||||
};
|
||||
|
||||
const PINNED_LOGS_LIMIT = 3;
|
||||
|
||||
class UnthemedLogs extends PureComponent<Props, State> {
|
||||
flipOrderTimer?: number;
|
||||
cancelFlippingTimer?: number;
|
||||
@ -183,6 +189,7 @@ class UnthemedLogs extends PureComponent<Props, State> {
|
||||
tableFrame: undefined,
|
||||
visualisationType: this.props.panelState?.logs?.visualisationType ?? getDefaultVisualisationType(),
|
||||
logsContainer: undefined,
|
||||
pinLineButtonTooltipTitle: 'Pin to content outline',
|
||||
};
|
||||
|
||||
constructor(props: Props) {
|
||||
@ -652,6 +659,56 @@ class UnthemedLogs extends PureComponent<Props, State> {
|
||||
this.topLogsRef.current?.scrollIntoView();
|
||||
};
|
||||
|
||||
onPinToContentOutlineClick = (row: LogRowModel) => {
|
||||
if (this.getPinnedLogsCount() === PINNED_LOGS_LIMIT) {
|
||||
this.setState({
|
||||
pinLineButtonTooltipTitle: (
|
||||
<span style={{ display: 'flex', textAlign: 'center' }}>
|
||||
❗️
|
||||
<Trans i18nKey="explore.logs.maximum-pinned-logs">
|
||||
Maximum of {{ PINNED_LOGS_LIMIT }} pinned logs reached. Unpin a log to add another.
|
||||
</Trans>
|
||||
</span>
|
||||
),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// find the Logs parent item
|
||||
const logsParent = this.context?.outlineItems.find((item) => item.panelId === 'Logs' && item.level === 'root');
|
||||
|
||||
//update the parent's expanded state
|
||||
if (logsParent) {
|
||||
this.context?.updateItem(logsParent.id, { expanded: true });
|
||||
}
|
||||
|
||||
this.context?.register({
|
||||
icon: 'gf-logs',
|
||||
title: 'Pinned log',
|
||||
panelId: 'Logs',
|
||||
level: 'child',
|
||||
ref: null,
|
||||
color: LogLevelColor[row.logLevel],
|
||||
childOnTop: true,
|
||||
onClick: () => this.onOpenContext(row, () => {}),
|
||||
onRemove: (id: string) => {
|
||||
this.context?.unregister(id);
|
||||
if (this.getPinnedLogsCount() < PINNED_LOGS_LIMIT) {
|
||||
this.setState({
|
||||
pinLineButtonTooltipTitle: 'Pin to content outline',
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
this.props.onPinLineCallback?.();
|
||||
};
|
||||
|
||||
getPinnedLogsCount = () => {
|
||||
const logsParent = this.context?.outlineItems.find((item) => item.panelId === 'Logs' && item.level === 'root');
|
||||
return logsParent?.children?.filter((child) => child.title === 'Pinned log').length ?? 0;
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
width,
|
||||
@ -945,6 +1002,8 @@ class UnthemedLogs extends PureComponent<Props, State> {
|
||||
containerRendered={!!this.state.logsContainer}
|
||||
onClickFilterString={this.props.onClickFilterString}
|
||||
onClickFilterOutString={this.props.onClickFilterOutString}
|
||||
onPinLine={this.onPinToContentOutlineClick}
|
||||
pinLineButtonTooltipTitle={this.state.pinLineButtonTooltipTitle}
|
||||
/>
|
||||
</InfiniteScroll>
|
||||
</div>
|
||||
@ -952,9 +1011,9 @@ class UnthemedLogs extends PureComponent<Props, State> {
|
||||
{!loading && !hasData && !scanning && (
|
||||
<div className={styles.logRows}>
|
||||
<div className={styles.noData}>
|
||||
No logs found.
|
||||
<Trans i18nKey="explore.logs.no-logs-found">No logs found.</Trans>
|
||||
<Button size="sm" variant="secondary" onClick={this.onClickScan}>
|
||||
Scan for older logs
|
||||
<Trans i18nKey="explore.logs.scan-for-older-logs">Scan for older logs</Trans>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@ -964,7 +1023,7 @@ class UnthemedLogs extends PureComponent<Props, State> {
|
||||
<div className={styles.noData}>
|
||||
<span>{scanText}</span>
|
||||
<Button size="sm" variant="secondary" onClick={this.onClickStopScan}>
|
||||
Stop scan
|
||||
<Trans i18nKey="explore.logs.stop-scan">Stop scan</Trans>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -60,6 +60,7 @@ interface LogsContainerProps extends PropsFromRedux {
|
||||
isFilterLabelActive: (key: string, value: string, refId?: string) => Promise<boolean>;
|
||||
onClickFilterString: (value: string, refId?: string) => void;
|
||||
onClickFilterOutString: (value: string, refId?: string) => void;
|
||||
onPinLineCallback?: () => void;
|
||||
}
|
||||
|
||||
type DataSourceInstance =
|
||||
@ -282,6 +283,7 @@ class LogsContainer extends PureComponent<LogsContainerProps, LogsContainerState
|
||||
exploreId,
|
||||
logsVolume,
|
||||
scrollElement,
|
||||
onPinLineCallback,
|
||||
} = this.props;
|
||||
|
||||
if (!logRows) {
|
||||
@ -350,6 +352,7 @@ class LogsContainer extends PureComponent<LogsContainerProps, LogsContainerState
|
||||
scrollElement={scrollElement}
|
||||
isFilterLabelActive={this.logDetailsFilterAvailable() ? this.props.isFilterLabelActive : undefined}
|
||||
range={range}
|
||||
onPinLineCallback={onPinLineCallback}
|
||||
onClickFilterString={this.filterValueAvailable() ? this.props.onClickFilterString : undefined}
|
||||
onClickFilterOutString={this.filterOutValueAvailable() ? this.props.onClickFilterOutString : undefined}
|
||||
/>
|
||||
|
@ -15,7 +15,7 @@ import {
|
||||
} from '@grafana/data';
|
||||
import { reportInteraction } from '@grafana/runtime';
|
||||
import { DataQuery, TimeZone } from '@grafana/schema';
|
||||
import { withTheme2, Themeable2, Icon, Tooltip } from '@grafana/ui';
|
||||
import { withTheme2, Themeable2, Icon, Tooltip, PopoverContent } from '@grafana/ui';
|
||||
|
||||
import { checkLogsError, escapeUnescapedString } from '../utils';
|
||||
|
||||
@ -60,6 +60,7 @@ interface Props extends Themeable2 {
|
||||
isFilterLabelActive?: (key: string, value: string, refId?: string) => Promise<boolean>;
|
||||
onPinLine?: (row: LogRowModel) => void;
|
||||
onUnpinLine?: (row: LogRowModel) => void;
|
||||
pinLineButtonTooltipTitle?: PopoverContent;
|
||||
pinned?: boolean;
|
||||
containerRendered?: boolean;
|
||||
handleTextSelection?: (e: MouseEvent<HTMLTableRowElement>, row: LogRowModel) => boolean;
|
||||
@ -305,6 +306,7 @@ class UnThemedLogRow extends PureComponent<Props, State> {
|
||||
styles={styles}
|
||||
onPinLine={this.props.onPinLine}
|
||||
onUnpinLine={this.props.onUnpinLine}
|
||||
pinLineButtonTooltipTitle={this.props.pinLineButtonTooltipTitle}
|
||||
pinned={this.props.pinned}
|
||||
mouseIsOver={this.state.mouseIsOver}
|
||||
onBlur={this.onMouseLeave}
|
||||
|
@ -2,7 +2,7 @@ import React, { FocusEvent, SyntheticEvent, useCallback } from 'react';
|
||||
|
||||
import { LogRowContextOptions, LogRowModel, getDefaultTimeRange, locationUtil, urlUtil } from '@grafana/data';
|
||||
import { DataQuery } from '@grafana/schema';
|
||||
import { ClipboardButton, IconButton } from '@grafana/ui';
|
||||
import { ClipboardButton, IconButton, PopoverContent } from '@grafana/ui';
|
||||
import { getConfig } from 'app/core/config';
|
||||
|
||||
import { LogRowStyles } from './getLogRowStyles';
|
||||
@ -20,10 +20,12 @@ interface Props {
|
||||
onPermalinkClick?: (row: LogRowModel) => Promise<void>;
|
||||
onPinLine?: (row: LogRowModel) => void;
|
||||
onUnpinLine?: (row: LogRowModel) => void;
|
||||
pinLineButtonTooltipTitle?: PopoverContent;
|
||||
pinned?: boolean;
|
||||
styles: LogRowStyles;
|
||||
mouseIsOver: boolean;
|
||||
onBlur: () => void;
|
||||
onPinToContentOutlineClick?: (row: LogRowModel, onOpenContext: (row: LogRowModel) => void) => void;
|
||||
}
|
||||
|
||||
export const LogRowMenuCell = React.memo(
|
||||
@ -33,6 +35,7 @@ export const LogRowMenuCell = React.memo(
|
||||
onPermalinkClick,
|
||||
onPinLine,
|
||||
onUnpinLine,
|
||||
pinLineButtonTooltipTitle,
|
||||
pinned,
|
||||
row,
|
||||
showContextToggle,
|
||||
@ -145,7 +148,7 @@ export const LogRowMenuCell = React.memo(
|
||||
size="md"
|
||||
name="gf-pin"
|
||||
onClick={() => onPinLine && onPinLine(row)}
|
||||
tooltip="Pin line"
|
||||
tooltip={pinLineButtonTooltipTitle ?? 'Pin line'}
|
||||
tooltipPlacement="top"
|
||||
aria-label="Pin line"
|
||||
tabIndex={0}
|
||||
|
@ -3,6 +3,7 @@ import Highlighter from 'react-highlight-words';
|
||||
|
||||
import { CoreApp, findHighlightChunksInText, LogRowContextOptions, LogRowModel } from '@grafana/data';
|
||||
import { DataQuery } from '@grafana/schema';
|
||||
import { PopoverContent } from '@grafana/ui';
|
||||
|
||||
import { LogMessageAnsi } from './LogMessageAnsi';
|
||||
import { LogRowMenuCell } from './LogRowMenuCell';
|
||||
@ -25,6 +26,7 @@ interface Props {
|
||||
onPermalinkClick?: (row: LogRowModel) => Promise<void>;
|
||||
onPinLine?: (row: LogRowModel) => void;
|
||||
onUnpinLine?: (row: LogRowModel) => void;
|
||||
pinLineButtonTooltipTitle?: PopoverContent;
|
||||
pinned?: boolean;
|
||||
styles: LogRowStyles;
|
||||
mouseIsOver: boolean;
|
||||
@ -88,6 +90,7 @@ export const LogRowMessage = React.memo((props: Props) => {
|
||||
onPermalinkClick,
|
||||
onUnpinLine,
|
||||
onPinLine,
|
||||
pinLineButtonTooltipTitle,
|
||||
pinned,
|
||||
mouseIsOver,
|
||||
onBlur,
|
||||
@ -124,6 +127,7 @@ export const LogRowMessage = React.memo((props: Props) => {
|
||||
onPermalinkClick={onPermalinkClick}
|
||||
onPinLine={onPinLine}
|
||||
onUnpinLine={onUnpinLine}
|
||||
pinLineButtonTooltipTitle={pinLineButtonTooltipTitle}
|
||||
pinned={pinned}
|
||||
styles={styles}
|
||||
mouseIsOver={mouseIsOver}
|
||||
|
@ -15,7 +15,7 @@ import {
|
||||
} from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { DataQuery } from '@grafana/schema';
|
||||
import { withTheme2, Themeable2 } from '@grafana/ui';
|
||||
import { withTheme2, Themeable2, PopoverContent } from '@grafana/ui';
|
||||
|
||||
import { PopoverMenu } from '../../explore/Logs/PopoverMenu';
|
||||
import { UniqueKeyMaker } from '../UniqueKeyMaker';
|
||||
@ -50,6 +50,7 @@ export interface Props extends Themeable2 {
|
||||
onClickHideField?: (key: string) => void;
|
||||
onPinLine?: (row: LogRowModel) => void;
|
||||
onUnpinLine?: (row: LogRowModel) => void;
|
||||
pinLineButtonTooltipTitle?: PopoverContent;
|
||||
onLogRowHover?: (row?: LogRowModel) => void;
|
||||
onOpenContext?: (row: LogRowModel, onClose: () => void) => void;
|
||||
getRowContextQuery?: (
|
||||
@ -238,6 +239,7 @@ class UnThemedLogRows extends PureComponent<Props, State> {
|
||||
permalinkedRowId={this.props.permalinkedRowId}
|
||||
onPinLine={this.props.onPinLine}
|
||||
onUnpinLine={this.props.onUnpinLine}
|
||||
pinLineButtonTooltipTitle={this.props.pinLineButtonTooltipTitle}
|
||||
pinned={this.props.pinnedRowId === row.uid}
|
||||
isFilterLabelActive={this.props.isFilterLabelActive}
|
||||
handleTextSelection={this.popoverMenuSupported() ? this.handleSelection : undefined}
|
||||
@ -260,6 +262,7 @@ class UnThemedLogRows extends PureComponent<Props, State> {
|
||||
permalinkedRowId={this.props.permalinkedRowId}
|
||||
onPinLine={this.props.onPinLine}
|
||||
onUnpinLine={this.props.onUnpinLine}
|
||||
pinLineButtonTooltipTitle={this.props.pinLineButtonTooltipTitle}
|
||||
pinned={this.props.pinnedRowId === row.uid}
|
||||
isFilterLabelActive={this.props.isFilterLabelActive}
|
||||
handleTextSelection={this.popoverMenuSupported() ? this.handleSelection : undefined}
|
||||
|
@ -26,15 +26,15 @@ composableKinds: PanelCfg: {
|
||||
version: [0, 0]
|
||||
schema: {
|
||||
Options: {
|
||||
showLabels: bool
|
||||
showCommonLabels: bool
|
||||
showTime: bool
|
||||
showLogContextToggle: bool
|
||||
wrapLogMessage: bool
|
||||
prettifyLogMessage: bool
|
||||
enableLogDetails: bool
|
||||
sortOrder: common.LogsSortOrder
|
||||
dedupStrategy: common.LogsDedupStrategy
|
||||
showLabels: bool
|
||||
showCommonLabels: bool
|
||||
showTime: bool
|
||||
showLogContextToggle: bool
|
||||
wrapLogMessage: bool
|
||||
prettifyLogMessage: bool
|
||||
enableLogDetails: bool
|
||||
sortOrder: common.LogsSortOrder
|
||||
dedupStrategy: common.LogsDedupStrategy
|
||||
// TODO: figure out how to define callbacks
|
||||
onClickFilterLabel?: _
|
||||
onClickFilterOutLabel?: _
|
||||
|
@ -497,6 +497,12 @@
|
||||
"title": "Add query to Query Library",
|
||||
"visibility": "Visibility"
|
||||
},
|
||||
"logs": {
|
||||
"maximum-pinned-logs": "Maximum of {{PINNED_LOGS_LIMIT}} pinned logs reached. Unpin a log to add another.",
|
||||
"no-logs-found": "No logs found.",
|
||||
"scan-for-older-logs": "Scan for older logs",
|
||||
"stop-scan": "Stop scan"
|
||||
},
|
||||
"rich-history": {
|
||||
"close-tooltip": "Close query history",
|
||||
"datasource-a-z": "Data source A-Z",
|
||||
|
@ -497,6 +497,12 @@
|
||||
"title": "Åđđ qūęřy ŧő Qūęřy Ŀįþřäřy",
|
||||
"visibility": "Vįşįþįľįŧy"
|
||||
},
|
||||
"logs": {
|
||||
"maximum-pinned-logs": "Mäχįmūm őƒ {{PINNED_LOGS_LIMIT}} pįʼnʼnęđ ľőģş řęäčĥęđ. Ůʼnpįʼn ä ľőģ ŧő äđđ äʼnőŧĥęř.",
|
||||
"no-logs-found": "Ńő ľőģş ƒőūʼnđ.",
|
||||
"scan-for-older-logs": "Ŝčäʼn ƒőř őľđęř ľőģş",
|
||||
"stop-scan": "Ŝŧőp şčäʼn"
|
||||
},
|
||||
"rich-history": {
|
||||
"close-tooltip": "Cľőşę qūęřy ĥįşŧőřy",
|
||||
"datasource-a-z": "Đäŧä şőūřčę Å-Ż",
|
||||
|
Loading…
Reference in New Issue
Block a user