feat: add list and table views for logs (#2163)

* feat: add list and table views for logs

* chore: some of the changes are updated

* chore: some of the refactoring is done

* chore: px to updated to rem

* chore: constant is moved to local storage

* refactor: some of the refactoring is updated

* chore: some of the changes are updated

* fix: resize log table issue

* chore: logs is updated

* chore: resize header is updated

* chore: font observer is added in package json and hook is added for same

* chore: no logs text is updated

* chore: no logs text is updated

* chore: updated some feedback in raw logs line

* chore: types is added

---------

Co-authored-by: Palash Gupta <palashgdev@gmail.com>
Co-authored-by: Pranay Prateek <pranay@signoz.io>
Co-authored-by: Vishal Sharma <makeavish786@gmail.com>
Co-authored-by: Chintan Sudani <csudani7@gmail.com>
This commit is contained in:
volodfast 2023-02-15 11:25:15 +02:00 committed by GitHub
parent 8965b9b503
commit bad80def90
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 714 additions and 59 deletions

View File

@ -55,6 +55,7 @@
"event-source-polyfill": "1.0.31",
"file-loader": "6.1.1",
"flat": "^5.0.2",
"fontfaceobserver": "2.3.0",
"history": "4.10.1",
"html-webpack-plugin": "5.1.0",
"i18next": "^21.6.12",
@ -127,6 +128,7 @@
"@types/d3-tip": "^3.5.5",
"@types/event-source-polyfill": "^1.0.0",
"@types/flat": "^5.0.2",
"@types/fontfaceobserver": "2.1.0",
"@types/jest": "^27.5.1",
"@types/lodash-es": "^4.17.4",
"@types/mini-css-extract-plugin": "^2.5.1",

View File

@ -4,17 +4,21 @@ import { Button, Divider, Row, Typography } from 'antd';
import { map } from 'd3';
import dayjs from 'dayjs';
import { useNotifications } from 'hooks/useNotifications';
// utils
import { FlatLogData } from 'lib/logs/flatLogData';
import React, { useCallback, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useCopyToClipboard } from 'react-use';
// interfaces
import { AppState } from 'store/reducers';
import { SET_DETAILED_LOG_DATA } from 'types/actions/logs';
import { ILog } from 'types/api/logs/log';
import { ILogsReducer } from 'types/reducer/logs';
// components
import AddToQueryHOC from '../AddToQueryHOC';
import CopyClipboardHOC from '../CopyClipboardHOC';
// styles
import { Container, LogContainer, Text, TextContainer } from './styles';
import { isValidLogField } from './util';
@ -37,6 +41,7 @@ function LogGeneralField({ fieldKey, fieldValue }: LogFieldProps): JSX.Element {
</TextContainer>
);
}
function LogSelectedField({
fieldKey = '',
fieldValue = '',
@ -70,15 +75,17 @@ function LogSelectedField({
);
}
interface LogItemProps {
interface ListLogViewProps {
logData: ILog;
}
function LogItem({ logData }: LogItemProps): JSX.Element {
function ListLogView({ logData }: ListLogViewProps): JSX.Element {
const {
fields: { selected },
} = useSelector<AppState, ILogsReducer>((state) => state.logs);
const dispatch = useDispatch();
const flattenLogData = useMemo(() => FlatLogData(logData), [logData]);
const [, setCopy] = useCopyToClipboard();
const { notifications } = useNotifications();
@ -152,4 +159,4 @@ function LogItem({ logData }: LogItemProps): JSX.Element {
);
}
export default LogItem;
export default ListLogView;

View File

@ -0,0 +1,5 @@
export const rawLineStyle: React.CSSProperties = {
marginBottom: 0,
fontFamily: "'Fira Code', monospace",
fontWeight: 300,
};

View File

@ -0,0 +1,48 @@
import { ExpandAltOutlined } from '@ant-design/icons';
import { Typography } from 'antd';
import dayjs from 'dayjs';
// hooks
import { useIsDarkMode } from 'hooks/useDarkMode';
import React, { useCallback, useMemo } from 'react';
// interfaces
import { ILog } from 'types/api/logs/log';
import { rawLineStyle } from './config';
// styles
import { ExpandIconWrapper, RawLogViewContainer } from './styles';
interface RawLogViewProps {
data: ILog;
linesPerRow: number;
onClickExpand: (log: ILog) => void;
}
function RawLogView(props: RawLogViewProps): JSX.Element {
const { data, linesPerRow, onClickExpand } = props;
const isDarkMode = useIsDarkMode();
const text = useMemo(
() => `${dayjs(data.timestamp / 1e6).format()} | ${data.body}`,
[data.timestamp, data.body],
);
const ellipsis = useMemo(() => ({ rows: linesPerRow }), [linesPerRow]);
const handleClickExpand = useCallback(() => {
onClickExpand(data);
}, [onClickExpand, data]);
return (
<RawLogViewContainer wrap={false} align="middle" $isDarkMode={isDarkMode}>
<ExpandIconWrapper flex="30px" onClick={handleClickExpand}>
<ExpandAltOutlined />
</ExpandIconWrapper>
<Typography.Paragraph style={rawLineStyle} ellipsis={ellipsis}>
{text}
</Typography.Paragraph>
</RawLogViewContainer>
);
}
export default RawLogView;

View File

@ -0,0 +1,24 @@
import { blue } from '@ant-design/colors';
import { Col, Row } from 'antd';
import styled from 'styled-components';
export const RawLogViewContainer = styled(Row)<{ $isDarkMode: boolean }>`
width: 100%;
font-weight: 700;
font-size: 0.625rem;
line-height: 1.25rem;
transition: background-color 0.2s ease-in;
&:hover {
background-color: ${({ $isDarkMode }): string =>
$isDarkMode ? 'rgba(255,255,255,0.1)' : 'rgba(0, 0, 0, 0.1)'};
}
`;
export const ExpandIconWrapper = styled(Col)`
color: ${blue[6]};
padding: 0.25rem 0.375rem;
cursor: pointer;
font-size: 12px;
`;

View File

@ -0,0 +1,12 @@
import { TableProps } from 'antd';
export const defaultCellStyle: React.CSSProperties = {
paddingTop: 4,
paddingBottom: 6,
paddingRight: 8,
paddingLeft: 8,
};
export const tableScroll: TableProps<Record<string, unknown>>['scroll'] = {
x: true,
};

View File

@ -0,0 +1,106 @@
import { ExpandAltOutlined } from '@ant-design/icons';
import { Table, Typography } from 'antd';
import { ColumnsType, ColumnType } from 'antd/es/table';
import dayjs from 'dayjs';
// utils
import { FlatLogData } from 'lib/logs/flatLogData';
import React, { useMemo } from 'react';
import { IField } from 'types/api/logs/fields';
// interfaces
import { ILog } from 'types/api/logs/log';
// styles
import { ExpandIconWrapper } from '../RawLogView/styles';
// config
import { defaultCellStyle, tableScroll } from './config';
type ColumnTypeRender<T = unknown> = ReturnType<
NonNullable<ColumnType<T>['render']>
>;
type LogsTableViewProps = {
logs: ILog[];
fields: IField[];
linesPerRow: number;
onClickExpand: (log: ILog) => void;
};
function LogsTableView(props: LogsTableViewProps): JSX.Element {
const { logs, fields, linesPerRow, onClickExpand } = props;
const flattenLogData = useMemo(() => logs.map((log) => FlatLogData(log)), [
logs,
]);
const columns: ColumnsType<Record<string, unknown>> = useMemo(() => {
const fieldColumns: ColumnsType<Record<string, unknown>> = fields.map(
({ name }) => ({
title: name,
dataIndex: name,
key: name,
render: (field): ColumnTypeRender<Record<string, unknown>> => ({
props: {
style: defaultCellStyle,
},
children: (
<Typography.Paragraph ellipsis={{ rows: linesPerRow }}>
{field}
</Typography.Paragraph>
),
}),
}),
);
return [
{
title: '',
dataIndex: 'id',
key: 'expand',
// https://github.com/ant-design/ant-design/discussions/36886
render: (_, item): ColumnTypeRender<Record<string, unknown>> => ({
props: {
style: defaultCellStyle,
},
children: (
<ExpandIconWrapper
onClick={(): void => {
onClickExpand((item as unknown) as ILog);
}}
>
<ExpandAltOutlined />
</ExpandIconWrapper>
),
}),
},
{
title: 'Timestamp',
dataIndex: 'timestamp',
key: 'timestamp',
// https://github.com/ant-design/ant-design/discussions/36886
render: (field): ColumnTypeRender<Record<string, unknown>> => {
const date = dayjs(field / 1e6).format();
return {
props: {
style: defaultCellStyle,
},
children: <span>{date}</span>,
};
},
},
...fieldColumns,
];
}, [fields, linesPerRow, onClickExpand]);
return (
<Table
columns={columns}
dataSource={flattenLogData}
pagination={false}
rowKey="id"
bordered
scroll={tableScroll}
/>
);
}
export default LogsTableView;

View File

@ -17,13 +17,6 @@ function ResizableHeader(props: ResizableHeaderProps): JSX.Element {
[],
);
const draggableOpts = useMemo(
() => ({
enableUserSelectHack,
}),
[],
);
if (!width) {
// eslint-disable-next-line react/jsx-props-no-spreading
return <th {...restProps} />;
@ -35,7 +28,7 @@ function ResizableHeader(props: ResizableHeaderProps): JSX.Element {
height={0}
handle={handle}
onResize={onResize}
draggableOpts={draggableOpts}
draggableOpts={enableUserSelectHack}
>
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
<th {...restProps} />

View File

@ -4,4 +4,6 @@ export enum LOCALSTORAGE {
AUTH_TOKEN = 'AUTH_TOKEN',
REFRESH_AUTH_TOKEN = 'REFRESH_AUTH_TOKEN',
THEME = 'THEME',
LOGS_VIEW_MODE = 'LOGS_VIEW_MODE',
LOGS_LINES_PER_ROW = 'LOGS_LINES_PER_ROW',
}

View File

@ -6,9 +6,12 @@ import {
import { Button, Divider, Select } from 'antd';
import { getGlobalTime } from 'container/LogsSearchFilter/utils';
import { getMinMax } from 'container/TopNav/AutoRefresh/config';
import { defaultSelectStyle } from 'pages/Logs/config';
import React, { memo, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Dispatch } from 'redux';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import {
GET_NEXT_LOG_LINES,
GET_PREVIOUS_LOG_LINES,
@ -21,8 +24,6 @@ import { ILogsReducer } from 'types/reducer/logs';
import { ITEMS_PER_PAGE_OPTIONS } from './config';
import { Container } from './styles';
const { Option } = Select;
function LogControls(): JSX.Element | null {
const {
logLinesPerPage,
@ -34,13 +35,14 @@ function LogControls(): JSX.Element | null {
const globalTime = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const dispatch = useDispatch();
const dispatch = useDispatch<Dispatch<AppActions>>();
const handleLogLinesPerPageChange = (e: number): void => {
dispatch({
type: SET_LOG_LINES_PER_PAGE,
payload: {
logLinesPerPage: e,
logsLinesPerPage: e,
},
});
};
@ -52,13 +54,17 @@ function LogControls(): JSX.Element | null {
globalTime.maxTime,
);
dispatch({
type: RESET_ID_START_AND_END,
payload: getGlobalTime(globalTime.selectedTime, {
const updatedGlobalTime = getGlobalTime(globalTime.selectedTime, {
maxTime,
minTime,
}),
});
if (updatedGlobalTime) {
dispatch({
type: RESET_ID_START_AND_END,
payload: updatedGlobalTime,
});
}
};
const handleNavigatePrevious = (): void => {
@ -117,12 +123,16 @@ function LogControls(): JSX.Element | null {
Next <RightOutlined />
</Button>
<Select
style={defaultSelectStyle}
loading={isLoading}
value={logLinesPerPage}
onChange={handleLogLinesPerPageChange}
>
{ITEMS_PER_PAGE_OPTIONS.map((count) => (
<Option key={count} value={count}>{`${count} / page`}</Option>
<Select.Option
key={count}
value={count}
>{`${count} / page`}</Select.Option>
))}
</Select>
</Container>

View File

@ -1,5 +1,5 @@
import { CloseOutlined, PlusCircleFilled } from '@ant-design/icons';
import { Input } from 'antd';
import { Col, Input } from 'antd';
import CategoryHeading from 'components/Logs/CategoryHeading';
import { fieldSearchFilter } from 'lib/logs/fieldSearch';
import React, { useCallback, useState } from 'react';
@ -9,7 +9,7 @@ import { ILogsReducer } from 'types/reducer/logs';
import { ICON_STYLE } from './config';
import FieldItem from './FieldItem';
import { CategoryContainer, Container, FieldContainer } from './styles';
import { CategoryContainer, FieldContainer } from './styles';
import { IHandleInterestProps, IHandleRemoveInterestProps } from './types';
import { onHandleAddInterest, onHandleRemoveInterest } from './utils';
@ -58,7 +58,7 @@ function LogsFilters(): JSX.Element {
);
return (
<Container flex="450px">
<Col flex="250px">
<Input
placeholder="Filter Values"
onInput={handleSearch}
@ -110,7 +110,7 @@ function LogsFilters(): JSX.Element {
))}
</FieldContainer>
</CategoryContainer>
</Container>
</Col>
);
}

View File

@ -1,13 +1,7 @@
import { blue, grey } from '@ant-design/colors';
import { Col, Typography } from 'antd';
import { Typography } from 'antd';
import styled from 'styled-components';
export const Container = styled(Col)`
padding-top: 0.3rem;
min-width: 15.625rem;
max-width: 21.875rem;
`;
export const CategoryContainer = styled.div`
margin: 1rem 0;
padding-left: 0.2rem;

View File

@ -48,6 +48,7 @@ function SearchFilter({
AppState,
ILogsReducer
>((state) => state.logs);
const globalTime = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);

View File

@ -1,19 +1,53 @@
import { Typography } from 'antd';
import LogItem from 'components/Logs/LogItem';
import { Card, Typography } from 'antd';
// components
import ListLogView from 'components/Logs/ListLogView';
import RawLogView from 'components/Logs/RawLogView';
import LogsTableView from 'components/Logs/TableView';
import Spinner from 'components/Spinner';
import { contentStyle } from 'container/Trace/Search/config';
import useFontFaceObserver from 'hooks/useFontObserver';
import React, { memo, useCallback, useMemo } from 'react';
import { useSelector } from 'react-redux';
import { Virtuoso } from 'react-virtuoso';
// interfaces
import { AppState } from 'store/reducers';
import { ILog } from 'types/api/logs/log';
import { ILogsReducer } from 'types/reducer/logs';
// styles
import { Container, Heading } from './styles';
function LogsTable(): JSX.Element {
const { logs, isLoading, liveTail } = useSelector<AppState, ILogsReducer>(
(state) => state.logs,
export type LogViewMode = 'raw' | 'table' | 'list';
type LogsTableProps = {
viewMode: LogViewMode;
linesPerRow: number;
onClickExpand: (logData: ILog) => void;
};
function LogsTable(props: LogsTableProps): JSX.Element {
const { viewMode, onClickExpand, linesPerRow } = props;
useFontFaceObserver(
[
{
family: 'Fira Code',
weight: '300',
},
],
viewMode === 'raw',
{
timeout: 5000,
},
);
const {
logs,
fields: { selected },
isLoading,
liveTail,
} = useSelector<AppState, ILogsReducer>((state) => state.logs);
const isLiveTail = useMemo(() => logs.length === 0 && liveTail === 'PLAYING', [
logs?.length,
liveTail,
@ -27,29 +61,63 @@ function LogsTable(): JSX.Element {
const getItemContent = useCallback(
(index: number): JSX.Element => {
const log = logs[index];
return <LogItem key={log.id} logData={log} />;
},
[logs],
if (viewMode === 'raw') {
return (
<RawLogView
key={log.id}
data={log}
linesPerRow={linesPerRow}
onClickExpand={onClickExpand}
/>
);
}
return <ListLogView key={log.id} logData={log} />;
},
[logs, linesPerRow, viewMode, onClickExpand],
);
const renderContent = useMemo(() => {
if (viewMode === 'table') {
return (
<LogsTableView
logs={logs}
fields={selected}
linesPerRow={linesPerRow}
onClickExpand={onClickExpand}
/>
);
}
return (
<Card bodyStyle={contentStyle}>
<Virtuoso
useWindowScroll
totalCount={logs.length}
itemContent={getItemContent}
/>
</Card>
);
}, [getItemContent, linesPerRow, logs, onClickExpand, selected, viewMode]);
if (isLoading) {
return <Spinner height={20} tip="Getting Logs" />;
}
return (
<Container flex="auto">
<Container>
{viewMode !== 'table' && (
<Heading>
<Typography.Text>Event</Typography.Text>
</Heading>
)}
{isLiveTail && <Typography>Getting live logs...</Typography>}
{isNoLogs && <Typography>No log lines found</Typography>}
{isNoLogs && <Typography>No logs lines found</Typography>}
<Virtuoso
useWindowScroll
totalCount={logs.length}
itemContent={getItemContent}
/>
{renderContent}
</Container>
);
}

View File

@ -1,14 +1,16 @@
import { Card, Col } from 'antd';
import { Card } from 'antd';
import styled from 'styled-components';
export const Container = styled(Col)`
export const Container = styled.div`
overflow-x: hidden;
width: 100%;
margin-bottom: 1rem;
margin-top: 0.5rem;
`;
export const Heading = styled(Card)`
margin-bottom: 0.1rem;
height: 32px;
.ant-card-body {
padding: 0.3rem 0.5rem;
}

View File

@ -0,0 +1,69 @@
import FontFaceObserver from 'fontfaceobserver';
import { useEffect, useState } from 'react';
export interface FontFace {
family: string;
weight?:
| `light`
| `normal`
| `bold`
| `bolder`
| `100`
| `200`
| `300`
| `400`
| `500`
| `600`
| `700`
| `800`
| `900`;
style?: `normal` | `italic` | `oblique`;
stretch?:
| `normal`
| `ultra-condensed`
| `extra-condensed`
| `condensed`
| `semi-condensed`
| `semi-expanded`
| `expanded`
| `extra-expanded`
| `ultra-expanded`;
}
export interface Options {
testString?: string;
timeout?: number;
}
export interface Config {
showErrors: boolean;
}
function useFontFaceObserver(
fontFaces: FontFace[] = [],
isEnabled = true,
{ testString, timeout }: Options = {},
{ showErrors }: Config = { showErrors: false },
): boolean {
const [isResolved, setIsResolved] = useState(false);
const fontFacesString = JSON.stringify(fontFaces);
useEffect(() => {
if (isEnabled) {
const promises = JSON.parse(fontFacesString).map(
({ family, weight, style, stretch }: FontFace) =>
new FontFaceObserver(family, {
weight,
style,
stretch,
}).load(testString, timeout),
);
Promise.all(promises).then(() => setIsResolved(true));
}
}, [fontFacesString, testString, timeout, showErrors, isEnabled]);
return isResolved;
}
export default useFontFaceObserver;

View File

@ -50,8 +50,14 @@
<meta data-react-helmet="true" name="docusaurus_locale" content="en" />
<meta data-react-helmet="true" name="docusaurus_tag" content="default" />
<link data-react-helmet="true" rel="shortcut icon" href="/favicon.ico" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css?family=Fira+Code"
rel="stylesheet"
/>
</head>
<body style="margin: 0; padding: 0; box-sizing: border-box;">
<body style="margin: 0; padding: 0; box-sizing: border-box">
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>

View File

@ -0,0 +1,28 @@
import { InputNumber, Row, Space, Typography } from 'antd';
import React from 'react';
interface PopoverContentProps {
linesPerRow: number;
handleLinesPerRowChange: (l: unknown) => void;
}
function PopoverContent({
linesPerRow,
handleLinesPerRowChange,
}: PopoverContentProps): JSX.Element {
return (
<Row align="middle">
<Space align="center">
<Typography>Max lines per Row </Typography>
<InputNumber
min={1}
max={10}
value={linesPerRow}
onChange={handleLinesPerRowChange}
/>
</Space>
</Row>
);
}
export default PopoverContent;

View File

@ -0,0 +1,25 @@
import { ViewModeOption } from './types';
export const viewModeOptionList: ViewModeOption[] = [
{
key: 'raw',
label: 'Raw',
value: 'raw',
},
{
key: 'table',
label: 'Table',
value: 'table',
},
{
key: 'list',
label: 'List',
value: 'list',
},
];
export const logsOptions = ['raw', 'table'];
export const defaultSelectStyle: React.CSSProperties = {
minWidth: '6rem',
};

View File

@ -0,0 +1,78 @@
// utils
import get from 'api/browser/localstorage/get';
import { LOCALSTORAGE } from 'constants/localStorage';
// interfaces
import { LogViewMode } from 'container/LogsTable';
import { useCallback, useLayoutEffect, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { setLinesPerRow } from 'store/actions/logs/setLInesPerRow';
// actions
import { setViewMode } from 'store/actions/logs/setViewMode';
import { AppState } from 'store/reducers';
import { viewModeOptionList } from './config';
import { SelectedLogViewData } from './types';
import { isLogViewMode } from './utils';
export const useSelectedLogView = (): SelectedLogViewData => {
const dispatch = useDispatch();
const viewMode = useSelector<AppState, LogViewMode>(
(state) => state.logs.viewMode,
);
const linesPerRow = useSelector<AppState, number>(
(state) => state.logs.linesPerRow,
);
const viewModeOption = useMemo(
() =>
viewModeOptionList.find(
(viewModeOption) => viewModeOption.value === viewMode,
) ?? viewModeOptionList[0],
[viewMode],
);
const handleViewModeChange = useCallback(
(selectedViewMode: LogViewMode) => {
dispatch(setViewMode(selectedViewMode));
},
[dispatch],
);
const handleViewModeOptionChange = useCallback(
({ key }: { key: string }) => {
if (isLogViewMode(key)) handleViewModeChange(key);
},
[handleViewModeChange],
);
const handleLinesPerRowChange = useCallback(
(selectedLinesPerRow: unknown) => {
if (typeof selectedLinesPerRow === 'number') {
dispatch(setLinesPerRow(selectedLinesPerRow));
}
},
[dispatch],
);
useLayoutEffect(() => {
const storedViewMode = get(LOCALSTORAGE.LOGS_VIEW_MODE);
if (storedViewMode) {
handleViewModeChange(storedViewMode as LogViewMode);
}
const storedLinesPerRow = get(LOCALSTORAGE.LOGS_LINES_PER_ROW);
if (storedLinesPerRow) {
handleLinesPerRowChange(+storedLinesPerRow);
}
}, [handleViewModeChange, handleLinesPerRowChange]);
return {
viewModeOptionList,
viewModeOption,
viewMode,
handleViewModeChange,
handleViewModeOptionChange,
linesPerRow,
handleLinesPerRowChange,
};
};

View File

@ -1,4 +1,4 @@
import { Divider, Row } from 'antd';
import { Button, Col, Divider, Popover, Row, Select, Space } from 'antd';
import LogControls from 'container/LogControls';
import LogDetailedView from 'container/LogDetailedView';
import LogLiveTail from 'container/LogLiveTail';
@ -6,11 +6,67 @@ import LogsAggregate from 'container/LogsAggregate';
import LogsFilters from 'container/LogsFilters';
import LogsSearchFilter from 'container/LogsSearchFilter';
import LogsTable from 'container/LogsTable';
import React from 'react';
import React, { useCallback, useMemo } from 'react';
import { useDispatch } from 'react-redux';
import { Dispatch } from 'redux';
import AppActions from 'types/actions';
import { SET_DETAILED_LOG_DATA } from 'types/actions/logs';
import { ILog } from 'types/api/logs/log';
import { defaultSelectStyle, logsOptions } from './config';
import { useSelectedLogView } from './hooks';
import PopoverContent from './PopoverContent';
import SpaceContainer from './styles';
function Logs(): JSX.Element {
const dispatch = useDispatch<Dispatch<AppActions>>();
const showExpandedLog = useCallback(
(logData: ILog) => {
dispatch({
type: SET_DETAILED_LOG_DATA,
payload: logData,
});
},
[dispatch],
);
const {
viewModeOptionList,
viewModeOption,
viewMode,
handleViewModeOptionChange,
linesPerRow,
handleLinesPerRowChange,
} = useSelectedLogView();
const renderPopoverContent = useCallback(
() => (
<PopoverContent
linesPerRow={linesPerRow}
handleLinesPerRowChange={handleLinesPerRowChange}
/>
),
[linesPerRow, handleLinesPerRowChange],
);
const isFormatButtonVisible = useMemo(() => logsOptions.includes(viewMode), [
viewMode,
]);
const selectedViewModeOption = useMemo(() => viewModeOption.value.toString(), [
viewModeOption.value,
]);
const onChangeVeiwMode = useCallback(
(key: string) => {
handleViewModeOptionChange({
key,
});
},
[handleViewModeOptionChange],
);
return (
<>
<SpaceContainer
@ -23,13 +79,44 @@ function Logs(): JSX.Element {
</SpaceContainer>
<LogsAggregate />
<LogControls />
<Divider plain orientationMargin={1} />
<Row gutter={20} wrap={false}>
<LogsFilters />
<Divider type="vertical" />
<LogsTable />
<Col flex={1}>
<Row>
<Col flex={1}>
<Space align="baseline" direction="horizontal">
<Select
style={defaultSelectStyle}
value={selectedViewModeOption}
onChange={onChangeVeiwMode}
>
{viewModeOptionList.map((option) => (
<Select.Option key={option.value}>{option.label}</Select.Option>
))}
</Select>
{isFormatButtonVisible && (
<Popover placement="right" content={renderPopoverContent}>
<Button>Format</Button>
</Popover>
)}
</Space>
</Col>
<Col>
<LogControls />
</Col>
</Row>
<LogsTable
viewMode={viewMode}
linesPerRow={linesPerRow}
onClickExpand={showExpandedLog}
/>
</Col>
</Row>
<LogDetailedView />
</>
);

View File

@ -0,0 +1,17 @@
import { ItemType } from 'antd/es/menu/hooks/useItems';
import { LogViewMode } from 'container/LogsTable';
export type ViewModeOption = ItemType & {
label: string;
value: LogViewMode;
};
export type SelectedLogViewData = {
viewModeOptionList: ViewModeOption[];
viewModeOption: ViewModeOption;
viewMode: LogViewMode;
handleViewModeChange: (s: LogViewMode) => void;
handleViewModeOptionChange: ({ key }: { key: string }) => void;
linesPerRow: number;
handleLinesPerRowChange: (l: unknown) => void;
};

View File

@ -0,0 +1,7 @@
import { LogViewMode } from 'container/LogsTable';
import { viewModeOptionList } from './config';
export const isLogViewMode = (value: unknown): value is LogViewMode =>
typeof value === 'string' &&
viewModeOptionList.some((option) => option.key === value);

View File

@ -0,0 +1,14 @@
import set from 'api/browser/localstorage/set';
import { LOCALSTORAGE } from 'constants/localStorage';
import { SET_LINES_PER_ROW } from 'types/actions/logs';
type ActionSetLinesPerRow = { type: typeof SET_LINES_PER_ROW; payload: number };
export function setLinesPerRow(lines: number): ActionSetLinesPerRow {
set(LOCALSTORAGE.LOGS_LINES_PER_ROW, lines.toString());
return {
type: SET_LINES_PER_ROW,
payload: lines,
};
}

View File

@ -0,0 +1,15 @@
import set from 'api/browser/localstorage/set';
import { LOCALSTORAGE } from 'constants/localStorage';
import { LogViewMode } from 'container/LogsTable';
import { SET_VIEW_MODE } from 'types/actions/logs';
type ActionSetViewMode = { type: typeof SET_VIEW_MODE; payload: LogViewMode };
export function setViewMode(viewMode: LogViewMode): ActionSetViewMode {
set(LOCALSTORAGE.LOGS_VIEW_MODE, viewMode);
return {
type: SET_VIEW_MODE,
payload: viewMode,
};
}

View File

@ -10,6 +10,7 @@ import {
RESET_ID_START_AND_END,
SET_DETAILED_LOG_DATA,
SET_FIELDS,
SET_LINES_PER_ROW,
SET_LIVE_TAIL_START_TIME,
SET_LOADING,
SET_LOADING_AGGREGATE,
@ -18,6 +19,7 @@ import {
SET_LOGS_AGGREGATE_SERIES,
SET_SEARCH_QUERY_PARSED_PAYLOAD,
SET_SEARCH_QUERY_STRING,
SET_VIEW_MODE,
STOP_LIVE_TAIL,
TOGGLE_LIVE_TAIL,
UPDATE_INTERESTING_FIELDS,
@ -36,6 +38,8 @@ const initialState: ILogsReducer = {
},
logs: [],
logLinesPerPage: 25,
linesPerRow: 2,
viewMode: 'raw',
idEnd: '',
idStart: '',
isLoading: false,
@ -205,6 +209,20 @@ export const LogsReducer = (
};
}
case SET_LINES_PER_ROW: {
return {
...state,
linesPerRow: action.payload,
};
}
case SET_VIEW_MODE: {
return {
...state,
viewMode: action.payload,
};
}
case UPDATE_INTERESTING_FIELDS: {
return {
...state,

View File

@ -1,3 +1,4 @@
import { LogViewMode } from 'container/LogsTable';
import { ILogQLParsedQueryItem } from 'lib/logql/types';
import { IField, IFieldMoveToSelected, IFields } from 'types/api/logs/fields';
import { TLogsLiveTailState } from 'types/api/logs/liveTail';
@ -28,6 +29,8 @@ export const PUSH_LIVE_TAIL_EVENT = 'LOGS_PUSH_LIVE_TAIL_EVENT';
export const STOP_LIVE_TAIL = 'LOGS_STOP_LIVE_TAIL';
export const FLUSH_LOGS = 'LOGS_FLUSH_LOGS';
export const SET_LIVE_TAIL_START_TIME = 'LOGS_SET_LIVE_TAIL_START_TIME';
export const SET_LINES_PER_ROW = 'SET_LINES_PER_ROW';
export const SET_VIEW_MODE = 'SET_VIEW_MODE';
export const UPDATE_SELECTED_FIELDS = 'LOGS_UPDATE_SELECTED_FIELDS';
export const UPDATE_INTERESTING_FIELDS = 'LOGS_UPDATE_INTERESTING_FIELDS';
@ -118,6 +121,15 @@ export interface SetLiveTailStartTime {
payload: number;
}
export interface SetLinesPerRow {
type: typeof SET_LINES_PER_ROW;
payload: number;
}
export interface SetViewMode {
type: typeof SET_VIEW_MODE;
payload: LogViewMode;
}
type IFieldType = 'interesting' | 'selected';
export interface UpdateSelectedInterestFields {
@ -149,4 +161,6 @@ export type LogsActions =
| StopLiveTail
| FlushLogs
| SetLiveTailStartTime
| SetLinesPerRow
| SetViewMode
| UpdateSelectedInterestFields;

View File

@ -1,3 +1,4 @@
import { LogViewMode } from 'container/LogsTable';
import { ILogQLParsedQueryItem } from 'lib/logql/types';
import { IFields } from 'types/api/logs/fields';
import { TLogsLiveTailState } from 'types/api/logs/liveTail';
@ -12,6 +13,8 @@ export interface ILogsReducer {
};
logs: ILog[];
logLinesPerPage: number;
linesPerRow: number;
viewMode: LogViewMode;
idEnd: string;
idStart: string;
isLoading: boolean;