mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-19 01:35:54 +08:00
feat: add the url pagination & update options menu (#2943)
* feat: add dynamic table based on query * fix: group by repeating * fix: change view when groupBy exist in the list * fix: table scroll * feat: add the pagination and update options menu * feat: trace explorer is updated --------- Co-authored-by: Yevhen Shevchenko <y.shevchenko@seedium.io> Co-authored-by: Nazarenko19 <danil.nazarenko2000@gmail.com> Co-authored-by: Palash Gupta <palashgdev@gmail.com>
This commit is contained in:
parent
bd18eee662
commit
56402b0d40
@ -233,6 +233,7 @@ export const PANEL_TYPES: Record<PanelTypeKeys, GRAPH_TYPES> = {
|
||||
TABLE: 'table',
|
||||
LIST: 'list',
|
||||
EMPTY_WIDGET: 'EMPTY_WIDGET',
|
||||
TRACE: 'trace',
|
||||
};
|
||||
|
||||
export type IQueryBuilderState = 'search';
|
||||
|
@ -1,33 +1,29 @@
|
||||
import { LeftOutlined, RightOutlined } from '@ant-design/icons';
|
||||
import { Button, Select } from 'antd';
|
||||
import { Pagination } from 'hooks/queryPagination';
|
||||
import { memo, useMemo } from 'react';
|
||||
|
||||
import { defaultSelectStyle, ITEMS_PER_PAGE_OPTIONS } from './config';
|
||||
import { Container } from './styles';
|
||||
|
||||
interface ControlsProps {
|
||||
count: number;
|
||||
countPerPage: number;
|
||||
isLoading: boolean;
|
||||
handleNavigatePrevious: () => void;
|
||||
handleNavigateNext: () => void;
|
||||
handleCountItemsPerPageChange: (e: number) => void;
|
||||
}
|
||||
|
||||
function Controls(props: ControlsProps): JSX.Element | null {
|
||||
const {
|
||||
count,
|
||||
isLoading,
|
||||
countPerPage,
|
||||
handleNavigatePrevious,
|
||||
handleNavigateNext,
|
||||
handleCountItemsPerPageChange,
|
||||
} = props;
|
||||
|
||||
function Controls({
|
||||
offset = 0,
|
||||
isLoading,
|
||||
totalCount,
|
||||
countPerPage,
|
||||
handleNavigatePrevious,
|
||||
handleNavigateNext,
|
||||
handleCountItemsPerPageChange,
|
||||
}: ControlsProps): JSX.Element | null {
|
||||
const isNextAndPreviousDisabled = useMemo(
|
||||
() => isLoading || countPerPage === 0 || count === 0 || count < countPerPage,
|
||||
[isLoading, countPerPage, count],
|
||||
() => isLoading || countPerPage < 0 || totalCount === 0,
|
||||
[isLoading, countPerPage, totalCount],
|
||||
);
|
||||
const isPreviousDisabled = useMemo(() => offset <= 0, [offset]);
|
||||
const isNextDisabled = useMemo(() => totalCount < countPerPage, [
|
||||
countPerPage,
|
||||
totalCount,
|
||||
]);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
@ -35,7 +31,7 @@ function Controls(props: ControlsProps): JSX.Element | null {
|
||||
loading={isLoading}
|
||||
size="small"
|
||||
type="link"
|
||||
disabled={isNextAndPreviousDisabled}
|
||||
disabled={isPreviousDisabled || isNextAndPreviousDisabled}
|
||||
onClick={handleNavigatePrevious}
|
||||
>
|
||||
<LeftOutlined /> Previous
|
||||
@ -44,12 +40,12 @@ function Controls(props: ControlsProps): JSX.Element | null {
|
||||
loading={isLoading}
|
||||
size="small"
|
||||
type="link"
|
||||
disabled={isNextAndPreviousDisabled}
|
||||
disabled={isNextDisabled || isNextAndPreviousDisabled}
|
||||
onClick={handleNavigateNext}
|
||||
>
|
||||
Next <RightOutlined />
|
||||
</Button>
|
||||
<Select
|
||||
<Select<Pagination['limit']>
|
||||
style={defaultSelectStyle}
|
||||
loading={isLoading}
|
||||
value={countPerPage}
|
||||
@ -66,4 +62,18 @@ function Controls(props: ControlsProps): JSX.Element | null {
|
||||
);
|
||||
}
|
||||
|
||||
Controls.defaultProps = {
|
||||
offset: 0,
|
||||
};
|
||||
|
||||
export interface ControlsProps {
|
||||
offset?: Pagination['offset'];
|
||||
totalCount: number;
|
||||
countPerPage: Pagination['limit'];
|
||||
isLoading: boolean;
|
||||
handleNavigatePrevious: () => void;
|
||||
handleNavigateNext: () => void;
|
||||
handleCountItemsPerPageChange: (value: Pagination['limit']) => void;
|
||||
}
|
||||
|
||||
export default memo(Controls);
|
||||
|
@ -5,6 +5,7 @@ import Controls from 'container/Controls';
|
||||
import { getGlobalTime } from 'container/LogsSearchFilter/utils';
|
||||
import { getMinMax } from 'container/TopNav/AutoRefresh/config';
|
||||
import dayjs from 'dayjs';
|
||||
import { Pagination } from 'hooks/queryPagination';
|
||||
import { FlatLogData } from 'lib/logs/flatLogData';
|
||||
import * as Papa from 'papaparse';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
@ -37,7 +38,7 @@ function LogControls(): JSX.Element | null {
|
||||
|
||||
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||
|
||||
const handleLogLinesPerPageChange = (e: number): void => {
|
||||
const handleLogLinesPerPageChange = (e: Pagination['limit']): void => {
|
||||
dispatch({
|
||||
type: SET_LOG_LINES_PER_PAGE,
|
||||
payload: {
|
||||
@ -166,7 +167,7 @@ function LogControls(): JSX.Element | null {
|
||||
<Divider type="vertical" />
|
||||
<Controls
|
||||
isLoading={isLoading}
|
||||
count={logs.length}
|
||||
totalCount={logs.length}
|
||||
countPerPage={logLinesPerPage}
|
||||
handleNavigatePrevious={handleNavigatePrevious}
|
||||
handleNavigateNext={handleNavigateNext}
|
||||
|
@ -16,7 +16,13 @@ const Items: ItemsProps[] = [
|
||||
},
|
||||
];
|
||||
|
||||
export type ITEMS = 'graph' | 'value' | 'list' | 'table' | 'EMPTY_WIDGET';
|
||||
export type ITEMS =
|
||||
| 'graph'
|
||||
| 'value'
|
||||
| 'list'
|
||||
| 'table'
|
||||
| 'EMPTY_WIDGET'
|
||||
| 'trace';
|
||||
|
||||
interface ItemsProps {
|
||||
name: ITEMS;
|
||||
|
@ -1,11 +1,18 @@
|
||||
import { SearchOutlined } from '@ant-design/icons';
|
||||
import { Input } from 'antd';
|
||||
import Typography from 'antd/es/typography/Typography';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { OptionsMenuConfig } from '..';
|
||||
import { FieldTitle } from '../styles';
|
||||
import { AddColumnSelect, AddColumnWrapper, SearchIconWrapper } from './styles';
|
||||
import { OptionsMenuConfig } from '../types';
|
||||
import {
|
||||
AddColumnItem,
|
||||
AddColumnSelect,
|
||||
AddColumnWrapper,
|
||||
DeleteOutlinedIcon,
|
||||
SearchIconWrapper,
|
||||
} from './styles';
|
||||
|
||||
function AddColumnField({ config }: AddColumnFieldProps): JSX.Element | null {
|
||||
const { t } = useTranslation(['trace']);
|
||||
@ -19,19 +26,32 @@ function AddColumnField({ config }: AddColumnFieldProps): JSX.Element | null {
|
||||
|
||||
<Input.Group compact>
|
||||
<AddColumnSelect
|
||||
allowClear
|
||||
maxTagCount={0}
|
||||
size="small"
|
||||
mode="multiple"
|
||||
placeholder="Search"
|
||||
options={config.options}
|
||||
value={config.value}
|
||||
value={[]}
|
||||
onChange={config.onChange}
|
||||
/>
|
||||
<SearchIconWrapper $isDarkMode={isDarkMode}>
|
||||
<SearchOutlined />
|
||||
</SearchIconWrapper>
|
||||
</Input.Group>
|
||||
|
||||
{config.value.map((selectedValue: string) => {
|
||||
const option = config?.options?.find(
|
||||
({ value }) => value === selectedValue,
|
||||
);
|
||||
|
||||
return (
|
||||
<AddColumnItem direction="horizontal" key={option?.value}>
|
||||
<Typography>{option?.label}</Typography>
|
||||
<DeleteOutlinedIcon
|
||||
onClick={(): void => config.onRemove(selectedValue)}
|
||||
/>
|
||||
</AddColumnItem>
|
||||
);
|
||||
})}
|
||||
</AddColumnWrapper>
|
||||
);
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { DeleteOutlined } from '@ant-design/icons';
|
||||
import { Card, Select, SelectProps, Space } from 'antd';
|
||||
import { themeColors } from 'constants/theme';
|
||||
import { FunctionComponent } from 'react';
|
||||
@ -26,3 +27,13 @@ export const AddColumnSelect: FunctionComponent<SelectProps> = styled(
|
||||
export const AddColumnWrapper = styled(Space)`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const AddColumnItem = styled(Space)`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
`;
|
||||
|
||||
export const DeleteOutlinedIcon = styled(DeleteOutlined)`
|
||||
color: red;
|
||||
`;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { OptionsMenuConfig } from '..';
|
||||
import { FieldTitle } from '../styles';
|
||||
import { OptionsMenuConfig } from '../types';
|
||||
import { FormatFieldWrapper, RadioButton, RadioGroup } from './styles';
|
||||
|
||||
function FormatField({ config }: FormatFieldProps): JSX.Element | null {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { OptionsMenuConfig } from '..';
|
||||
import { FieldTitle } from '../styles';
|
||||
import { OptionsMenuConfig } from '../types';
|
||||
import { MaxLinesFieldWrapper, MaxLinesInput } from './styles';
|
||||
|
||||
function MaxLinesField({ config }: MaxLinesFieldProps): JSX.Element | null {
|
||||
|
9
frontend/src/container/OptionsMenu/constants.ts
Normal file
9
frontend/src/container/OptionsMenu/constants.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { OptionsQuery } from './types';
|
||||
|
||||
export const URL_OPTIONS = 'options';
|
||||
|
||||
export const defaultOptionsQuery: OptionsQuery = {
|
||||
selectColumns: [],
|
||||
maxLines: 0,
|
||||
format: 'default',
|
||||
};
|
@ -1,11 +1,5 @@
|
||||
import { SettingFilled, SettingOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
InputNumberProps,
|
||||
Popover,
|
||||
RadioProps,
|
||||
SelectProps,
|
||||
Space,
|
||||
} from 'antd';
|
||||
import { Popover, Space } from 'antd';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@ -14,6 +8,12 @@ import AddColumnField from './AddColumnField';
|
||||
import FormatField from './FormatField';
|
||||
import MaxLinesField from './MaxLinesField';
|
||||
import { OptionsContainer, OptionsContentWrapper } from './styles';
|
||||
import { OptionsMenuConfig } from './types';
|
||||
import useOptionsMenu from './useOptionsMenu';
|
||||
|
||||
interface OptionsMenuProps {
|
||||
config: OptionsMenuConfig;
|
||||
}
|
||||
|
||||
function OptionsMenu({ config }: OptionsMenuProps): JSX.Element {
|
||||
const { t } = useTranslation(['trace']);
|
||||
@ -44,14 +44,6 @@ function OptionsMenu({ config }: OptionsMenuProps): JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
export type OptionsMenuConfig = {
|
||||
format?: Pick<RadioProps, 'value' | 'onChange'>;
|
||||
maxLines?: Pick<InputNumberProps, 'value' | 'onChange'>;
|
||||
addColumn?: Pick<SelectProps, 'options' | 'value' | 'onChange'>;
|
||||
};
|
||||
|
||||
interface OptionsMenuProps {
|
||||
config: OptionsMenuConfig;
|
||||
}
|
||||
|
||||
export default OptionsMenu;
|
||||
|
||||
export { useOptionsMenu };
|
||||
|
21
frontend/src/container/OptionsMenu/types.ts
Normal file
21
frontend/src/container/OptionsMenu/types.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { InputNumberProps, RadioProps, SelectProps } from 'antd';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
export interface OptionsQuery {
|
||||
selectColumns: BaseAutocompleteData[];
|
||||
maxLines: number;
|
||||
format: 'default' | 'row' | 'column';
|
||||
}
|
||||
|
||||
export interface InitialOptions
|
||||
extends Omit<Partial<OptionsQuery>, 'selectColumns'> {
|
||||
selectColumns?: string[];
|
||||
}
|
||||
|
||||
export type OptionsMenuConfig = {
|
||||
format?: Pick<RadioProps, 'value' | 'onChange'>;
|
||||
maxLines?: Pick<InputNumberProps, 'value' | 'onChange'>;
|
||||
addColumn?: Pick<SelectProps, 'options' | 'value' | 'onChange'> & {
|
||||
onRemove: (key: string) => void;
|
||||
};
|
||||
};
|
166
frontend/src/container/OptionsMenu/useOptionsMenu.ts
Normal file
166
frontend/src/container/OptionsMenu/useOptionsMenu.ts
Normal file
@ -0,0 +1,166 @@
|
||||
import { RadioChangeEvent } from 'antd';
|
||||
import { getAggregateKeys } from 'api/queryBuilder/getAttributeKeys';
|
||||
import { QueryBuilderKeys } from 'constants/queryBuilder';
|
||||
import useUrlQueryData from 'hooks/useUrlQueryData';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { defaultOptionsQuery, URL_OPTIONS } from './constants';
|
||||
import { InitialOptions, OptionsMenuConfig, OptionsQuery } from './types';
|
||||
import { getInitialColumns, getOptionsFromKeys } from './utils';
|
||||
|
||||
interface UseOptionsMenuProps {
|
||||
dataSource: DataSource;
|
||||
aggregateOperator: string;
|
||||
initialOptions?: InitialOptions;
|
||||
}
|
||||
|
||||
interface UseOptionsMenu {
|
||||
isLoading: boolean;
|
||||
options: OptionsQuery;
|
||||
config: OptionsMenuConfig;
|
||||
}
|
||||
|
||||
const useOptionsMenu = ({
|
||||
dataSource,
|
||||
aggregateOperator,
|
||||
initialOptions = {},
|
||||
}: UseOptionsMenuProps): UseOptionsMenu => {
|
||||
const {
|
||||
query: optionsQuery,
|
||||
queryData: optionsQueryData,
|
||||
redirectWithQuery: redirectWithOptionsData,
|
||||
} = useUrlQueryData<OptionsQuery>(URL_OPTIONS);
|
||||
|
||||
const { data, isFetched, isLoading } = useQuery(
|
||||
[QueryBuilderKeys.GET_ATTRIBUTE_KEY],
|
||||
async () =>
|
||||
getAggregateKeys({
|
||||
searchText: '',
|
||||
dataSource,
|
||||
aggregateOperator,
|
||||
aggregateAttribute: '',
|
||||
}),
|
||||
);
|
||||
|
||||
const attributeKeys = useMemo(() => data?.payload?.attributeKeys || [], [
|
||||
data?.payload?.attributeKeys,
|
||||
]);
|
||||
|
||||
const initialOptionsQuery: OptionsQuery = useMemo(
|
||||
() => ({
|
||||
...defaultOptionsQuery,
|
||||
...initialOptions,
|
||||
selectColumns: initialOptions?.selectColumns
|
||||
? getInitialColumns(initialOptions?.selectColumns || [], attributeKeys)
|
||||
: defaultOptionsQuery.selectColumns,
|
||||
}),
|
||||
[initialOptions, attributeKeys],
|
||||
);
|
||||
|
||||
const options = useMemo(() => getOptionsFromKeys(attributeKeys), [
|
||||
attributeKeys,
|
||||
]);
|
||||
|
||||
const selectedColumnKeys = useMemo(
|
||||
() => optionsQueryData?.selectColumns?.map(({ id }) => id) || [],
|
||||
[optionsQueryData],
|
||||
);
|
||||
|
||||
const handleSelectedColumnsChange = useCallback(
|
||||
(value: string[]) => {
|
||||
const newSelectedColumnKeys = [
|
||||
...new Set([...selectedColumnKeys, ...value]),
|
||||
];
|
||||
const newSelectedColumns = newSelectedColumnKeys.reduce((acc, key) => {
|
||||
const column = attributeKeys.find(({ id }) => id === key);
|
||||
|
||||
if (!column) return acc;
|
||||
return [...acc, column];
|
||||
}, [] as BaseAutocompleteData[]);
|
||||
|
||||
redirectWithOptionsData({
|
||||
...defaultOptionsQuery,
|
||||
selectColumns: newSelectedColumns,
|
||||
});
|
||||
},
|
||||
[attributeKeys, selectedColumnKeys, redirectWithOptionsData],
|
||||
);
|
||||
|
||||
const handleRemoveSelectedColumn = useCallback(
|
||||
(columnKey: string) => {
|
||||
redirectWithOptionsData({
|
||||
...defaultOptionsQuery,
|
||||
selectColumns: optionsQueryData?.selectColumns?.filter(
|
||||
({ id }) => id !== columnKey,
|
||||
),
|
||||
});
|
||||
},
|
||||
[optionsQueryData, redirectWithOptionsData],
|
||||
);
|
||||
|
||||
const handleFormatChange = useCallback(
|
||||
(event: RadioChangeEvent) => {
|
||||
redirectWithOptionsData({
|
||||
...defaultOptionsQuery,
|
||||
format: event.target.value,
|
||||
});
|
||||
},
|
||||
[redirectWithOptionsData],
|
||||
);
|
||||
|
||||
const handleMaxLinesChange = useCallback(
|
||||
(value: string | number | null) => {
|
||||
redirectWithOptionsData({
|
||||
...defaultOptionsQuery,
|
||||
maxLines: value as number,
|
||||
});
|
||||
},
|
||||
[redirectWithOptionsData],
|
||||
);
|
||||
|
||||
const optionsMenuConfig: Required<OptionsMenuConfig> = useMemo(
|
||||
() => ({
|
||||
addColumn: {
|
||||
value: selectedColumnKeys || defaultOptionsQuery.selectColumns,
|
||||
options: options || [],
|
||||
onChange: handleSelectedColumnsChange,
|
||||
onRemove: handleRemoveSelectedColumn,
|
||||
},
|
||||
format: {
|
||||
value: optionsQueryData?.format || defaultOptionsQuery.format,
|
||||
onChange: handleFormatChange,
|
||||
},
|
||||
maxLines: {
|
||||
value: optionsQueryData?.maxLines || defaultOptionsQuery.maxLines,
|
||||
onChange: handleMaxLinesChange,
|
||||
},
|
||||
}),
|
||||
[
|
||||
options,
|
||||
selectedColumnKeys,
|
||||
optionsQueryData?.maxLines,
|
||||
optionsQueryData?.format,
|
||||
handleSelectedColumnsChange,
|
||||
handleRemoveSelectedColumn,
|
||||
handleFormatChange,
|
||||
handleMaxLinesChange,
|
||||
],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (optionsQuery || !isFetched) return;
|
||||
|
||||
redirectWithOptionsData(initialOptionsQuery);
|
||||
}, [isFetched, optionsQuery, initialOptionsQuery, redirectWithOptionsData]);
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
options: optionsQueryData,
|
||||
config: optionsMenuConfig,
|
||||
};
|
||||
};
|
||||
|
||||
export default useOptionsMenu;
|
22
frontend/src/container/OptionsMenu/utils.ts
Normal file
22
frontend/src/container/OptionsMenu/utils.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { SelectProps } from 'antd';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
export const getOptionsFromKeys = (
|
||||
keys: BaseAutocompleteData[],
|
||||
): SelectProps['options'] =>
|
||||
keys.map(({ id, key }) => ({
|
||||
label: key,
|
||||
value: id,
|
||||
}));
|
||||
|
||||
export const getInitialColumns = (
|
||||
initialColumnTitles: string[],
|
||||
attributeKeys: BaseAutocompleteData[],
|
||||
): BaseAutocompleteData[] =>
|
||||
initialColumnTitles.reduce((acc, title) => {
|
||||
const initialColumn = attributeKeys.find(({ key }) => title === key);
|
||||
|
||||
if (!initialColumn) return acc;
|
||||
|
||||
return [...acc, initialColumn];
|
||||
}, [] as BaseAutocompleteData[]);
|
@ -12,8 +12,8 @@ function TraceExplorerControls(): JSX.Element | null {
|
||||
<Container>
|
||||
<Controls
|
||||
isLoading={false}
|
||||
count={0}
|
||||
countPerPage={0}
|
||||
totalCount={0}
|
||||
countPerPage={25}
|
||||
handleNavigatePrevious={handleNavigatePrevious}
|
||||
handleNavigateNext={handleNavigateNext}
|
||||
handleCountItemsPerPageChange={handleCountItemsPerPageChange}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { Button } from 'antd';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { QueryBuilder } from 'container/QueryBuilder';
|
||||
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
@ -9,10 +10,12 @@ import { ButtonWrapper, Container } from './styles';
|
||||
function QuerySection(): JSX.Element {
|
||||
const { handleRunQuery } = useQueryBuilder();
|
||||
|
||||
const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.TIME_SERIES);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<QueryBuilder
|
||||
panelType={PANEL_TYPES.TIME_SERIES}
|
||||
panelType={panelTypes}
|
||||
config={{
|
||||
queryVariant: 'static',
|
||||
initialDataSource: DataSource.TRACES,
|
||||
|
8
frontend/src/hooks/queryPagination/config.ts
Normal file
8
frontend/src/hooks/queryPagination/config.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { Pagination } from './types';
|
||||
|
||||
export const URL_PAGINATION = 'pagination';
|
||||
|
||||
export const defaultPaginationConfig: Pagination = {
|
||||
offset: 0,
|
||||
limit: 25,
|
||||
};
|
2
frontend/src/hooks/queryPagination/index.ts
Normal file
2
frontend/src/hooks/queryPagination/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './config';
|
||||
export * from './types';
|
4
frontend/src/hooks/queryPagination/types.ts
Normal file
4
frontend/src/hooks/queryPagination/types.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export interface Pagination {
|
||||
offset: number;
|
||||
limit: 25 | 50 | 100 | 200;
|
||||
}
|
70
frontend/src/hooks/queryPagination/useQueryPagination.ts
Normal file
70
frontend/src/hooks/queryPagination/useQueryPagination.ts
Normal file
@ -0,0 +1,70 @@
|
||||
import { ControlsProps } from 'container/Controls';
|
||||
import useUrlQueryData from 'hooks/useUrlQueryData';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
|
||||
import { defaultPaginationConfig, URL_PAGINATION } from './config';
|
||||
import { Pagination } from './types';
|
||||
import { checkIsValidPaginationData } from './utils';
|
||||
|
||||
const useQueryPagination = (totalCount: number): UseQueryPagination => {
|
||||
const {
|
||||
query: paginationQuery,
|
||||
queryData: paginationQueryData,
|
||||
redirectWithQuery: redirectWithCurrentPagination,
|
||||
} = useUrlQueryData<Pagination>(URL_PAGINATION);
|
||||
|
||||
const handleCountItemsPerPageChange = useCallback(
|
||||
(newLimit: Pagination['limit']) => {
|
||||
redirectWithCurrentPagination({
|
||||
...paginationQueryData,
|
||||
limit: newLimit,
|
||||
});
|
||||
},
|
||||
[paginationQueryData, redirectWithCurrentPagination],
|
||||
);
|
||||
|
||||
const handleNavigatePrevious = useCallback(() => {
|
||||
const previousOffset = paginationQueryData.offset - paginationQueryData.limit;
|
||||
|
||||
redirectWithCurrentPagination({
|
||||
...paginationQueryData,
|
||||
offset: previousOffset > 0 ? previousOffset : 0,
|
||||
});
|
||||
}, [paginationQueryData, redirectWithCurrentPagination]);
|
||||
|
||||
const handleNavigateNext = useCallback(() => {
|
||||
redirectWithCurrentPagination({
|
||||
...paginationQueryData,
|
||||
offset:
|
||||
paginationQueryData.limit === totalCount
|
||||
? paginationQueryData.offset + paginationQueryData.limit
|
||||
: paginationQueryData.offset,
|
||||
});
|
||||
}, [totalCount, paginationQueryData, redirectWithCurrentPagination]);
|
||||
|
||||
useEffect(() => {
|
||||
const isValidPaginationData = checkIsValidPaginationData(
|
||||
paginationQueryData || defaultPaginationConfig,
|
||||
);
|
||||
|
||||
if (paginationQuery && isValidPaginationData) return;
|
||||
|
||||
redirectWithCurrentPagination(defaultPaginationConfig);
|
||||
}, [paginationQuery, paginationQueryData, redirectWithCurrentPagination]);
|
||||
|
||||
return {
|
||||
pagination: paginationQueryData || defaultPaginationConfig,
|
||||
handleCountItemsPerPageChange,
|
||||
handleNavigatePrevious,
|
||||
handleNavigateNext,
|
||||
};
|
||||
};
|
||||
|
||||
type UseQueryPagination = Pick<
|
||||
ControlsProps,
|
||||
| 'handleCountItemsPerPageChange'
|
||||
| 'handleNavigateNext'
|
||||
| 'handleNavigatePrevious'
|
||||
> & { pagination: Pagination };
|
||||
|
||||
export default useQueryPagination;
|
12
frontend/src/hooks/queryPagination/utils.ts
Normal file
12
frontend/src/hooks/queryPagination/utils.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { Pagination } from './types';
|
||||
|
||||
export const checkIsValidPaginationData = ({
|
||||
limit,
|
||||
offset,
|
||||
}: Pagination): boolean =>
|
||||
Boolean(
|
||||
limit &&
|
||||
(limit === 25 || limit === 50 || limit === 100 || limit === 200) &&
|
||||
offset &&
|
||||
offset > 0,
|
||||
);
|
44
frontend/src/hooks/useUrlQueryData.ts
Normal file
44
frontend/src/hooks/useUrlQueryData.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
|
||||
const useUrlQueryData = <T>(
|
||||
queryKey: string,
|
||||
defaultData?: T,
|
||||
): UseUrlQueryData<T> => {
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
const urlQuery = useUrlQuery();
|
||||
|
||||
const query = urlQuery.get(queryKey);
|
||||
|
||||
const queryData: T = useMemo(() => (query ? JSON.parse(query) : defaultData), [
|
||||
query,
|
||||
defaultData,
|
||||
]);
|
||||
|
||||
const redirectWithQuery = useCallback(
|
||||
(newQueryData: T): void => {
|
||||
const newQuery = JSON.stringify(newQueryData);
|
||||
|
||||
urlQuery.set(queryKey, newQuery);
|
||||
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
|
||||
history.push(generatedUrl);
|
||||
},
|
||||
[history, location, urlQuery, queryKey],
|
||||
);
|
||||
|
||||
return {
|
||||
query,
|
||||
queryData,
|
||||
redirectWithQuery,
|
||||
};
|
||||
};
|
||||
|
||||
interface UseUrlQueryData<T> {
|
||||
query: string | null;
|
||||
queryData: T;
|
||||
redirectWithQuery: (newQueryData: T) => void;
|
||||
}
|
||||
|
||||
export default useUrlQueryData;
|
@ -12,7 +12,7 @@ import { DataSource } from 'types/common/queryBuilder';
|
||||
// ** Styles
|
||||
import { ButtonWrapperStyled, WrapperStyled } from './styles';
|
||||
|
||||
function LogsExporer(): JSX.Element {
|
||||
function LogsExplorer(): JSX.Element {
|
||||
const { handleRunQuery, updateAllQueriesOperators } = useQueryBuilder();
|
||||
const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
|
||||
|
||||
@ -53,4 +53,4 @@ function LogsExporer(): JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
export default LogsExporer;
|
||||
export default LogsExplorer;
|
||||
|
@ -1,6 +0,0 @@
|
||||
export const CURRENT_TRACES_EXPLORER_TAB = 'currentTab';
|
||||
|
||||
export enum TracesExplorerTabs {
|
||||
TIME_SERIES = 'times-series',
|
||||
TRACES = 'traces',
|
||||
}
|
@ -1,56 +1,71 @@
|
||||
import { Tabs } from 'antd';
|
||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { PANEL_TYPES_QUERY } from 'constants/queryBuilderQueryNames';
|
||||
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
|
||||
import QuerySection from 'container/TracesExplorer/QuerySection';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { CURRENT_TRACES_EXPLORER_TAB, TracesExplorerTabs } from './constants';
|
||||
import { Container } from './styles';
|
||||
import { getTabsItems } from './utils';
|
||||
|
||||
function TracesExplorer(): JSX.Element {
|
||||
const urlQuery = useUrlQuery();
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
const {
|
||||
updateAllQueriesOperators,
|
||||
redirectWithQueryBuilderData,
|
||||
currentQuery,
|
||||
panelType,
|
||||
} = useQueryBuilder();
|
||||
|
||||
const currentUrlTab = urlQuery.get(
|
||||
CURRENT_TRACES_EXPLORER_TAB,
|
||||
) as TracesExplorerTabs;
|
||||
const currentTab = currentUrlTab || TracesExplorerTabs.TIME_SERIES;
|
||||
const tabsItems = getTabsItems();
|
||||
|
||||
const redirectWithCurrentTab = useCallback(
|
||||
(tabKey: string): void => {
|
||||
urlQuery.set(CURRENT_TRACES_EXPLORER_TAB, tabKey);
|
||||
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
|
||||
history.push(generatedUrl);
|
||||
},
|
||||
[history, location, urlQuery],
|
||||
);
|
||||
const currentTab = panelType || PANEL_TYPES.TIME_SERIES;
|
||||
|
||||
const handleTabChange = useCallback(
|
||||
(tabKey: string): void => {
|
||||
redirectWithCurrentTab(tabKey);
|
||||
(newPanelType: string): void => {
|
||||
if (panelType === newPanelType) return;
|
||||
|
||||
const query = updateAllQueriesOperators(
|
||||
currentQuery,
|
||||
newPanelType as GRAPH_TYPES,
|
||||
DataSource.TRACES,
|
||||
);
|
||||
|
||||
redirectWithQueryBuilderData(query, { [PANEL_TYPES_QUERY]: newPanelType });
|
||||
},
|
||||
[redirectWithCurrentTab],
|
||||
[
|
||||
currentQuery,
|
||||
panelType,
|
||||
redirectWithQueryBuilderData,
|
||||
updateAllQueriesOperators,
|
||||
],
|
||||
);
|
||||
|
||||
useShareBuilderUrl(initialQueriesMap.traces);
|
||||
const defaultValue = useMemo(
|
||||
() =>
|
||||
updateAllQueriesOperators(
|
||||
initialQueriesMap.traces,
|
||||
PANEL_TYPES.TIME_SERIES,
|
||||
DataSource.TRACES,
|
||||
),
|
||||
[updateAllQueriesOperators],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentUrlTab) return;
|
||||
|
||||
redirectWithCurrentTab(TracesExplorerTabs.TIME_SERIES);
|
||||
}, [currentUrlTab, redirectWithCurrentTab]);
|
||||
useShareBuilderUrl(defaultValue);
|
||||
|
||||
return (
|
||||
<>
|
||||
<QuerySection />
|
||||
|
||||
<Container>
|
||||
<Tabs activeKey={currentTab} items={tabsItems} onChange={handleTabChange} />
|
||||
<Tabs
|
||||
defaultActiveKey={currentTab}
|
||||
activeKey={currentTab}
|
||||
items={tabsItems}
|
||||
onChange={handleTabChange}
|
||||
/>
|
||||
</Container>
|
||||
</>
|
||||
);
|
||||
|
@ -1,17 +1,16 @@
|
||||
import { TabsProps } from 'antd';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import TimeSeriesView from 'container/TracesExplorer/TimeSeriesView';
|
||||
|
||||
import { TracesExplorerTabs } from './constants';
|
||||
|
||||
export const getTabsItems = (): TabsProps['items'] => [
|
||||
{
|
||||
label: 'Time Series',
|
||||
key: TracesExplorerTabs.TIME_SERIES,
|
||||
key: PANEL_TYPES.TIME_SERIES,
|
||||
children: <TimeSeriesView />,
|
||||
},
|
||||
{
|
||||
label: 'Traces',
|
||||
key: TracesExplorerTabs.TRACES,
|
||||
key: PANEL_TYPES.TRACE,
|
||||
children: <div>Traces tab</div>,
|
||||
},
|
||||
];
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { LogViewMode } from 'container/LogsTable';
|
||||
import { Pagination } from 'hooks/queryPagination';
|
||||
import { ILogQLParsedQueryItem } from 'lib/logql/types';
|
||||
import { IField, IFieldMoveToSelected, IFields } from 'types/api/logs/fields';
|
||||
import { TLogsLiveTailState } from 'types/api/logs/liveTail';
|
||||
@ -70,7 +71,7 @@ export interface UpdateLogs {
|
||||
export interface SetLogsLinesPerPage {
|
||||
type: typeof SET_LOG_LINES_PER_PAGE;
|
||||
payload: {
|
||||
logsLinesPerPage: number;
|
||||
logsLinesPerPage: Pagination['limit'];
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -143,7 +143,8 @@ export type PanelTypeKeys =
|
||||
| 'VALUE'
|
||||
| 'TABLE'
|
||||
| 'LIST'
|
||||
| 'EMPTY_WIDGET';
|
||||
| 'EMPTY_WIDGET'
|
||||
| 'TRACE';
|
||||
|
||||
export type ReduceOperators = 'last' | 'sum' | 'avg' | 'max' | 'min';
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { LogViewMode } from 'container/LogsTable';
|
||||
import { Pagination } from 'hooks/queryPagination';
|
||||
import { ILogQLParsedQueryItem } from 'lib/logql/types';
|
||||
import { IFields } from 'types/api/logs/fields';
|
||||
import { TLogsLiveTailState } from 'types/api/logs/liveTail';
|
||||
@ -12,7 +13,7 @@ export interface ILogsReducer {
|
||||
parsedQuery: ILogQLParsedQueryItem[];
|
||||
};
|
||||
logs: ILog[];
|
||||
logLinesPerPage: number;
|
||||
logLinesPerPage: Pagination['limit'];
|
||||
linesPerRow: number;
|
||||
viewMode: LogViewMode;
|
||||
idEnd: string;
|
||||
|
Loading…
x
Reference in New Issue
Block a user