mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Explore: Add logs navigation to request more logs (#33259)
* Update layout for new logs navigation * Add LogsNavigation component * WIP clean up of css * Add clear navigation and styling * Add clearing to queyrRows run Query * Make querying and displaying reliable * Rename chunks to pages * Refactor, remove logsNavigationCleared from navigation * Rmove clear logs navigation * Add flipping * Update styling * Add test * Fix import * Update Wrapper test * Update public/sass/pages/_explore.scss * Update public/app/features/explore/Logs.tsx Co-authored-by: Giordano Ricci <me@giordanoricci.com> * Add spinner to page to prevent jumpy time * Create standard size for page * Fix test * Fix postioning and useRefs * Add maxHeight Co-authored-by: Giordano Ricci <me@giordanoricci.com>
This commit is contained in:
parent
fa866f1154
commit
0609b80fdc
@ -24,9 +24,6 @@ export interface Props extends Themeable {
|
||||
logsSortOrder?: LogsSortOrder | null;
|
||||
allowDetails?: boolean;
|
||||
previewLimit?: number;
|
||||
// Passed to fix problems with inactive scrolling in Logs Panel
|
||||
// Can be removed when we unify scrolling for Panel and Explore
|
||||
disableCustomHorizontalScroll?: boolean;
|
||||
forceEscape?: boolean;
|
||||
showDetectedFields?: string[];
|
||||
showContextToggle?: (row?: LogRowModel) => boolean;
|
||||
@ -97,7 +94,6 @@ class UnThemedLogRows extends PureComponent<Props, State> {
|
||||
allowDetails,
|
||||
previewLimit,
|
||||
getFieldLinks,
|
||||
disableCustomHorizontalScroll,
|
||||
logsSortOrder,
|
||||
showDetectedFields,
|
||||
onClickShowDetectedField,
|
||||
@ -105,18 +101,13 @@ class UnThemedLogRows extends PureComponent<Props, State> {
|
||||
forceEscape,
|
||||
} = this.props;
|
||||
const { renderAll } = this.state;
|
||||
const { logsRowsTable, logsRowsHorizontalScroll } = getLogRowStyles(theme);
|
||||
const { logsRowsTable } = getLogRowStyles(theme);
|
||||
const dedupedRows = deduplicatedRows ? deduplicatedRows : logRows;
|
||||
const hasData = logRows && logRows.length > 0;
|
||||
const dedupCount = dedupedRows
|
||||
? dedupedRows.reduce((sum, row) => (row.duplicates ? sum + row.duplicates : sum), 0)
|
||||
: 0;
|
||||
const showDuplicates = dedupStrategy !== LogsDedupStrategy.none && dedupCount > 0;
|
||||
|
||||
// For horizontal scrolling we can't use CustomScrollbar as it causes the problem with logs context - it is not visible
|
||||
// for top log rows. Therefore we use CustomScrollbar only in LogsPanel and for Explore, we use custom css styling.
|
||||
const horizontalScrollWindow = wrapLogMessage || disableCustomHorizontalScroll ? '' : logsRowsHorizontalScroll;
|
||||
|
||||
// Staged rendering
|
||||
const processedRows = dedupedRows ? dedupedRows : [];
|
||||
const orderedRows = logsSortOrder ? this.sortLogs(processedRows, logsSortOrder) : processedRows;
|
||||
@ -128,67 +119,65 @@ class UnThemedLogRows extends PureComponent<Props, State> {
|
||||
const getRowContext = this.props.getRowContext ? this.props.getRowContext : () => Promise.resolve([]);
|
||||
|
||||
return (
|
||||
<div className={horizontalScrollWindow}>
|
||||
<table className={logsRowsTable}>
|
||||
<tbody>
|
||||
{hasData &&
|
||||
firstRows.map((row, index) => (
|
||||
<LogRow
|
||||
key={row.uid}
|
||||
getRows={getRows}
|
||||
getRowContext={getRowContext}
|
||||
highlighterExpressions={highlighterExpressions}
|
||||
row={row}
|
||||
showContextToggle={showContextToggle}
|
||||
showDuplicates={showDuplicates}
|
||||
showLabels={showLabels}
|
||||
showTime={showTime}
|
||||
showDetectedFields={showDetectedFields}
|
||||
wrapLogMessage={wrapLogMessage}
|
||||
timeZone={timeZone}
|
||||
allowDetails={allowDetails}
|
||||
onClickFilterLabel={onClickFilterLabel}
|
||||
onClickFilterOutLabel={onClickFilterOutLabel}
|
||||
onClickShowDetectedField={onClickShowDetectedField}
|
||||
onClickHideDetectedField={onClickHideDetectedField}
|
||||
getFieldLinks={getFieldLinks}
|
||||
logsSortOrder={logsSortOrder}
|
||||
forceEscape={forceEscape}
|
||||
/>
|
||||
))}
|
||||
{hasData &&
|
||||
renderAll &&
|
||||
lastRows.map((row, index) => (
|
||||
<LogRow
|
||||
key={row.uid}
|
||||
getRows={getRows}
|
||||
getRowContext={getRowContext}
|
||||
row={row}
|
||||
showContextToggle={showContextToggle}
|
||||
showDuplicates={showDuplicates}
|
||||
showLabels={showLabels}
|
||||
showTime={showTime}
|
||||
showDetectedFields={showDetectedFields}
|
||||
wrapLogMessage={wrapLogMessage}
|
||||
timeZone={timeZone}
|
||||
allowDetails={allowDetails}
|
||||
onClickFilterLabel={onClickFilterLabel}
|
||||
onClickFilterOutLabel={onClickFilterOutLabel}
|
||||
onClickShowDetectedField={onClickShowDetectedField}
|
||||
onClickHideDetectedField={onClickHideDetectedField}
|
||||
getFieldLinks={getFieldLinks}
|
||||
logsSortOrder={logsSortOrder}
|
||||
forceEscape={forceEscape}
|
||||
/>
|
||||
))}
|
||||
{hasData && !renderAll && (
|
||||
<tr>
|
||||
<td colSpan={5}>Rendering {orderedRows.length - previewLimit!} rows...</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<table className={logsRowsTable}>
|
||||
<tbody>
|
||||
{hasData &&
|
||||
firstRows.map((row, index) => (
|
||||
<LogRow
|
||||
key={row.uid}
|
||||
getRows={getRows}
|
||||
getRowContext={getRowContext}
|
||||
highlighterExpressions={highlighterExpressions}
|
||||
row={row}
|
||||
showContextToggle={showContextToggle}
|
||||
showDuplicates={showDuplicates}
|
||||
showLabels={showLabels}
|
||||
showTime={showTime}
|
||||
showDetectedFields={showDetectedFields}
|
||||
wrapLogMessage={wrapLogMessage}
|
||||
timeZone={timeZone}
|
||||
allowDetails={allowDetails}
|
||||
onClickFilterLabel={onClickFilterLabel}
|
||||
onClickFilterOutLabel={onClickFilterOutLabel}
|
||||
onClickShowDetectedField={onClickShowDetectedField}
|
||||
onClickHideDetectedField={onClickHideDetectedField}
|
||||
getFieldLinks={getFieldLinks}
|
||||
logsSortOrder={logsSortOrder}
|
||||
forceEscape={forceEscape}
|
||||
/>
|
||||
))}
|
||||
{hasData &&
|
||||
renderAll &&
|
||||
lastRows.map((row, index) => (
|
||||
<LogRow
|
||||
key={row.uid}
|
||||
getRows={getRows}
|
||||
getRowContext={getRowContext}
|
||||
row={row}
|
||||
showContextToggle={showContextToggle}
|
||||
showDuplicates={showDuplicates}
|
||||
showLabels={showLabels}
|
||||
showTime={showTime}
|
||||
showDetectedFields={showDetectedFields}
|
||||
wrapLogMessage={wrapLogMessage}
|
||||
timeZone={timeZone}
|
||||
allowDetails={allowDetails}
|
||||
onClickFilterLabel={onClickFilterLabel}
|
||||
onClickFilterOutLabel={onClickFilterOutLabel}
|
||||
onClickShowDetectedField={onClickShowDetectedField}
|
||||
onClickHideDetectedField={onClickHideDetectedField}
|
||||
getFieldLinks={getFieldLinks}
|
||||
logsSortOrder={logsSortOrder}
|
||||
forceEscape={forceEscape}
|
||||
/>
|
||||
))}
|
||||
{hasData && !renderAll && (
|
||||
<tr>
|
||||
<td colSpan={5}>Rendering {orderedRows.length - previewLimit!} rows...</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -49,10 +49,6 @@ export const getLogRowStyles = stylesFactory((theme: GrafanaTheme, logLevel?: Lo
|
||||
font-size: ${theme.typography.size.sm};
|
||||
width: 100%;
|
||||
`,
|
||||
logsRowsHorizontalScroll: css`
|
||||
label: logs-rows__horizontal-scroll;
|
||||
overflow: auto;
|
||||
`,
|
||||
context: css`
|
||||
label: context;
|
||||
visibility: hidden;
|
||||
|
@ -18,6 +18,7 @@ import {
|
||||
LinkModel,
|
||||
Field,
|
||||
GrafanaTheme,
|
||||
DataQuery,
|
||||
} from '@grafana/data';
|
||||
import {
|
||||
RadioButtonGroup,
|
||||
@ -28,11 +29,13 @@ import {
|
||||
InlineSwitch,
|
||||
withTheme,
|
||||
stylesFactory,
|
||||
CustomScrollbar,
|
||||
} from '@grafana/ui';
|
||||
import store from 'app/core/store';
|
||||
import { dedupLogRows, filterLogLevels } from 'app/core/logs_model';
|
||||
import { ExploreGraphPanel } from './ExploreGraphPanel';
|
||||
import { LogsMetaRow } from './LogsMetaRow';
|
||||
import LogsNavigation from './LogsNavigation';
|
||||
import { RowContextOptions } from '@grafana/ui/src/components/Logs/LogRowContextProvider';
|
||||
|
||||
const SETTINGS_KEYS = {
|
||||
@ -54,6 +57,7 @@ interface Props {
|
||||
timeZone: TimeZone;
|
||||
scanning?: boolean;
|
||||
scanRange?: RawTimeRange;
|
||||
queries: DataQuery[];
|
||||
showContextToggle?: (row?: LogRowModel) => boolean;
|
||||
onChangeTime: (range: AbsoluteTimeRange) => void;
|
||||
onClickFilterLabel?: (key: string, value: string) => void;
|
||||
@ -237,6 +241,7 @@ export class UnthemedLogs extends PureComponent<Props, State> {
|
||||
onChangeTime,
|
||||
getFieldLinks,
|
||||
theme,
|
||||
queries,
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
@ -310,7 +315,6 @@ export class UnthemedLogs extends PureComponent<Props, State> {
|
||||
{isFlipping ? 'Flipping...' : 'Flip results order'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<LogsMetaRow
|
||||
logRows={logRows}
|
||||
meta={logsMeta || []}
|
||||
@ -322,28 +326,39 @@ export class UnthemedLogs extends PureComponent<Props, State> {
|
||||
onEscapeNewlines={this.onEscapeNewlines}
|
||||
clearDetectedFields={this.clearDetectedFields}
|
||||
/>
|
||||
|
||||
<LogRows
|
||||
logRows={logRows}
|
||||
deduplicatedRows={dedupedRows}
|
||||
dedupStrategy={dedupStrategy}
|
||||
getRowContext={this.props.getRowContext}
|
||||
highlighterExpressions={highlighterExpressions}
|
||||
onClickFilterLabel={onClickFilterLabel}
|
||||
onClickFilterOutLabel={onClickFilterOutLabel}
|
||||
showContextToggle={showContextToggle}
|
||||
showLabels={showLabels}
|
||||
showTime={showTime}
|
||||
forceEscape={forceEscape}
|
||||
wrapLogMessage={wrapLogMessage}
|
||||
timeZone={timeZone}
|
||||
getFieldLinks={getFieldLinks}
|
||||
logsSortOrder={logsSortOrder}
|
||||
showDetectedFields={showDetectedFields}
|
||||
onClickShowDetectedField={this.showDetectedField}
|
||||
onClickHideDetectedField={this.hideDetectedField}
|
||||
/>
|
||||
|
||||
<div className={styles.logsSection}>
|
||||
<CustomScrollbar autoHide>
|
||||
<LogRows
|
||||
logRows={logRows}
|
||||
deduplicatedRows={dedupedRows}
|
||||
dedupStrategy={dedupStrategy}
|
||||
getRowContext={this.props.getRowContext}
|
||||
highlighterExpressions={highlighterExpressions}
|
||||
onClickFilterLabel={onClickFilterLabel}
|
||||
onClickFilterOutLabel={onClickFilterOutLabel}
|
||||
showContextToggle={showContextToggle}
|
||||
showLabels={showLabels}
|
||||
showTime={showTime}
|
||||
forceEscape={forceEscape}
|
||||
wrapLogMessage={wrapLogMessage}
|
||||
timeZone={timeZone}
|
||||
getFieldLinks={getFieldLinks}
|
||||
logsSortOrder={logsSortOrder}
|
||||
showDetectedFields={showDetectedFields}
|
||||
onClickShowDetectedField={this.showDetectedField}
|
||||
onClickHideDetectedField={this.hideDetectedField}
|
||||
/>
|
||||
</CustomScrollbar>
|
||||
<LogsNavigation
|
||||
logsSortOrder={logsSortOrder}
|
||||
visibleRange={visibleRange}
|
||||
absoluteRange={absoluteRange}
|
||||
timeZone={timeZone}
|
||||
onChangeTime={onChangeTime}
|
||||
loading={loading}
|
||||
queries={queries}
|
||||
/>
|
||||
</div>
|
||||
{!loading && !hasData && !scanning && (
|
||||
<div className={styles.noData}>
|
||||
No logs found.
|
||||
@ -392,5 +407,10 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
radioButtons: css`
|
||||
margin: 0 ${theme.spacing.sm};
|
||||
`,
|
||||
logsSection: css`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
max-height: 95vh;
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
@ -78,6 +78,7 @@ export class LogsContainer extends PureComponent<PropsFromRedux & LogsContainerP
|
||||
width,
|
||||
isLive,
|
||||
exploreId,
|
||||
queries,
|
||||
} = this.props;
|
||||
|
||||
if (!logRows) {
|
||||
@ -124,6 +125,7 @@ export class LogsContainer extends PureComponent<PropsFromRedux & LogsContainerP
|
||||
width={width}
|
||||
getRowContext={this.getLogRowContext}
|
||||
getFieldLinks={this.getFieldLinks}
|
||||
queries={queries}
|
||||
/>
|
||||
</Collapse>
|
||||
</LogsCrossFadeTransition>
|
||||
@ -146,6 +148,7 @@ function mapStateToProps(state: StoreState, { exploreId }: { exploreId: string }
|
||||
isPaused,
|
||||
range,
|
||||
absoluteRange,
|
||||
queries,
|
||||
} = item;
|
||||
const timeZone = getTimeZone(state.user);
|
||||
|
||||
@ -163,6 +166,7 @@ function mapStateToProps(state: StoreState, { exploreId }: { exploreId: string }
|
||||
isPaused,
|
||||
range,
|
||||
absoluteRange,
|
||||
queries,
|
||||
};
|
||||
}
|
||||
|
||||
|
44
public/app/features/explore/LogsNavigation.test.tsx
Normal file
44
public/app/features/explore/LogsNavigation.test.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
import React, { ComponentProps } from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { LogsSortOrder } from '@grafana/data';
|
||||
import LogsNavigation from './LogsNavigation';
|
||||
|
||||
type LogsNavigationrProps = ComponentProps<typeof LogsNavigation>;
|
||||
|
||||
const setup = (propOverrides?: object) => {
|
||||
const props: LogsNavigationrProps = {
|
||||
absoluteRange: { from: 1619081645930, to: 1619081945930 },
|
||||
timeZone: 'local',
|
||||
queries: [],
|
||||
loading: false,
|
||||
logsSortOrder: undefined,
|
||||
visibleRange: { from: 1619081941000, to: 1619081945930 },
|
||||
onChangeTime: jest.fn(),
|
||||
...propOverrides,
|
||||
};
|
||||
|
||||
return render(<LogsNavigation {...props} />);
|
||||
};
|
||||
|
||||
describe('LogsNavigation', () => {
|
||||
it('should render fetch logs button on bottom when default logs order', () => {
|
||||
setup();
|
||||
expect(screen.getByTestId('fetchLogsBottom')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('fetchLogsTop')).not.toBeInTheDocument();
|
||||
});
|
||||
it('should render fetch logs button on top when flipped logs order', () => {
|
||||
setup({ logsSortOrder: LogsSortOrder.Ascending });
|
||||
expect(screen.getByTestId('fetchLogsTop')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('fetchLogsBottom')).not.toBeInTheDocument();
|
||||
});
|
||||
it('should disable button to fetch logs when loading', () => {
|
||||
setup({ loading: true });
|
||||
const button = screen.getByTestId('fetchLogsBottom');
|
||||
expect(button).toBeInTheDocument();
|
||||
expect(button).toBeDisabled();
|
||||
});
|
||||
it('should render logs page with correct range', () => {
|
||||
setup();
|
||||
expect(screen.getByText(/02:59:05 — 02:59:01/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
251
public/app/features/explore/LogsNavigation.tsx
Normal file
251
public/app/features/explore/LogsNavigation.tsx
Normal file
@ -0,0 +1,251 @@
|
||||
import React, { memo, useState, useEffect, useRef } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { isEqual } from 'lodash';
|
||||
import { css } from 'emotion';
|
||||
import {
|
||||
LogsSortOrder,
|
||||
AbsoluteTimeRange,
|
||||
dateTimeFormat,
|
||||
systemDateFormats,
|
||||
TimeZone,
|
||||
DataQuery,
|
||||
GrafanaTheme,
|
||||
} from '@grafana/data';
|
||||
import { Button, Icon, Spinner, useTheme, stylesFactory, CustomScrollbar } from '@grafana/ui';
|
||||
|
||||
type Props = {
|
||||
absoluteRange: AbsoluteTimeRange;
|
||||
timeZone: TimeZone;
|
||||
queries: DataQuery[];
|
||||
loading: boolean;
|
||||
visibleRange?: AbsoluteTimeRange;
|
||||
logsSortOrder?: LogsSortOrder | null;
|
||||
onChangeTime: (range: AbsoluteTimeRange) => void;
|
||||
};
|
||||
|
||||
type LogsPage = {
|
||||
logsRange: AbsoluteTimeRange;
|
||||
queryRange: AbsoluteTimeRange;
|
||||
};
|
||||
|
||||
function LogsNavigation({
|
||||
absoluteRange,
|
||||
logsSortOrder,
|
||||
timeZone,
|
||||
loading,
|
||||
onChangeTime,
|
||||
visibleRange = absoluteRange,
|
||||
queries = [],
|
||||
}: Props) {
|
||||
const [pages, setPages] = useState<LogsPage[]>([]);
|
||||
const [currentPageIndex, setCurrentPageIndex] = useState(0);
|
||||
|
||||
// These refs are to determine, if we want to clear up logs navigation when totally new query is run
|
||||
const expectedQueriesRef = useRef<DataQuery[]>();
|
||||
const expectedRangeRef = useRef<AbsoluteTimeRange>();
|
||||
// This ref is to store range span for future queres based on firstly selected time range
|
||||
// e.g. if last 5 min selected, always run 5 min range
|
||||
const rangeSpanRef = useRef(0);
|
||||
|
||||
// Main effect to set pages and index
|
||||
useEffect(() => {
|
||||
const newPage = { logsRange: visibleRange, queryRange: absoluteRange };
|
||||
let newPages: LogsPage[] = [];
|
||||
// We want to start new pagination if queries change or if absolute range is different than expected
|
||||
if (!isEqual(expectedRangeRef.current, absoluteRange) || !isEqual(expectedQueriesRef.current, queries)) {
|
||||
setPages([newPage]);
|
||||
setCurrentPageIndex(0);
|
||||
expectedQueriesRef.current = queries;
|
||||
rangeSpanRef.current = absoluteRange.to - absoluteRange.from;
|
||||
} else {
|
||||
setPages((pages) => {
|
||||
// Remove duplicates with new query
|
||||
newPages = pages.filter((page) => !isEqual(newPage.queryRange, page.queryRange));
|
||||
// Sort pages based on logsOrder so they visually align with displayed logs
|
||||
newPages = [...newPages, newPage].sort((a, b) => sortPages(a, b, logsSortOrder));
|
||||
// Set new pages
|
||||
|
||||
return newPages;
|
||||
});
|
||||
|
||||
// Set current page index
|
||||
const index = newPages.findIndex((page) => page.queryRange.to === absoluteRange.to);
|
||||
setCurrentPageIndex(index);
|
||||
}
|
||||
}, [visibleRange, absoluteRange, logsSortOrder, queries]);
|
||||
|
||||
const changeTime = ({ from, to }: AbsoluteTimeRange) => {
|
||||
expectedRangeRef.current = { from, to };
|
||||
onChangeTime({ from, to });
|
||||
};
|
||||
|
||||
const sortPages = (a: LogsPage, b: LogsPage, logsSortOrder?: LogsSortOrder | null) => {
|
||||
if (logsSortOrder === LogsSortOrder.Ascending) {
|
||||
return a.queryRange.to > b.queryRange.to ? 1 : -1;
|
||||
}
|
||||
return a.queryRange.to > b.queryRange.to ? -1 : 1;
|
||||
};
|
||||
|
||||
const formatTime = (time: number) => {
|
||||
return `${dateTimeFormat(time, {
|
||||
format: systemDateFormats.interval.second,
|
||||
timeZone: timeZone,
|
||||
})}`;
|
||||
};
|
||||
|
||||
const createPageContent = (page: LogsPage, index: number) => {
|
||||
if (currentPageIndex === index && loading) {
|
||||
return <Spinner />;
|
||||
}
|
||||
const topContent = formatTime(oldestLogsFirst ? page.logsRange.from : page.logsRange.to);
|
||||
const bottomContent = formatTime(oldestLogsFirst ? page.logsRange.to : page.logsRange.from);
|
||||
return `${topContent} — ${bottomContent}`;
|
||||
};
|
||||
|
||||
const oldestLogsFirst = logsSortOrder === LogsSortOrder.Ascending;
|
||||
const theme = useTheme();
|
||||
const styles = getStyles(theme, oldestLogsFirst, loading);
|
||||
return (
|
||||
<div className={styles.navContainer}>
|
||||
{/*
|
||||
* We are going to have 2 buttons - on the top and bottom - Oldest and Newest.
|
||||
* Therefore I have at the moment duplicated the same code, but in the future iteration, it ill be updated
|
||||
*/}
|
||||
{oldestLogsFirst && (
|
||||
<Button
|
||||
data-testid="fetchLogsTop"
|
||||
className={styles.navButton}
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
// the range is based on initally selected range
|
||||
changeTime({ from: visibleRange.from - rangeSpanRef.current, to: visibleRange.from });
|
||||
}}
|
||||
disabled={loading}
|
||||
>
|
||||
<div className={styles.navButtonContent}>
|
||||
{loading ? <Spinner /> : <Icon name="angle-up" size="lg" />}
|
||||
Older logs
|
||||
</div>
|
||||
</Button>
|
||||
)}
|
||||
<CustomScrollbar autoHide>
|
||||
<div className={styles.pagesWrapper}>
|
||||
<div className={styles.pagesContainer}>
|
||||
{pages.map((page: LogsPage, index) => (
|
||||
<div
|
||||
className={styles.page}
|
||||
key={page.queryRange.to}
|
||||
onClick={() => !loading && changeTime({ from: page.queryRange.from, to: page.queryRange.to })}
|
||||
>
|
||||
<div className={classNames(styles.line, { selectedBg: currentPageIndex === index })} />
|
||||
<div className={classNames(styles.time, { selectedText: currentPageIndex === index })}>
|
||||
{createPageContent(page, index)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className={styles.filler}></div>
|
||||
</div>
|
||||
</CustomScrollbar>
|
||||
|
||||
{!oldestLogsFirst && (
|
||||
<Button
|
||||
data-testid="fetchLogsBottom"
|
||||
className={styles.navButton}
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
// the range is based on initally selected range
|
||||
changeTime({ from: visibleRange.from - rangeSpanRef.current, to: visibleRange.from });
|
||||
}}
|
||||
disabled={loading}
|
||||
>
|
||||
<div className={styles.navButtonContent}>
|
||||
Older logs
|
||||
{loading ? <Spinner /> : <Icon name="angle-down" size="lg" />}
|
||||
</div>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(LogsNavigation);
|
||||
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme, oldestLogsFirst: boolean, loading: boolean) => {
|
||||
return {
|
||||
navContainer: css`
|
||||
max-height: 95vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: ${oldestLogsFirst ? 'flex-start' : 'space-between'};
|
||||
`,
|
||||
navButton: css`
|
||||
width: 58px;
|
||||
height: 68px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
line-height: 1;
|
||||
`,
|
||||
navButtonContent: css`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
white-space: normal;
|
||||
`,
|
||||
pagesWrapper: css`
|
||||
height: 100%;
|
||||
padding-left: ${theme.spacing.xs};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: scroll;
|
||||
`,
|
||||
pagesContainer: css`
|
||||
display: flex;
|
||||
padding: 0;
|
||||
flex-direction: column;
|
||||
`,
|
||||
page: css`
|
||||
display: flex;
|
||||
margin: ${theme.spacing.md} 0;
|
||||
cursor: ${loading ? 'auto' : 'pointer'};
|
||||
white-space: normal;
|
||||
.selectedBg {
|
||||
background: ${theme.colors.bgBlue2};
|
||||
}
|
||||
.selectedText {
|
||||
color: ${theme.colors.bgBlue2};
|
||||
}
|
||||
`,
|
||||
line: css`
|
||||
width: 3px;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
background: ${theme.colors.textWeak};
|
||||
`,
|
||||
time: css`
|
||||
width: 60px;
|
||||
min-height: 80px;
|
||||
font-size: ${theme.typography.size.sm};
|
||||
padding-left: ${theme.spacing.xs};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`,
|
||||
filler: css`
|
||||
height: inherit;
|
||||
background: repeating-linear-gradient(
|
||||
135deg,
|
||||
${theme.colors.bg1},
|
||||
${theme.colors.bg1} 5px,
|
||||
${theme.colors.bg2} 5px,
|
||||
${theme.colors.bg2} 15px
|
||||
);
|
||||
width: 3px;
|
||||
margin-bottom: 8px;
|
||||
`,
|
||||
};
|
||||
});
|
@ -64,7 +64,7 @@ describe('Wrapper', () => {
|
||||
(datasources.loki.query as Mock).mockReturnValueOnce(makeLogsQueryResponse());
|
||||
|
||||
// Make sure we render the logs panel
|
||||
await screen.findByText(/^Logs$/i);
|
||||
await screen.findByText(/^Logs$/);
|
||||
|
||||
// Make sure we render the log line
|
||||
await screen.findByText(/custom log line/i);
|
||||
@ -169,7 +169,7 @@ describe('Wrapper', () => {
|
||||
|
||||
// Make sure we render the logs panel
|
||||
await waitFor(() => {
|
||||
const logsPanels = screen.getAllByText(/^Logs$/i);
|
||||
const logsPanels = screen.getAllByText(/^Logs$/);
|
||||
expect(logsPanels.length).toBe(2);
|
||||
});
|
||||
|
||||
|
@ -35,7 +35,6 @@ export const LogsPanel: React.FunctionComponent<LogsPanelProps> = ({
|
||||
wrapLogMessage={wrapLogMessage}
|
||||
timeZone={timeZone}
|
||||
allowDetails={true}
|
||||
disableCustomHorizontalScroll={true}
|
||||
logsSortOrder={sortOrder}
|
||||
/>
|
||||
</CustomScrollbar>
|
||||
|
Loading…
Reference in New Issue
Block a user