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:
dnazarenkoo 2023-06-23 21:39:59 +03:00 committed by GitHub
parent bd18eee662
commit 56402b0d40
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 512 additions and 99 deletions

View File

@ -233,6 +233,7 @@ export const PANEL_TYPES: Record<PanelTypeKeys, GRAPH_TYPES> = {
TABLE: 'table', TABLE: 'table',
LIST: 'list', LIST: 'list',
EMPTY_WIDGET: 'EMPTY_WIDGET', EMPTY_WIDGET: 'EMPTY_WIDGET',
TRACE: 'trace',
}; };
export type IQueryBuilderState = 'search'; export type IQueryBuilderState = 'search';

View File

@ -1,33 +1,29 @@
import { LeftOutlined, RightOutlined } from '@ant-design/icons'; import { LeftOutlined, RightOutlined } from '@ant-design/icons';
import { Button, Select } from 'antd'; import { Button, Select } from 'antd';
import { Pagination } from 'hooks/queryPagination';
import { memo, useMemo } from 'react'; import { memo, useMemo } from 'react';
import { defaultSelectStyle, ITEMS_PER_PAGE_OPTIONS } from './config'; import { defaultSelectStyle, ITEMS_PER_PAGE_OPTIONS } from './config';
import { Container } from './styles'; import { Container } from './styles';
interface ControlsProps { function Controls({
count: number; offset = 0,
countPerPage: number; isLoading,
isLoading: boolean; totalCount,
handleNavigatePrevious: () => void; countPerPage,
handleNavigateNext: () => void; handleNavigatePrevious,
handleCountItemsPerPageChange: (e: number) => void; handleNavigateNext,
} handleCountItemsPerPageChange,
}: ControlsProps): JSX.Element | null {
function Controls(props: ControlsProps): JSX.Element | null {
const {
count,
isLoading,
countPerPage,
handleNavigatePrevious,
handleNavigateNext,
handleCountItemsPerPageChange,
} = props;
const isNextAndPreviousDisabled = useMemo( const isNextAndPreviousDisabled = useMemo(
() => isLoading || countPerPage === 0 || count === 0 || count < countPerPage, () => isLoading || countPerPage < 0 || totalCount === 0,
[isLoading, countPerPage, count], [isLoading, countPerPage, totalCount],
); );
const isPreviousDisabled = useMemo(() => offset <= 0, [offset]);
const isNextDisabled = useMemo(() => totalCount < countPerPage, [
countPerPage,
totalCount,
]);
return ( return (
<Container> <Container>
@ -35,7 +31,7 @@ function Controls(props: ControlsProps): JSX.Element | null {
loading={isLoading} loading={isLoading}
size="small" size="small"
type="link" type="link"
disabled={isNextAndPreviousDisabled} disabled={isPreviousDisabled || isNextAndPreviousDisabled}
onClick={handleNavigatePrevious} onClick={handleNavigatePrevious}
> >
<LeftOutlined /> Previous <LeftOutlined /> Previous
@ -44,12 +40,12 @@ function Controls(props: ControlsProps): JSX.Element | null {
loading={isLoading} loading={isLoading}
size="small" size="small"
type="link" type="link"
disabled={isNextAndPreviousDisabled} disabled={isNextDisabled || isNextAndPreviousDisabled}
onClick={handleNavigateNext} onClick={handleNavigateNext}
> >
Next <RightOutlined /> Next <RightOutlined />
</Button> </Button>
<Select <Select<Pagination['limit']>
style={defaultSelectStyle} style={defaultSelectStyle}
loading={isLoading} loading={isLoading}
value={countPerPage} 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); export default memo(Controls);

View File

@ -5,6 +5,7 @@ import Controls from 'container/Controls';
import { getGlobalTime } from 'container/LogsSearchFilter/utils'; import { getGlobalTime } from 'container/LogsSearchFilter/utils';
import { getMinMax } from 'container/TopNav/AutoRefresh/config'; import { getMinMax } from 'container/TopNav/AutoRefresh/config';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { Pagination } from 'hooks/queryPagination';
import { FlatLogData } from 'lib/logs/flatLogData'; import { FlatLogData } from 'lib/logs/flatLogData';
import * as Papa from 'papaparse'; import * as Papa from 'papaparse';
import { memo, useCallback, useMemo } from 'react'; import { memo, useCallback, useMemo } from 'react';
@ -37,7 +38,7 @@ function LogControls(): JSX.Element | null {
const dispatch = useDispatch<Dispatch<AppActions>>(); const dispatch = useDispatch<Dispatch<AppActions>>();
const handleLogLinesPerPageChange = (e: number): void => { const handleLogLinesPerPageChange = (e: Pagination['limit']): void => {
dispatch({ dispatch({
type: SET_LOG_LINES_PER_PAGE, type: SET_LOG_LINES_PER_PAGE,
payload: { payload: {
@ -166,7 +167,7 @@ function LogControls(): JSX.Element | null {
<Divider type="vertical" /> <Divider type="vertical" />
<Controls <Controls
isLoading={isLoading} isLoading={isLoading}
count={logs.length} totalCount={logs.length}
countPerPage={logLinesPerPage} countPerPage={logLinesPerPage}
handleNavigatePrevious={handleNavigatePrevious} handleNavigatePrevious={handleNavigatePrevious}
handleNavigateNext={handleNavigateNext} handleNavigateNext={handleNavigateNext}

View File

@ -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 { interface ItemsProps {
name: ITEMS; name: ITEMS;

View File

@ -1,11 +1,18 @@
import { SearchOutlined } from '@ant-design/icons'; import { SearchOutlined } from '@ant-design/icons';
import { Input } from 'antd'; import { Input } from 'antd';
import Typography from 'antd/es/typography/Typography';
import { useIsDarkMode } from 'hooks/useDarkMode'; import { useIsDarkMode } from 'hooks/useDarkMode';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { OptionsMenuConfig } from '..';
import { FieldTitle } from '../styles'; 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 { function AddColumnField({ config }: AddColumnFieldProps): JSX.Element | null {
const { t } = useTranslation(['trace']); const { t } = useTranslation(['trace']);
@ -19,19 +26,32 @@ function AddColumnField({ config }: AddColumnFieldProps): JSX.Element | null {
<Input.Group compact> <Input.Group compact>
<AddColumnSelect <AddColumnSelect
allowClear
maxTagCount={0}
size="small" size="small"
mode="multiple" mode="multiple"
placeholder="Search" placeholder="Search"
options={config.options} options={config.options}
value={config.value} value={[]}
onChange={config.onChange} onChange={config.onChange}
/> />
<SearchIconWrapper $isDarkMode={isDarkMode}> <SearchIconWrapper $isDarkMode={isDarkMode}>
<SearchOutlined /> <SearchOutlined />
</SearchIconWrapper> </SearchIconWrapper>
</Input.Group> </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> </AddColumnWrapper>
); );
} }

View File

@ -1,3 +1,4 @@
import { DeleteOutlined } from '@ant-design/icons';
import { Card, Select, SelectProps, Space } from 'antd'; import { Card, Select, SelectProps, Space } from 'antd';
import { themeColors } from 'constants/theme'; import { themeColors } from 'constants/theme';
import { FunctionComponent } from 'react'; import { FunctionComponent } from 'react';
@ -26,3 +27,13 @@ export const AddColumnSelect: FunctionComponent<SelectProps> = styled(
export const AddColumnWrapper = styled(Space)` export const AddColumnWrapper = styled(Space)`
width: 100%; width: 100%;
`; `;
export const AddColumnItem = styled(Space)`
width: 100%;
display: flex;
justify-content: space-between;
`;
export const DeleteOutlinedIcon = styled(DeleteOutlined)`
color: red;
`;

View File

@ -1,7 +1,7 @@
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { OptionsMenuConfig } from '..';
import { FieldTitle } from '../styles'; import { FieldTitle } from '../styles';
import { OptionsMenuConfig } from '../types';
import { FormatFieldWrapper, RadioButton, RadioGroup } from './styles'; import { FormatFieldWrapper, RadioButton, RadioGroup } from './styles';
function FormatField({ config }: FormatFieldProps): JSX.Element | null { function FormatField({ config }: FormatFieldProps): JSX.Element | null {

View File

@ -1,7 +1,7 @@
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { OptionsMenuConfig } from '..';
import { FieldTitle } from '../styles'; import { FieldTitle } from '../styles';
import { OptionsMenuConfig } from '../types';
import { MaxLinesFieldWrapper, MaxLinesInput } from './styles'; import { MaxLinesFieldWrapper, MaxLinesInput } from './styles';
function MaxLinesField({ config }: MaxLinesFieldProps): JSX.Element | null { function MaxLinesField({ config }: MaxLinesFieldProps): JSX.Element | null {

View File

@ -0,0 +1,9 @@
import { OptionsQuery } from './types';
export const URL_OPTIONS = 'options';
export const defaultOptionsQuery: OptionsQuery = {
selectColumns: [],
maxLines: 0,
format: 'default',
};

View File

@ -1,11 +1,5 @@
import { SettingFilled, SettingOutlined } from '@ant-design/icons'; import { SettingFilled, SettingOutlined } from '@ant-design/icons';
import { import { Popover, Space } from 'antd';
InputNumberProps,
Popover,
RadioProps,
SelectProps,
Space,
} from 'antd';
import { useIsDarkMode } from 'hooks/useDarkMode'; import { useIsDarkMode } from 'hooks/useDarkMode';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -14,6 +8,12 @@ import AddColumnField from './AddColumnField';
import FormatField from './FormatField'; import FormatField from './FormatField';
import MaxLinesField from './MaxLinesField'; import MaxLinesField from './MaxLinesField';
import { OptionsContainer, OptionsContentWrapper } from './styles'; import { OptionsContainer, OptionsContentWrapper } from './styles';
import { OptionsMenuConfig } from './types';
import useOptionsMenu from './useOptionsMenu';
interface OptionsMenuProps {
config: OptionsMenuConfig;
}
function OptionsMenu({ config }: OptionsMenuProps): JSX.Element { function OptionsMenu({ config }: OptionsMenuProps): JSX.Element {
const { t } = useTranslation(['trace']); 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 default OptionsMenu;
export { useOptionsMenu };

View 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;
};
};

View 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;

View 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[]);

View File

@ -12,8 +12,8 @@ function TraceExplorerControls(): JSX.Element | null {
<Container> <Container>
<Controls <Controls
isLoading={false} isLoading={false}
count={0} totalCount={0}
countPerPage={0} countPerPage={25}
handleNavigatePrevious={handleNavigatePrevious} handleNavigatePrevious={handleNavigatePrevious}
handleNavigateNext={handleNavigateNext} handleNavigateNext={handleNavigateNext}
handleCountItemsPerPageChange={handleCountItemsPerPageChange} handleCountItemsPerPageChange={handleCountItemsPerPageChange}

View File

@ -1,6 +1,7 @@
import { Button } from 'antd'; import { Button } from 'antd';
import { PANEL_TYPES } from 'constants/queryBuilder'; import { PANEL_TYPES } from 'constants/queryBuilder';
import { QueryBuilder } from 'container/QueryBuilder'; import { QueryBuilder } from 'container/QueryBuilder';
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { DataSource } from 'types/common/queryBuilder'; import { DataSource } from 'types/common/queryBuilder';
@ -9,10 +10,12 @@ import { ButtonWrapper, Container } from './styles';
function QuerySection(): JSX.Element { function QuerySection(): JSX.Element {
const { handleRunQuery } = useQueryBuilder(); const { handleRunQuery } = useQueryBuilder();
const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.TIME_SERIES);
return ( return (
<Container> <Container>
<QueryBuilder <QueryBuilder
panelType={PANEL_TYPES.TIME_SERIES} panelType={panelTypes}
config={{ config={{
queryVariant: 'static', queryVariant: 'static',
initialDataSource: DataSource.TRACES, initialDataSource: DataSource.TRACES,

View File

@ -0,0 +1,8 @@
import { Pagination } from './types';
export const URL_PAGINATION = 'pagination';
export const defaultPaginationConfig: Pagination = {
offset: 0,
limit: 25,
};

View File

@ -0,0 +1,2 @@
export * from './config';
export * from './types';

View File

@ -0,0 +1,4 @@
export interface Pagination {
offset: number;
limit: 25 | 50 | 100 | 200;
}

View 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;

View 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,
);

View 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;

View File

@ -12,7 +12,7 @@ import { DataSource } from 'types/common/queryBuilder';
// ** Styles // ** Styles
import { ButtonWrapperStyled, WrapperStyled } from './styles'; import { ButtonWrapperStyled, WrapperStyled } from './styles';
function LogsExporer(): JSX.Element { function LogsExplorer(): JSX.Element {
const { handleRunQuery, updateAllQueriesOperators } = useQueryBuilder(); const { handleRunQuery, updateAllQueriesOperators } = useQueryBuilder();
const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.LIST); const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
@ -53,4 +53,4 @@ function LogsExporer(): JSX.Element {
); );
} }
export default LogsExporer; export default LogsExplorer;

View File

@ -1,6 +0,0 @@
export const CURRENT_TRACES_EXPLORER_TAB = 'currentTab';
export enum TracesExplorerTabs {
TIME_SERIES = 'times-series',
TRACES = 'traces',
}

View File

@ -1,56 +1,71 @@
import { Tabs } from 'antd'; 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 QuerySection from 'container/TracesExplorer/QuerySection';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl'; import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
import useUrlQuery from 'hooks/useUrlQuery'; import { useCallback, useMemo } from 'react';
import { useCallback, useEffect } from 'react'; import { DataSource } from 'types/common/queryBuilder';
import { useHistory, useLocation } from 'react-router-dom';
import { CURRENT_TRACES_EXPLORER_TAB, TracesExplorerTabs } from './constants';
import { Container } from './styles'; import { Container } from './styles';
import { getTabsItems } from './utils'; import { getTabsItems } from './utils';
function TracesExplorer(): JSX.Element { function TracesExplorer(): JSX.Element {
const urlQuery = useUrlQuery(); const {
const history = useHistory(); updateAllQueriesOperators,
const location = useLocation(); redirectWithQueryBuilderData,
currentQuery,
panelType,
} = useQueryBuilder();
const currentUrlTab = urlQuery.get(
CURRENT_TRACES_EXPLORER_TAB,
) as TracesExplorerTabs;
const currentTab = currentUrlTab || TracesExplorerTabs.TIME_SERIES;
const tabsItems = getTabsItems(); const tabsItems = getTabsItems();
const redirectWithCurrentTab = useCallback( const currentTab = panelType || PANEL_TYPES.TIME_SERIES;
(tabKey: string): void => {
urlQuery.set(CURRENT_TRACES_EXPLORER_TAB, tabKey);
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
history.push(generatedUrl);
},
[history, location, urlQuery],
);
const handleTabChange = useCallback( const handleTabChange = useCallback(
(tabKey: string): void => { (newPanelType: string): void => {
redirectWithCurrentTab(tabKey); 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(() => { useShareBuilderUrl(defaultValue);
if (currentUrlTab) return;
redirectWithCurrentTab(TracesExplorerTabs.TIME_SERIES);
}, [currentUrlTab, redirectWithCurrentTab]);
return ( return (
<> <>
<QuerySection /> <QuerySection />
<Container> <Container>
<Tabs activeKey={currentTab} items={tabsItems} onChange={handleTabChange} /> <Tabs
defaultActiveKey={currentTab}
activeKey={currentTab}
items={tabsItems}
onChange={handleTabChange}
/>
</Container> </Container>
</> </>
); );

View File

@ -1,17 +1,16 @@
import { TabsProps } from 'antd'; import { TabsProps } from 'antd';
import { PANEL_TYPES } from 'constants/queryBuilder';
import TimeSeriesView from 'container/TracesExplorer/TimeSeriesView'; import TimeSeriesView from 'container/TracesExplorer/TimeSeriesView';
import { TracesExplorerTabs } from './constants';
export const getTabsItems = (): TabsProps['items'] => [ export const getTabsItems = (): TabsProps['items'] => [
{ {
label: 'Time Series', label: 'Time Series',
key: TracesExplorerTabs.TIME_SERIES, key: PANEL_TYPES.TIME_SERIES,
children: <TimeSeriesView />, children: <TimeSeriesView />,
}, },
{ {
label: 'Traces', label: 'Traces',
key: TracesExplorerTabs.TRACES, key: PANEL_TYPES.TRACE,
children: <div>Traces tab</div>, children: <div>Traces tab</div>,
}, },
]; ];

View File

@ -1,4 +1,5 @@
import { LogViewMode } from 'container/LogsTable'; import { LogViewMode } from 'container/LogsTable';
import { Pagination } from 'hooks/queryPagination';
import { ILogQLParsedQueryItem } from 'lib/logql/types'; import { ILogQLParsedQueryItem } from 'lib/logql/types';
import { IField, IFieldMoveToSelected, IFields } from 'types/api/logs/fields'; import { IField, IFieldMoveToSelected, IFields } from 'types/api/logs/fields';
import { TLogsLiveTailState } from 'types/api/logs/liveTail'; import { TLogsLiveTailState } from 'types/api/logs/liveTail';
@ -70,7 +71,7 @@ export interface UpdateLogs {
export interface SetLogsLinesPerPage { export interface SetLogsLinesPerPage {
type: typeof SET_LOG_LINES_PER_PAGE; type: typeof SET_LOG_LINES_PER_PAGE;
payload: { payload: {
logsLinesPerPage: number; logsLinesPerPage: Pagination['limit'];
}; };
} }

View File

@ -143,7 +143,8 @@ export type PanelTypeKeys =
| 'VALUE' | 'VALUE'
| 'TABLE' | 'TABLE'
| 'LIST' | 'LIST'
| 'EMPTY_WIDGET'; | 'EMPTY_WIDGET'
| 'TRACE';
export type ReduceOperators = 'last' | 'sum' | 'avg' | 'max' | 'min'; export type ReduceOperators = 'last' | 'sum' | 'avg' | 'max' | 'min';

View File

@ -1,4 +1,5 @@
import { LogViewMode } from 'container/LogsTable'; import { LogViewMode } from 'container/LogsTable';
import { Pagination } from 'hooks/queryPagination';
import { ILogQLParsedQueryItem } from 'lib/logql/types'; import { ILogQLParsedQueryItem } from 'lib/logql/types';
import { IFields } from 'types/api/logs/fields'; import { IFields } from 'types/api/logs/fields';
import { TLogsLiveTailState } from 'types/api/logs/liveTail'; import { TLogsLiveTailState } from 'types/api/logs/liveTail';
@ -12,7 +13,7 @@ export interface ILogsReducer {
parsedQuery: ILogQLParsedQueryItem[]; parsedQuery: ILogQLParsedQueryItem[];
}; };
logs: ILog[]; logs: ILog[];
logLinesPerPage: number; logLinesPerPage: Pagination['limit'];
linesPerRow: number; linesPerRow: number;
viewMode: LogViewMode; viewMode: LogViewMode;
idEnd: string; idEnd: string;