mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-06-04 11:25:52 +08:00
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:
parent
8965b9b503
commit
bad80def90
@ -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",
|
||||
|
@ -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;
|
5
frontend/src/components/Logs/RawLogView/config.ts
Normal file
5
frontend/src/components/Logs/RawLogView/config.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export const rawLineStyle: React.CSSProperties = {
|
||||
marginBottom: 0,
|
||||
fontFamily: "'Fira Code', monospace",
|
||||
fontWeight: 300,
|
||||
};
|
48
frontend/src/components/Logs/RawLogView/index.tsx
Normal file
48
frontend/src/components/Logs/RawLogView/index.tsx
Normal 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;
|
24
frontend/src/components/Logs/RawLogView/styles.ts
Normal file
24
frontend/src/components/Logs/RawLogView/styles.ts
Normal 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;
|
||||
`;
|
12
frontend/src/components/Logs/TableView/config.ts
Normal file
12
frontend/src/components/Logs/TableView/config.ts
Normal 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,
|
||||
};
|
106
frontend/src/components/Logs/TableView/index.tsx
Normal file
106
frontend/src/components/Logs/TableView/index.tsx
Normal 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;
|
@ -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} />
|
||||
|
@ -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',
|
||||
}
|
||||
|
@ -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, {
|
||||
maxTime,
|
||||
minTime,
|
||||
}),
|
||||
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>
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -48,6 +48,7 @@ function SearchFilter({
|
||||
AppState,
|
||||
ILogsReducer
|
||||
>((state) => state.logs);
|
||||
|
||||
const globalTime = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
|
@ -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} />;
|
||||
|
||||
if (viewMode === 'raw') {
|
||||
return (
|
||||
<RawLogView
|
||||
key={log.id}
|
||||
data={log}
|
||||
linesPerRow={linesPerRow}
|
||||
onClickExpand={onClickExpand}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <ListLogView key={log.id} logData={log} />;
|
||||
},
|
||||
[logs],
|
||||
[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">
|
||||
<Heading>
|
||||
<Typography.Text>Event</Typography.Text>
|
||||
</Heading>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
69
frontend/src/hooks/useFontObserver.tsx
Normal file
69
frontend/src/hooks/useFontObserver.tsx
Normal 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;
|
@ -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>
|
||||
|
28
frontend/src/pages/Logs/PopoverContent.tsx
Normal file
28
frontend/src/pages/Logs/PopoverContent.tsx
Normal 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;
|
25
frontend/src/pages/Logs/config.ts
Normal file
25
frontend/src/pages/Logs/config.ts
Normal 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',
|
||||
};
|
78
frontend/src/pages/Logs/hooks.ts
Normal file
78
frontend/src/pages/Logs/hooks.ts
Normal 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,
|
||||
};
|
||||
};
|
@ -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 />
|
||||
</>
|
||||
);
|
||||
|
17
frontend/src/pages/Logs/types.ts
Normal file
17
frontend/src/pages/Logs/types.ts
Normal 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;
|
||||
};
|
7
frontend/src/pages/Logs/utils.ts
Normal file
7
frontend/src/pages/Logs/utils.ts
Normal 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);
|
14
frontend/src/store/actions/logs/setLInesPerRow.ts
Normal file
14
frontend/src/store/actions/logs/setLInesPerRow.ts
Normal 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,
|
||||
};
|
||||
}
|
15
frontend/src/store/actions/logs/setViewMode.ts
Normal file
15
frontend/src/store/actions/logs/setViewMode.ts
Normal 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,
|
||||
};
|
||||
}
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user