Merge pull request #3038 from SigNoz/release/v0.22.0

Release/v0.22.0
This commit is contained in:
Prashant Shahi 2023-07-06 01:12:30 +05:30 committed by GitHub
commit e97609ce23
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
144 changed files with 3733 additions and 821 deletions

View File

@ -137,7 +137,7 @@ services:
condition: on-failure
query-service:
image: signoz/query-service:0.21.0
image: signoz/query-service:0.22.0
command: ["-config=/root/config/prometheus.yml"]
# ports:
# - "6060:6060" # pprof port
@ -166,7 +166,7 @@ services:
<<: *clickhouse-depend
frontend:
image: signoz/frontend:0.21.0
image: signoz/frontend:0.22.0
deploy:
restart_policy:
condition: on-failure
@ -179,7 +179,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector:
image: signoz/signoz-otel-collector:0.79.1
image: signoz/signoz-otel-collector:0.79.2
command: ["--config=/etc/otel-collector-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"]
user: root # required for reading docker container logs
volumes:
@ -208,7 +208,7 @@ services:
<<: *clickhouse-depend
otel-collector-metrics:
image: signoz/signoz-otel-collector:0.79.1
image: signoz/signoz-otel-collector:0.79.2
command: ["--config=/etc/otel-collector-metrics-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"]
volumes:
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
@ -233,7 +233,7 @@ services:
max-file: "3"
load-hotrod:
image: "grubykarol/locust:1.2.3-python3.9-alpine3.12"
image: "signoz/locust:1.2.3"
hostname: load-hotrod
environment:
ATTACKED_HOST: http://hotrod:8080

View File

@ -41,7 +41,7 @@ services:
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
otel-collector:
container_name: otel-collector
image: signoz/signoz-otel-collector:0.79.1
image: signoz/signoz-otel-collector:0.79.2
command: ["--config=/etc/otel-collector-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"]
# user: root # required for reading docker container logs
volumes:
@ -67,7 +67,7 @@ services:
otel-collector-metrics:
container_name: otel-collector-metrics
image: signoz/signoz-otel-collector:0.79.1
image: signoz/signoz-otel-collector:0.79.2
command: ["--config=/etc/otel-collector-metrics-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"]
volumes:
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
@ -93,7 +93,7 @@ services:
- JAEGER_ENDPOINT=http://otel-collector:14268/api/traces
load-hotrod:
image: "grubykarol/locust:1.2.3-python3.9-alpine3.12"
image: "signoz/locust:1.2.3"
container_name: load-hotrod
hostname: load-hotrod
environment:

View File

@ -153,7 +153,7 @@ services:
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
query-service:
image: signoz/query-service:${DOCKER_TAG:-0.21.0}
image: signoz/query-service:${DOCKER_TAG:-0.22.0}
container_name: query-service
command: ["-config=/root/config/prometheus.yml"]
# ports:
@ -181,7 +181,7 @@ services:
<<: *clickhouse-depend
frontend:
image: signoz/frontend:${DOCKER_TAG:-0.21.0}
image: signoz/frontend:${DOCKER_TAG:-0.22.0}
container_name: frontend
restart: on-failure
depends_on:
@ -193,7 +193,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector:
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.79.1}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.79.2}
command: ["--config=/etc/otel-collector-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"]
user: root # required for reading docker container logs
volumes:
@ -219,7 +219,7 @@ services:
<<: *clickhouse-depend
otel-collector-metrics:
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.79.1}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.79.2}
command: ["--config=/etc/otel-collector-metrics-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"]
volumes:
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
@ -243,7 +243,7 @@ services:
- JAEGER_ENDPOINT=http://otel-collector:14268/api/traces
load-hotrod:
image: "grubykarol/locust:1.2.3-python3.9-alpine3.12"
image: "signoz/locust:1.2.3"
container_name: load-hotrod
hostname: load-hotrod
environment:

View File

@ -16,6 +16,7 @@ import (
type APIHandlerOptions struct {
DataConnector interfaces.DataConnector
SkipConfig *basemodel.SkipConfig
AppDao dao.ModelDao
RulesManager *rules.Manager
FeatureFlags baseint.FeatureLookup
@ -32,6 +33,7 @@ func NewAPIHandler(opts APIHandlerOptions) (*APIHandler, error) {
baseHandler, err := baseapp.NewAPIHandler(baseapp.APIHandlerOpts{
Reader: opts.DataConnector,
SkipConfig: opts.SkipConfig,
AppDao: opts.AppDao,
RuleManager: opts.RulesManager,
FeatureFlags: opts.FeatureFlags})

View File

@ -49,9 +49,10 @@ import (
const AppDbEngine = "sqlite"
type ServerOptions struct {
PromConfigPath string
HTTPHostPort string
PrivateHostPort string
PromConfigPath string
SkipTopLvlOpsPath string
HTTPHostPort string
PrivateHostPort string
// alert specific params
DisableRules bool
RuleRepoURL string
@ -119,7 +120,15 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
go qb.Start(readerReady)
reader = qb
} else {
return nil, fmt.Errorf("Storage type: %s is not supported in query service", storage)
return nil, fmt.Errorf("storage type: %s is not supported in query service", storage)
}
skipConfig := &basemodel.SkipConfig{}
if serverOptions.SkipTopLvlOpsPath != "" {
// read skip config
skipConfig, err = basemodel.ReadSkipConfig(serverOptions.SkipTopLvlOpsPath)
if err != nil {
return nil, err
}
}
<-readerReady
@ -160,6 +169,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
apiOpts := api.APIHandlerOptions{
DataConnector: reader,
SkipConfig: skipConfig,
AppDao: modelDao,
RulesManager: rm,
FeatureFlags: lm,

View File

@ -74,7 +74,7 @@ func initZapLog(enableQueryServiceLogOTLPExport bool) *zap.Logger {
}
func main() {
var promConfigPath string
var promConfigPath, skipTopLvlOpsPath string
// disables rule execution but allows change to the rule definition
var disableRules bool
@ -85,6 +85,7 @@ func main() {
var enableQueryServiceLogOTLPExport bool
flag.StringVar(&promConfigPath, "config", "./config/prometheus.yml", "(prometheus config to read metrics)")
flag.StringVar(&skipTopLvlOpsPath, "skip-top-level-ops", "", "(config file to skip top level operations)")
flag.BoolVar(&disableRules, "rules.disable", false, "(disable rule evaluation)")
flag.StringVar(&ruleRepoURL, "rules.repo-url", baseconst.AlertHelpPage, "(host address used to build rule link in alert messages)")
flag.BoolVar(&enableQueryServiceLogOTLPExport, "enable.query.service.log.otlp.export", false, "(enable query service log otlp export)")
@ -98,11 +99,12 @@ func main() {
version.PrintVersion()
serverOptions := &app.ServerOptions{
HTTPHostPort: baseconst.HTTPHostPort,
PromConfigPath: promConfigPath,
PrivateHostPort: baseconst.PrivateHostPort,
DisableRules: disableRules,
RuleRepoURL: ruleRepoURL,
HTTPHostPort: baseconst.HTTPHostPort,
PromConfigPath: promConfigPath,
SkipTopLvlOpsPath: skipTopLvlOpsPath,
PrivateHostPort: baseconst.PrivateHostPort,
DisableRules: disableRules,
RuleRepoURL: ruleRepoURL,
}
// Read the jwt secret key

View File

@ -181,6 +181,9 @@ function Graph({
},
},
position: 'custom',
itemSort(item1, item2) {
return item2.parsed.y - item1.parsed.y;
},
},
[dragSelectPluginId]: createDragSelectPluginOptions(
!!onDragSelect,

View File

@ -1,14 +1,20 @@
import { Table } from 'antd';
import type { TableProps } from 'antd/es/table';
import { ColumnsType } from 'antd/lib/table';
import { SyntheticEvent, useCallback, useMemo, useState } from 'react';
import {
SyntheticEvent,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import { ResizeCallbackData } from 'react-resizable';
import ResizableHeader from './ResizableHeader';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function ResizeTable({ columns, ...restprops }: TableProps<any>): JSX.Element {
const [columnsData, setColumns] = useState<ColumnsType>(columns || []);
const [columnsData, setColumns] = useState<ColumnsType>([]);
const handleResize = useCallback(
(index: number) => (
@ -37,6 +43,12 @@ function ResizeTable({ columns, ...restprops }: TableProps<any>): JSX.Element {
[columnsData, handleResize],
);
useEffect(() => {
if (columns) {
setColumns(columns);
}
}, [columns]);
return (
<Table
// eslint-disable-next-line react/jsx-props-no-spreading

View File

@ -12,4 +12,8 @@ export enum QueryParams {
aggregationOption = 'aggregationOption',
entity = 'entity',
resourceAttributes = 'resourceAttribute',
graphType = 'graphType',
widgetId = 'widgetId',
order = 'order',
q = 'q',
}

View File

@ -125,7 +125,7 @@ const initialQueryBuilderFormValues: IBuilderQuery = {
}),
disabled: false,
having: [],
stepInterval: 30,
stepInterval: 60,
limit: null,
orderBy: [],
groupBy: [],
@ -232,6 +232,7 @@ export const PANEL_TYPES: Record<PanelTypeKeys, GRAPH_TYPES> = {
VALUE: 'value',
TABLE: 'table',
LIST: 'list',
TRACE: 'trace',
EMPTY_WIDGET: 'EMPTY_WIDGET',
};

View File

@ -1,33 +1,30 @@
import { LeftOutlined, RightOutlined } from '@ant-design/icons';
import { Button, Select } from 'antd';
import { DEFAULT_PER_PAGE_OPTIONS, Pagination } from 'hooks/queryPagination';
import { memo, useMemo } from 'react';
import { defaultSelectStyle, ITEMS_PER_PAGE_OPTIONS } from './config';
import { defaultSelectStyle } 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,
perPageOptions = DEFAULT_PER_PAGE_OPTIONS,
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 +32,7 @@ function Controls(props: ControlsProps): JSX.Element | null {
loading={isLoading}
size="small"
type="link"
disabled={isNextAndPreviousDisabled}
disabled={isPreviousDisabled || isNextAndPreviousDisabled}
onClick={handleNavigatePrevious}
>
<LeftOutlined /> Previous
@ -44,18 +41,18 @@ 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}
onChange={handleCountItemsPerPageChange}
>
{ITEMS_PER_PAGE_OPTIONS.map((count) => (
{perPageOptions.map((count) => (
<Select.Option
key={count}
value={count}
@ -66,4 +63,20 @@ function Controls(props: ControlsProps): JSX.Element | null {
);
}
Controls.defaultProps = {
offset: 0,
perPageOptions: DEFAULT_PER_PAGE_OPTIONS,
};
export interface ControlsProps {
offset?: Pagination['offset'];
perPageOptions?: number[];
totalCount: number;
countPerPage: Pagination['limit'];
isLoading: boolean;
handleNavigatePrevious: () => void;
handleNavigateNext: () => void;
handleCountItemsPerPageChange: (value: Pagination['limit']) => void;
}
export default memo(Controls);

View File

@ -0,0 +1,27 @@
import { TFunction } from 'i18next';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { OptionType } from './types';
export const getOptionList = (t: TFunction): OptionType[] => [
{
title: t('metric_based_alert'),
selection: AlertTypes.METRICS_BASED_ALERT,
description: t('metric_based_alert_desc'),
},
{
title: t('log_based_alert'),
selection: AlertTypes.LOGS_BASED_ALERT,
description: t('log_based_alert_desc'),
},
{
title: t('traces_based_alert'),
selection: AlertTypes.TRACES_BASED_ALERT,
description: t('traces_based_alert_desc'),
},
{
title: t('exceptions_based_alert'),
selection: AlertTypes.EXCEPTIONS_BASED_ALERT,
description: t('exceptions_based_alert_desc'),
},
];

View File

@ -1,61 +1,40 @@
import { Row } from 'antd';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { getOptionList } from './config';
import { AlertTypeCard, SelectTypeContainer } from './styles';
interface OptionType {
title: string;
selection: AlertTypes;
description: string;
}
import { OptionType } from './types';
function SelectAlertType({ onSelect }: SelectAlertTypeProps): JSX.Element {
const { t } = useTranslation(['alerts']);
const renderOptions = (): JSX.Element => {
const optionList: OptionType[] = [
{
title: t('metric_based_alert'),
selection: AlertTypes.METRICS_BASED_ALERT,
description: t('metric_based_alert_desc'),
},
{
title: t('log_based_alert'),
selection: AlertTypes.LOGS_BASED_ALERT,
description: t('log_based_alert_desc'),
},
{
title: t('traces_based_alert'),
selection: AlertTypes.TRACES_BASED_ALERT,
description: t('traces_based_alert_desc'),
},
{
title: t('exceptions_based_alert'),
selection: AlertTypes.EXCEPTIONS_BASED_ALERT,
description: t('exceptions_based_alert_desc'),
},
];
return (
const optionList = getOptionList(t);
const renderOptions = useMemo(
() => (
<>
{optionList.map((o: OptionType) => (
{optionList.map((option: OptionType) => (
<AlertTypeCard
key={o.selection}
title={o.title}
key={option.selection}
title={option.title}
onClick={(): void => {
onSelect(o.selection);
onSelect(option.selection);
}}
>
{o.description}
{option.description}
</AlertTypeCard>
))}
</>
);
};
),
[onSelect, optionList],
);
return (
<SelectTypeContainer>
<h3> {t('choose_alert_type')} </h3>
<Row>{renderOptions()}</Row>
<Row>{renderOptions}</Row>
</SelectTypeContainer>
);
}

View File

@ -0,0 +1,7 @@
import { AlertTypes } from 'types/api/alerts/alertTypes';
export interface OptionType {
title: string;
selection: AlertTypes;
description: string;
}

View File

@ -0,0 +1,8 @@
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { DataSource } from 'types/common/queryBuilder';
export const ALERT_TYPE_VS_SOURCE_MAPPING = {
[DataSource.LOGS]: AlertTypes.LOGS_BASED_ALERT,
[DataSource.METRICS]: AlertTypes.METRICS_BASED_ALERT,
[DataSource.TRACES]: AlertTypes.TRACES_BASED_ALERT,
};

View File

@ -38,7 +38,7 @@ export const alertDefaults: AlertDef = {
disabled: false,
},
},
queryType: EQueryType.CLICKHOUSE,
queryType: EQueryType.QUERY_BUILDER,
panelType: PANEL_TYPES.TIME_SERIES,
},
op: defaultCompareOp,
@ -67,7 +67,7 @@ export const logAlertDefaults: AlertDef = {
disabled: false,
},
},
queryType: EQueryType.CLICKHOUSE,
queryType: EQueryType.QUERY_BUILDER,
panelType: PANEL_TYPES.TIME_SERIES,
},
op: defaultCompareOp,
@ -97,7 +97,7 @@ export const traceAlertDefaults: AlertDef = {
disabled: false,
},
},
queryType: EQueryType.CLICKHOUSE,
queryType: EQueryType.QUERY_BUILDER,
panelType: PANEL_TYPES.TIME_SERIES,
},
op: defaultCompareOp,
@ -127,7 +127,7 @@ export const exceptionAlertDefaults: AlertDef = {
disabled: false,
},
},
queryType: EQueryType.CLICKHOUSE,
queryType: EQueryType.QUERY_BUILDER,
panelType: PANEL_TYPES.TIME_SERIES,
},
op: defaultCompareOp,

View File

@ -1,9 +1,11 @@
import { Form, Row } from 'antd';
import FormAlertRules from 'container/FormAlertRules';
import { useState } from 'react';
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
import { useEffect, useState } from 'react';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { AlertDef } from 'types/api/alerts/def';
import { ALERT_TYPE_VS_SOURCE_MAPPING } from './config';
import {
alertDefaults,
exceptionAlertDefaults,
@ -17,6 +19,9 @@ function CreateRules(): JSX.Element {
const [alertType, setAlertType] = useState<AlertTypes>(
AlertTypes.METRICS_BASED_ALERT,
);
const compositeQuery = useGetCompositeQueryParam();
const [formInstance] = Form.useForm();
const onSelectType = (typ: AlertTypes): void => {
@ -36,6 +41,19 @@ function CreateRules(): JSX.Element {
}
};
useEffect(() => {
if (!compositeQuery) {
return;
}
const dataSource = compositeQuery?.builder?.queryData[0]?.dataSource;
const alertType = ALERT_TYPE_VS_SOURCE_MAPPING[dataSource];
if (alertType) {
onSelectType(alertType);
}
}, [compositeQuery]);
if (!initValues) {
return (
<Row wrap={false}>

View File

@ -1,12 +1,11 @@
import { Button, Typography } from 'antd';
import createDashboard from 'api/dashboard/create';
import getAll from 'api/dashboard/getAll';
import axios from 'axios';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard';
import { useNotifications } from 'hooks/useNotifications';
import { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useMutation, useQuery } from 'react-query';
import { useMutation } from 'react-query';
import { ExportPanelProps } from '.';
import {
@ -18,7 +17,7 @@ import {
} from './styles';
import { getSelectOptions } from './utils';
function ExportPanel({ onExport }: ExportPanelProps): JSX.Element {
function ExportPanel({ isLoading, onExport }: ExportPanelProps): JSX.Element {
const { notifications } = useNotifications();
const { t } = useTranslation(['dashboard']);
@ -26,16 +25,18 @@ function ExportPanel({ onExport }: ExportPanelProps): JSX.Element {
null,
);
const { data, isLoading, refetch } = useQuery({
queryFn: getAll,
queryKey: REACT_QUERY_KEY.GET_ALL_DASHBOARDS,
});
const {
data,
isLoading: isAllDashboardsLoading,
refetch,
} = useGetAllDashboard();
const {
mutate: createNewDashboard,
isLoading: createDashboardLoading,
} = useMutation(createDashboard, {
onSuccess: () => {
onSuccess: (data) => {
onExport(data?.payload || null);
refetch();
},
onError: (error) => {
@ -73,6 +74,14 @@ function ExportPanel({ onExport }: ExportPanelProps): JSX.Element {
});
}, [t, createNewDashboard]);
const isDashboardLoading = isAllDashboardsLoading || createDashboardLoading;
const isDisabled =
isAllDashboardsLoading ||
!options?.length ||
!selectedDashboardId ||
isLoading;
return (
<Wrapper direction="vertical">
<Title>Export Panel</Title>
@ -81,14 +90,15 @@ function ExportPanel({ onExport }: ExportPanelProps): JSX.Element {
<DashboardSelect
placeholder="Select Dashboard"
options={options}
loading={isLoading || createDashboardLoading}
disabled={isLoading || createDashboardLoading}
loading={isDashboardLoading}
disabled={isDashboardLoading}
value={selectedDashboardId}
onSelect={handleSelect}
/>
<Button
type="primary"
disabled={isLoading || !options?.length || !selectedDashboardId}
loading={isLoading}
disabled={isDisabled}
onClick={handleExportClick}
>
Export

View File

@ -1,24 +1,44 @@
import { Button, Dropdown, MenuProps, Modal } from 'antd';
import { COMPOSITE_QUERY } from 'constants/queryBuilderQueryNames';
import ROUTES from 'constants/routes';
import history from 'lib/history';
import { useCallback, useMemo, useState } from 'react';
import { Dashboard } from 'types/api/dashboard/getAll';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { MENU_KEY, MENU_LABEL } from './config';
import ExportPanelContainer from './ExportPanel';
function ExportPanel({ onExport }: ExportPanelProps): JSX.Element {
function ExportPanel({
isLoading,
onExport,
query,
}: ExportPanelProps): JSX.Element {
const [isExport, setIsExport] = useState<boolean>(false);
const onModalToggle = useCallback((value: boolean) => {
setIsExport(value);
}, []);
const onCreateAlertsHandler = useCallback(() => {
history.push(
`${ROUTES.ALERTS_NEW}?${COMPOSITE_QUERY}=${encodeURIComponent(
JSON.stringify(query),
)}`,
);
}, [query]);
const onMenuClickHandler: MenuProps['onClick'] = useCallback(
(e: OnClickProps) => {
if (e.key === MENU_KEY.EXPORT) {
onModalToggle(true);
}
if (e.key === MENU_KEY.CREATE_ALERTS) {
onCreateAlertsHandler();
}
},
[onModalToggle],
[onModalToggle, onCreateAlertsHandler],
);
const menu: MenuProps = useMemo(
@ -48,23 +68,34 @@ function ExportPanel({ onExport }: ExportPanelProps): JSX.Element {
<Button>Actions</Button>
</Dropdown>
<Modal
footer={null}
onOk={onCancel(false)}
onCancel={onCancel(false)}
open={isExport}
centered
>
<ExportPanelContainer onExport={onExport} />
<ExportPanelContainer
query={query}
isLoading={isLoading}
onExport={onExport}
/>
</Modal>
</>
);
}
ExportPanel.defaultProps = {
isLoading: false,
};
interface OnClickProps {
key: string;
}
export interface ExportPanelProps {
isLoading?: boolean;
onExport: (dashboard: Dashboard | null) => void;
query: Query | null;
}
export default ExportPanel;

View File

@ -8,6 +8,7 @@ import QueryTypeTag from 'container/NewWidget/LeftContainer/QueryTypeTag';
import PlotTag from 'container/NewWidget/LeftContainer/WidgetGraph/PlotTag';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
import { updateStepInterval } from 'hooks/queryBuilder/useStepInterval';
import { MESSAGE, useIsFeatureDisabled } from 'hooks/useFeatureFlag';
import { useNotifications } from 'hooks/useNotifications';
import history from 'lib/history';
@ -16,6 +17,8 @@ import { mapQueryDataToApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQu
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useQueryClient } from 'react-query';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import {
AlertDef,
@ -24,6 +27,7 @@ import {
} from 'types/api/alerts/def';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { GlobalReducer } from 'types/reducer/globalTime';
import BasicInfo from './BasicInfo';
import ChartPreview from './ChartPreview';
@ -48,6 +52,10 @@ function FormAlertRules({
// init namespace for translations
const { t } = useTranslation('alerts');
const { minTime, maxTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const {
currentQuery,
stagedQuery,
@ -70,16 +78,12 @@ function FormAlertRules({
const sq = useMemo(() => mapQueryDataFromApi(initQuery), [initQuery]);
useShareBuilderUrl({ defaultValue: sq });
useShareBuilderUrl(sq);
useEffect(() => {
setAlertDef(initialValue);
}, [initialValue]);
const onRunQuery = (): void => {
handleRunQuery();
};
const onCancelHandler = useCallback(() => {
history.replace(ROUTES.LIST_ALL_ALERT);
}, []);
@ -99,7 +103,7 @@ function FormAlertRules({
}
const query: Query = { ...currentQuery, queryType: val };
redirectWithQueryBuilderData(query);
redirectWithQueryBuilderData(updateStepInterval(query, maxTime, minTime));
};
const { notifications } = useNotifications();
@ -402,7 +406,7 @@ function FormAlertRules({
queryCategory={currentQuery.queryType}
setQueryCategory={onQueryCategoryChange}
alertType={alertType || AlertTypes.METRICS_BASED_ALERT}
runQuery={onRunQuery}
runQuery={handleRunQuery}
/>
<RuleOptions

View File

@ -8,6 +8,7 @@ import {
timePreferance,
} from 'container/NewWidget/RightContainer/timeItems';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useStepInterval } from 'hooks/queryBuilder/useStepInterval';
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
import getChartData from 'lib/getChartData';
import { useCallback, useMemo, useState } from 'react';
@ -48,11 +49,13 @@ function FullView({
[selectedTime, globalSelectedTime, widget],
);
const updatedQuery = useStepInterval(widget?.query);
const response = useGetQueryRange(
{
selectedTime: selectedTime.enum,
graphType: widget.panelTypes,
query: widget.query,
query: updatedQuery,
globalSelectedInterval: globalSelectedTime,
variables: getDashboardVariables(),
},
@ -84,10 +87,8 @@ function FullView({
{fullViewOptions && (
<TimeContainer>
<TimePreference
{...{
selectedTime,
setSelectedTime,
}}
selectedTime={selectedTime}
setSelectedTime={setSelectedTime}
/>
<Button
onClick={(): void => {

View File

@ -4,6 +4,7 @@ import Spinner from 'components/Spinner';
import GridGraphComponent from 'container/GridGraphComponent';
import { UpdateDashboard } from 'container/GridGraphLayout/utils';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useStepInterval } from 'hooks/queryBuilder/useStepInterval';
import { useNotifications } from 'hooks/useNotifications';
import usePreviousValue from 'hooks/usePreviousValue';
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
@ -80,11 +81,13 @@ function GridCardGraph({
const selectedData = selectedDashboard?.data;
const { variables } = selectedData;
const updatedQuery = useStepInterval(widget?.query);
const queryResponse = useGetQueryRange(
{
selectedTime: widget?.timePreferance,
graphType: widget?.panelTypes,
query: widget?.query,
query: updatedQuery,
globalSelectedInterval,
variables: getDashboardVariables(),
},

View File

@ -64,7 +64,7 @@ function WidgetHeader({
history.push(
`${window.location.pathname}/new?widgetId=${widgetId}&graphType=${
widget.panelTypes
}&${COMPOSITE_QUERY}=${JSON.stringify(widget.query)}`,
}&${COMPOSITE_QUERY}=${encodeURIComponent(JSON.stringify(widget.query))}`,
);
}, [widget.id, widget.panelTypes, widget.query]);

View File

@ -77,8 +77,8 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
history.push(
`${
ROUTES.EDIT_ALERTS
}?ruleId=${record.id.toString()}&${COMPOSITE_QUERY}=${JSON.stringify(
compositeQuery,
}?ruleId=${record.id.toString()}&${COMPOSITE_QUERY}=${encodeURIComponent(
JSON.stringify(compositeQuery),
)}`,
);
})

View File

@ -75,8 +75,8 @@ function ListOfAllDashboard(): JSX.Element {
errorMessage: '',
});
const columns: TableColumnProps<Data>[] = useMemo(
() => [
const columns = useMemo(() => {
const tableColumns: TableColumnProps<Data>[] = [
{
title: 'Name',
dataIndex: 'name',
@ -118,19 +118,19 @@ function ListOfAllDashboard(): JSX.Element {
},
render: DateComponent,
},
],
[],
);
];
if (action) {
columns.push({
title: 'Action',
dataIndex: '',
key: 'x',
width: 40,
render: DeleteButton,
});
}
if (action) {
tableColumns.push({
title: 'Action',
dataIndex: '',
width: 40,
render: DeleteButton,
});
}
return tableColumns;
}, [action]);
const data: Data[] = (filteredDashboards || dashboards).map((e) => ({
createdBy: e.created_at,

View File

@ -5,7 +5,9 @@ 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 { OrderPreferenceItems } from 'pages/Logs/config';
import * as Papa from 'papaparse';
import { memo, useCallback, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
@ -30,6 +32,7 @@ function LogControls(): JSX.Element | null {
isLoading: isLogsLoading,
isLoadingAggregate,
logs,
order,
} = useSelector<AppState, ILogsReducer>((state) => state.logs);
const globalTime = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
@ -37,7 +40,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: {
@ -159,6 +162,7 @@ function LogControls(): JSX.Element | null {
loading={isLoading}
size="small"
type="link"
disabled={order === OrderPreferenceItems.ASC}
onClick={handleGoToLatest}
>
<FastBackwardOutlined /> Go to latest
@ -166,7 +170,7 @@ function LogControls(): JSX.Element | null {
<Divider type="vertical" />
<Controls
isLoading={isLoading}
count={logs.length}
totalCount={logs.length}
countPerPage={logLinesPerPage}
handleNavigatePrevious={handleNavigatePrevious}
handleNavigateNext={handleNavigateNext}

View File

@ -2,6 +2,7 @@ import { MinusCircleOutlined, PlusCircleOutlined } from '@ant-design/icons';
import { Button, Col, Popover } from 'antd';
import getStep from 'lib/getStep';
import { generateFilterQuery } from 'lib/logs/generateFilterQuery';
import { getIdConditions } from 'pages/Logs/utils';
import { memo, useMemo } from 'react';
import { connect, useDispatch, useSelector } from 'react-redux';
import { bindActionCreators, Dispatch } from 'redux';
@ -45,6 +46,7 @@ function ActionItem({
idStart,
liveTail,
idEnd,
order,
} = useSelector<AppState, ILogsReducer>((store) => store.logs);
const dispatch = useDispatch<Dispatch<AppActions>>();
@ -72,11 +74,10 @@ function ActionItem({
q: updatedQueryString,
limit: logLinesPerPage,
orderBy: 'timestamp',
order: 'desc',
order,
timestampStart: minTime,
timestampEnd: maxTime,
...(idStart ? { idGt: idStart } : {}),
...(idEnd ? { idLt: idEnd } : {}),
...getIdConditions(idStart, idEnd, order),
});
getLogsAggregate({
timestampStart: minTime,

View File

@ -1 +0,0 @@
export { LogsExplorerChart } from './LogsExplorerChart';

View File

@ -2,41 +2,33 @@ import Graph from 'components/Graph';
import Spinner from 'components/Spinner';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { getExplorerChartData } from 'lib/explorer/getExplorerChartData';
import { useMemo } from 'react';
import { memo, useMemo } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { CardStyled } from './LogsExplorerChart.styled';
export function LogsExplorerChart(): JSX.Element {
const { stagedQuery } = useQueryBuilder();
function LogsExplorerChart(): JSX.Element {
const { stagedQuery, panelType, isEnabledQuery } = useQueryBuilder();
const { selectedTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const panelTypeParam = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
const { data, isFetching } = useGetQueryRange(
{
query: stagedQuery || initialQueriesMap.metrics,
graphType: panelTypeParam,
graphType: panelType || PANEL_TYPES.LIST,
globalSelectedInterval: selectedTime,
selectedTime: 'GLOBAL_TIME',
},
{
queryKey: [
REACT_QUERY_KEY.GET_QUERY_RANGE,
selectedTime,
stagedQuery,
panelTypeParam,
],
enabled: !!stagedQuery,
queryKey: [REACT_QUERY_KEY.GET_QUERY_RANGE, selectedTime, stagedQuery],
enabled: isEnabledQuery,
},
);
@ -64,3 +56,5 @@ export function LogsExplorerChart(): JSX.Element {
</CardStyled>
);
}
export default memo(LogsExplorerChart);

View File

@ -0,0 +1,3 @@
import { QueryDataV3 } from 'types/api/widgets/getQuery';
export type LogsExplorerListProps = { data: QueryDataV3[]; isLoading: boolean };

View File

@ -0,0 +1,117 @@
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 { LogViewMode } from 'container/LogsTable';
import { Container, Heading } from 'container/LogsTable/styles';
import { contentStyle } from 'container/Trace/Search/config';
import useFontFaceObserver from 'hooks/useFontObserver';
import { memo, useCallback, useMemo, useState } from 'react';
import { Virtuoso } from 'react-virtuoso';
// interfaces
import { ILog } from 'types/api/logs/log';
import { LogsExplorerListProps } from './LogsExplorerList.interfaces';
function LogsExplorerList({
data,
isLoading,
}: LogsExplorerListProps): JSX.Element {
const [viewMode] = useState<LogViewMode>('raw');
const [linesPerRow] = useState<number>(20);
const logs: ILog[] = useMemo(() => {
if (data.length > 0 && data[0].list) {
const logs: ILog[] = data[0].list.map((item) => ({
timestamp: +item.timestamp,
...item.data,
}));
return logs;
}
return [];
}, [data]);
useFontFaceObserver(
[
{
family: 'Fira Code',
weight: '300',
},
],
viewMode === 'raw',
{
timeout: 5000,
},
);
// TODO: implement here linesPerRow, mode like in useSelectedLogView
const getItemContent = useCallback(
(index: number): JSX.Element => {
const log = logs[index];
if (viewMode === 'raw') {
return (
<RawLogView
key={log.id}
data={log}
linesPerRow={linesPerRow}
// TODO: write new onClickExpanded logic
onClickExpand={(): void => {}}
/>
);
}
return <ListLogView key={log.id} logData={log} />;
},
[logs, linesPerRow, viewMode],
);
const renderContent = useMemo(() => {
if (viewMode === 'table') {
return (
<LogsTableView
logs={logs}
// TODO: write new selected logic
fields={[]}
linesPerRow={linesPerRow}
// TODO: write new onClickExpanded logic
onClickExpand={(): void => {}}
/>
);
}
return (
<Card bodyStyle={contentStyle}>
<Virtuoso
useWindowScroll
totalCount={logs.length}
itemContent={getItemContent}
/>
</Card>
);
}, [getItemContent, linesPerRow, logs, viewMode]);
if (isLoading) {
return <Spinner height={20} tip="Getting Logs" />;
}
return (
<Container>
{viewMode !== 'table' && (
<Heading>
<Typography.Text>Event</Typography.Text>
</Heading>
)}
{logs.length === 0 && <Typography>No logs lines found</Typography>}
{renderContent}
</Container>
);
}
export default memo(LogsExplorerList);

View File

@ -0,0 +1,6 @@
import { QueryDataV3 } from 'types/api/widgets/getQuery';
export type LogsExplorerTableProps = {
data: QueryDataV3[];
isLoading: boolean;
};

View File

@ -0,0 +1,23 @@
import { initialQueriesMap } from 'constants/queryBuilder';
import { QueryTable } from 'container/QueryTable';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { memo } from 'react';
import { LogsExplorerTableProps } from './LogsExplorerTable.interfaces';
function LogsExplorerTable({
isLoading,
data,
}: LogsExplorerTableProps): JSX.Element {
const { stagedQuery } = useQueryBuilder();
return (
<QueryTable
query={stagedQuery || initialQueriesMap.metrics}
queryTableData={data}
loading={isLoading}
/>
);
}
export default memo(LogsExplorerTable);

View File

@ -1,75 +0,0 @@
import { TabsProps } from 'antd';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { PANEL_TYPES_QUERY } from 'constants/queryBuilderQueryNames';
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import useUrlQuery from 'hooks/useUrlQuery';
import { useCallback, useEffect, useMemo } from 'react';
import { useHistory, useLocation } from 'react-router-dom';
import { TabsStyled } from './LogsExplorerViews.styled';
export function LogsExplorerViews(): JSX.Element {
const location = useLocation();
const urlQuery = useUrlQuery();
const history = useHistory();
const { currentQuery } = useQueryBuilder();
const panelTypeParams = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
const isMultipleQueries = useMemo(
() =>
currentQuery.builder.queryData.length > 1 ||
currentQuery.builder.queryFormulas.length > 0,
[currentQuery],
);
const tabsItems: TabsProps['items'] = useMemo(
() => [
{
label: 'List View',
key: PANEL_TYPES.LIST,
disabled: isMultipleQueries,
},
{ label: 'TimeSeries', key: PANEL_TYPES.TIME_SERIES },
{ label: 'Table', key: PANEL_TYPES.TABLE },
],
[isMultipleQueries],
);
const handleChangeView = useCallback(
(panelType: string) => {
urlQuery.set(PANEL_TYPES_QUERY, JSON.stringify(panelType) as GRAPH_TYPES);
const path = `${location.pathname}?${urlQuery}`;
history.push(path);
},
[history, location, urlQuery],
);
const currentTabKey = useMemo(
() =>
Object.values(PANEL_TYPES).includes(panelTypeParams)
? panelTypeParams
: PANEL_TYPES.LIST,
[panelTypeParams],
);
useEffect(() => {
if (panelTypeParams === 'list' && isMultipleQueries) {
handleChangeView(PANEL_TYPES.TIME_SERIES);
}
}, [panelTypeParams, isMultipleQueries, handleChangeView]);
return (
<div>
<TabsStyled
items={tabsItems}
defaultActiveKey={currentTabKey}
activeKey={currentTabKey}
onChange={handleChangeView}
/>
</div>
);
}

View File

@ -1 +0,0 @@
export { LogsExplorerViews } from './LogsExplorerViews';

View File

@ -0,0 +1,134 @@
import { TabsProps } from 'antd';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import { PANEL_TYPES_QUERY } from 'constants/queryBuilderQueryNames';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import LogsExplorerList from 'container/LogsExplorerList';
import LogsExplorerTable from 'container/LogsExplorerTable';
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import TimeSeriesView from 'container/TimeSeriesView/TimeSeriesView';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { memo, useCallback, useEffect, useMemo } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { DataSource } from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { TabsStyled } from './LogsExplorerViews.styled';
function LogsExplorerViews(): JSX.Element {
const {
currentQuery,
stagedQuery,
panelType,
isEnabledQuery,
updateAllQueriesOperators,
redirectWithQueryBuilderData,
} = useQueryBuilder();
const { selectedTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const { data, isFetching, isError } = useGetQueryRange(
{
query: stagedQuery || initialQueriesMap.metrics,
graphType: panelType || PANEL_TYPES.LIST,
globalSelectedInterval: selectedTime,
selectedTime: 'GLOBAL_TIME',
params: {
dataSource: DataSource.LOGS,
},
},
{
queryKey: [REACT_QUERY_KEY.GET_QUERY_RANGE, selectedTime, stagedQuery],
enabled: isEnabledQuery,
},
);
const isMultipleQueries = useMemo(
() =>
currentQuery.builder.queryData.length > 1 ||
currentQuery.builder.queryFormulas.length > 0,
[currentQuery],
);
const isGroupByExist = useMemo(() => {
const groupByCount: number = currentQuery.builder.queryData.reduce<number>(
(acc, query) => acc + query.groupBy.length,
0,
);
return groupByCount > 0;
}, [currentQuery]);
const currentData = useMemo(
() => data?.payload.data.newResult.data.result || [],
[data],
);
const tabsItems: TabsProps['items'] = useMemo(
() => [
{
label: 'List View',
key: PANEL_TYPES.LIST,
disabled: isMultipleQueries || isGroupByExist,
children: <LogsExplorerList data={currentData} isLoading={isFetching} />,
},
{
label: 'TimeSeries',
key: PANEL_TYPES.TIME_SERIES,
children: (
<TimeSeriesView isLoading={isFetching} data={data} isError={isError} />
),
},
{
label: 'Table',
key: PANEL_TYPES.TABLE,
children: <LogsExplorerTable data={currentData} isLoading={isFetching} />,
},
],
[isMultipleQueries, isGroupByExist, currentData, isFetching, data, isError],
);
const handleChangeView = useCallback(
(newPanelType: string) => {
if (newPanelType === panelType) return;
const query = updateAllQueriesOperators(
currentQuery,
newPanelType as GRAPH_TYPES,
DataSource.LOGS,
);
redirectWithQueryBuilderData(query, { [PANEL_TYPES_QUERY]: newPanelType });
},
[
currentQuery,
panelType,
updateAllQueriesOperators,
redirectWithQueryBuilderData,
],
);
useEffect(() => {
const shouldChangeView = isMultipleQueries || isGroupByExist;
if (panelType === 'list' && shouldChangeView) {
handleChangeView(PANEL_TYPES.TIME_SERIES);
}
}, [panelType, isMultipleQueries, isGroupByExist, handleChangeView]);
return (
<div>
<TabsStyled
items={tabsItems}
defaultActiveKey={panelType || PANEL_TYPES.LIST}
activeKey={panelType || PANEL_TYPES.LIST}
onChange={handleChangeView}
/>
</div>
);
}
export default memo(LogsExplorerViews);

View File

@ -2,6 +2,7 @@ import { Input, InputRef, Popover } from 'antd';
import useUrlQuery from 'hooks/useUrlQuery';
import getStep from 'lib/getStep';
import debounce from 'lodash-es/debounce';
import { getIdConditions } from 'pages/Logs/utils';
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { connect, useDispatch, useSelector } from 'react-redux';
import { bindActionCreators, Dispatch } from 'redux';
@ -33,7 +34,7 @@ function SearchFilter({
const [searchText, setSearchText] = useState(queryString);
const [showDropDown, setShowDropDown] = useState(false);
const searchRef = useRef<InputRef>(null);
const { logLinesPerPage, idEnd, idStart, liveTail } = useSelector<
const { logLinesPerPage, idEnd, idStart, liveTail, order } = useSelector<
AppState,
ILogsReducer
>((state) => state.logs);
@ -99,11 +100,10 @@ function SearchFilter({
q: customQuery,
limit: logLinesPerPage,
orderBy: 'timestamp',
order: 'desc',
order,
timestampStart: minTime,
timestampEnd: maxTime,
...(idStart ? { idGt: idStart } : {}),
...(idEnd ? { idLt: idEnd } : {}),
...getIdConditions(idStart, idEnd, order),
});
getLogsAggregate({
@ -128,6 +128,7 @@ function SearchFilter({
logLinesPerPage,
globalTime,
getLogsFields,
order,
],
);
@ -160,6 +161,7 @@ function SearchFilter({
dispatch,
globalTime.maxTime,
globalTime.minTime,
order,
]);
const onPopOverChange = useCallback(

View File

@ -1,3 +1,4 @@
import { QueryParams } from 'constants/query';
import { getMinMax } from 'container/TopNav/AutoRefresh/config';
import useUrlQuery from 'hooks/useUrlQuery';
import history from 'lib/history';
@ -25,6 +26,7 @@ export function useSearchParser(): {
const dispatch = useDispatch<Dispatch<AppActions>>();
const {
searchFilter: { parsedQuery, queryString },
order,
} = useSelector<AppState, ILogsReducer>((store) => store.logs);
const urlQuery = useUrlQuery();
@ -39,7 +41,7 @@ export function useSearchParser(): {
(updatedQueryString: string) => {
history.replace({
pathname: history.location.pathname,
search: `?q=${updatedQueryString}`,
search: `?${QueryParams.q}=${updatedQueryString}&${QueryParams.order}=${order}`,
});
const globalTime = getMinMax(selectedTime, minTime, maxTime);

View File

@ -2,6 +2,8 @@ import {
initialFormulaBuilderFormValues,
initialQueryBuilderFormValuesMap,
} from 'constants/queryBuilder';
import getStep from 'lib/getStep';
import store from 'store';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import {
@ -24,6 +26,11 @@ export const getQueryBuilderQueries = ({
groupBy,
aggregateAttribute: metricName,
legend,
stepInterval: getStep({
end: store.getState().globalTime.maxTime,
inputFormat: 'ns',
start: store.getState().globalTime.minTime,
}),
reduceTo: 'sum',
filters: {
items: itemsA,
@ -64,6 +71,11 @@ export const getQueryBuilderQuerieswithFormula = ({
items: additionalItemsA,
op: 'AND',
},
stepInterval: getStep({
end: store.getState().globalTime.maxTime,
inputFormat: 'ns',
start: store.getState().globalTime.minTime,
}),
},
{
...initialQueryBuilderFormValuesMap.metrics,
@ -79,6 +91,11 @@ export const getQueryBuilderQuerieswithFormula = ({
items: additionalItemsB,
op: 'AND',
},
stepInterval: getStep({
end: store.getState().globalTime.maxTime,
inputFormat: 'ns',
start: store.getState().globalTime.minTime,
}),
},
],
});

View File

@ -11,11 +11,11 @@ import {
} from 'hooks/useResourceAttribute/utils';
import { useMemo, useState } from 'react';
import { useParams } from 'react-router-dom';
import { Widgets } from 'types/api/dashboard/getAll';
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { v4 as uuid } from 'uuid';
import { getWidgetQueryBuilder } from '../MetricsApplication.factory';
import { Card, GraphContainer, GraphTitle, Row } from '../styles';
import { Button } from './styles';
import {
@ -25,7 +25,7 @@ import {
onViewTracePopupClick,
} from './util';
function DBCall({ getWidgetQueryBuilder }: DBCallProps): JSX.Element {
function DBCall(): JSX.Element {
const { servicename } = useParams<{ servicename?: string }>();
const [selectedTimeStamp, setSelectedTimeStamp] = useState<number>(0);
const { queries } = useResourceAttribute();
@ -59,7 +59,7 @@ function DBCall({ getWidgetQueryBuilder }: DBCallProps): JSX.Element {
clickhouse_sql: [],
id: uuid(),
}),
[getWidgetQueryBuilder, servicename, tagFilterItems],
[servicename, tagFilterItems],
);
const databaseCallsAverageDurationWidget = useMemo(
() =>
@ -73,7 +73,7 @@ function DBCall({ getWidgetQueryBuilder }: DBCallProps): JSX.Element {
clickhouse_sql: [],
id: uuid(),
}),
[getWidgetQueryBuilder, servicename, tagFilterItems],
[servicename, tagFilterItems],
);
return (
@ -151,8 +151,4 @@ function DBCall({ getWidgetQueryBuilder }: DBCallProps): JSX.Element {
);
}
interface DBCallProps {
getWidgetQueryBuilder: (query: Widgets['query']) => Widgets;
}
export default DBCall;

View File

@ -13,10 +13,10 @@ import {
} from 'hooks/useResourceAttribute/utils';
import { useMemo, useState } from 'react';
import { useParams } from 'react-router-dom';
import { Widgets } from 'types/api/dashboard/getAll';
import { EQueryType } from 'types/common/dashboard';
import { v4 as uuid } from 'uuid';
import { getWidgetQueryBuilder } from '../MetricsApplication.factory';
import { Card, GraphContainer, GraphTitle, Row } from '../styles';
import { legend } from './constant';
import { Button } from './styles';
@ -26,7 +26,7 @@ import {
onViewTracePopupClick,
} from './util';
function External({ getWidgetQueryBuilder }: ExternalProps): JSX.Element {
function External(): JSX.Element {
const [selectedTimeStamp, setSelectedTimeStamp] = useState<number>(0);
const { servicename } = useParams<{ servicename?: string }>();
@ -51,7 +51,7 @@ function External({ getWidgetQueryBuilder }: ExternalProps): JSX.Element {
clickhouse_sql: [],
id: uuid(),
}),
[getWidgetQueryBuilder, servicename, tagFilterItems],
[servicename, tagFilterItems],
);
const selectedTraceTags = useMemo(
@ -71,7 +71,7 @@ function External({ getWidgetQueryBuilder }: ExternalProps): JSX.Element {
clickhouse_sql: [],
id: uuid(),
}),
[getWidgetQueryBuilder, servicename, tagFilterItems],
[servicename, tagFilterItems],
);
const externalCallRPSWidget = useMemo(
@ -87,7 +87,7 @@ function External({ getWidgetQueryBuilder }: ExternalProps): JSX.Element {
clickhouse_sql: [],
id: uuid(),
}),
[getWidgetQueryBuilder, servicename, tagFilterItems],
[servicename, tagFilterItems],
);
const externalCallDurationAddressWidget = useMemo(
@ -103,7 +103,7 @@ function External({ getWidgetQueryBuilder }: ExternalProps): JSX.Element {
clickhouse_sql: [],
id: uuid(),
}),
[getWidgetQueryBuilder, servicename, tagFilterItems],
[servicename, tagFilterItems],
);
return (
@ -261,8 +261,4 @@ function External({ getWidgetQueryBuilder }: ExternalProps): JSX.Element {
);
}
interface ExternalProps {
getWidgetQueryBuilder: (query: Widgets['query']) => Widgets;
}
export default External;

View File

@ -18,11 +18,11 @@ import { useDispatch, useSelector } from 'react-redux';
import { useLocation, useParams } from 'react-router-dom';
import { UpdateTimeInterval } from 'store/actions';
import { AppState } from 'store/reducers';
import { Widgets } from 'types/api/dashboard/getAll';
import { EQueryType } from 'types/common/dashboard';
import MetricReducer from 'types/reducer/metrics';
import { v4 as uuid } from 'uuid';
import { getWidgetQueryBuilder } from '../MetricsApplication.factory';
import {
errorPercentage,
operationPerSec,
@ -36,7 +36,7 @@ import {
onViewTracePopupClick,
} from './util';
function Application({ getWidgetQueryBuilder }: DashboardProps): JSX.Element {
function Application(): JSX.Element {
const { servicename } = useParams<{ servicename?: string }>();
const [selectedTimeStamp, setSelectedTimeStamp] = useState<number>(0);
const { search } = useLocation();
@ -94,7 +94,7 @@ function Application({ getWidgetQueryBuilder }: DashboardProps): JSX.Element {
clickhouse_sql: [],
id: uuid(),
}),
[getWidgetQueryBuilder, servicename, topLevelOperations, tagFilterItems],
[servicename, topLevelOperations, tagFilterItems],
);
const errorPercentageWidget = useMemo(
@ -110,7 +110,7 @@ function Application({ getWidgetQueryBuilder }: DashboardProps): JSX.Element {
clickhouse_sql: [],
id: uuid(),
}),
[servicename, topLevelOperations, tagFilterItems, getWidgetQueryBuilder],
[servicename, topLevelOperations, tagFilterItems],
);
const onDragSelect = useCallback(
@ -289,10 +289,6 @@ function Application({ getWidgetQueryBuilder }: DashboardProps): JSX.Element {
);
}
interface DashboardProps {
getWidgetQueryBuilder: (query: Widgets['query']) => Widgets;
}
type ClickHandlerType = (
ChartEvent: ChartEvent,
activeElements: ActiveElement[],

View File

@ -6,21 +6,20 @@ import { memo, useMemo } from 'react';
import { generatePath, useParams } from 'react-router-dom';
import { useLocation } from 'react-use';
import { getWidgetQueryBuilder } from './MetricsApplication.factory';
import DBCall from './Tabs/DBCall';
import External from './Tabs/External';
import Overview from './Tabs/Overview';
function OverViewTab(): JSX.Element {
return <Overview getWidgetQueryBuilder={getWidgetQueryBuilder} />;
return <Overview />;
}
function DbCallTab(): JSX.Element {
return <DBCall getWidgetQueryBuilder={getWidgetQueryBuilder} />;
return <DBCall />;
}
function ExternalTab(): JSX.Element {
return <External getWidgetQueryBuilder={getWidgetQueryBuilder} />;
return <External />;
}
function ServiceMetrics(): JSX.Element {

View File

@ -47,7 +47,9 @@ function DashboardGraphSlider({ toggleAddWidget }: Props): JSX.Element {
history.push(
`${history.location.pathname}/new?graphType=${name}&widgetId=${
emptyLayout.i
}&${COMPOSITE_QUERY}=${JSON.stringify(initialQueriesMap.metrics)}`,
}&${COMPOSITE_QUERY}=${encodeURIComponent(
JSON.stringify(initialQueriesMap.metrics),
)}`,
);
} catch (error) {
notifications.error({

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

View File

@ -6,6 +6,7 @@ import { QueryBuilder } from 'container/QueryBuilder';
import { useGetWidgetQueryRange } from 'hooks/queryBuilder/useGetWidgetQueryRange';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
import { updateStepInterval } from 'hooks/queryBuilder/useStepInterval';
import useUrlQuery from 'hooks/useUrlQuery';
import { useCallback } from 'react';
import { connect, useSelector } from 'react-redux';
@ -22,6 +23,7 @@ import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import AppReducer from 'types/reducer/app';
import DashboardReducer from 'types/reducer/dashboards';
import { GlobalReducer } from 'types/reducer/globalTime';
import ClickHouseQueryContainer from './QueryBuilder/clickHouse';
import PromQLQueryContainer from './QueryBuilder/promQL';
@ -33,6 +35,11 @@ function QuerySection({
}: QueryProps): JSX.Element {
const { currentQuery, redirectWithQueryBuilderData } = useQueryBuilder();
const urlQuery = useUrlQuery();
const { minTime, maxTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const { featureResponse } = useSelector<AppState, AppReducer>(
(state) => state.app,
);
@ -58,7 +65,7 @@ function QuerySection({
const { query } = selectedWidget;
useShareBuilderUrl({ defaultValue: query });
useShareBuilderUrl(query);
const handleStageQuery = useCallback(
(updatedQuery: Query): void => {
@ -67,10 +74,19 @@ function QuerySection({
yAxisUnit: selectedWidget.yAxisUnit,
});
redirectWithQueryBuilderData(updatedQuery);
redirectWithQueryBuilderData(
updateStepInterval(updatedQuery, maxTime, minTime),
);
},
[urlQuery, selectedWidget, updateQuery, redirectWithQueryBuilderData],
[
updateQuery,
urlQuery,
selectedWidget.yAxisUnit,
redirectWithQueryBuilderData,
maxTime,
minTime,
],
);
const handleQueryCategoryChange = (qCategory: string): void => {

View File

@ -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,24 @@ 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(({ key, id }) => (
<AddColumnItem direction="horizontal" key={id}>
<Typography>{key}</Typography>
<DeleteOutlinedIcon onClick={(): void => config.onRemove(id as string)} />
</AddColumnItem>
))}
</AddColumnWrapper>
);
}

View File

@ -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;
`;

View File

@ -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 {

View File

@ -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 {

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

View File

@ -0,0 +1,22 @@
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' | 'onChange'> & {
value: BaseAutocompleteData[];
onRemove: (key: string) => void;
};
};

View File

@ -0,0 +1,178 @@
import { RadioChangeEvent } from 'antd';
import { getAggregateKeys } from 'api/queryBuilder/getAttributeKeys';
import { QueryBuilderKeys } from 'constants/queryBuilder';
import { useNotifications } from 'hooks/useNotifications';
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 { notifications } = useNotifications();
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 selectedColumnKeys = useMemo(
() => optionsQueryData?.selectColumns?.map(({ id }) => id) || [],
[optionsQueryData],
);
const addColumnOptions = useMemo(
() => getOptionsFromKeys(attributeKeys, selectedColumnKeys),
[attributeKeys, selectedColumnKeys],
);
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) => {
const newSelectedColumns = optionsQueryData?.selectColumns?.filter(
({ id }) => id !== columnKey,
);
if (!newSelectedColumns.length) {
notifications.error({
message: 'There must be at least one selected column',
});
} else {
redirectWithOptionsData({
...defaultOptionsQuery,
selectColumns: newSelectedColumns,
});
}
},
[optionsQueryData, notifications, 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: optionsQueryData?.selectColumns || defaultOptionsQuery.selectColumns,
options: addColumnOptions || [],
onChange: handleSelectedColumnsChange,
onRemove: handleRemoveSelectedColumn,
},
format: {
value: optionsQueryData?.format || defaultOptionsQuery.format,
onChange: handleFormatChange,
},
maxLines: {
value: optionsQueryData?.maxLines || defaultOptionsQuery.maxLines,
onChange: handleMaxLinesChange,
},
}),
[
addColumnOptions,
optionsQueryData?.maxLines,
optionsQueryData?.format,
optionsQueryData?.selectColumns,
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,28 @@
import { SelectProps } from 'antd';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
export const getOptionsFromKeys = (
keys: BaseAutocompleteData[],
selectedKeys: (string | undefined)[],
): SelectProps['options'] => {
const options = keys.map(({ id, key }) => ({
label: key,
value: id,
}));
return options.filter(
({ value }) => !selectedKeys.find((key) => key === value),
);
};
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

@ -15,26 +15,36 @@ import { ActionsWrapperStyled } from './QueryBuilder.styled';
export const QueryBuilder = memo(function QueryBuilder({
config,
panelType,
panelType: newPanelType,
actions,
}: QueryBuilderProps): JSX.Element {
const {
currentQuery,
setupInitialDataSource,
addNewBuilderQuery,
addNewFormula,
handleSetPanelType,
handleSetConfig,
panelType,
initialDataSource,
} = useQueryBuilder();
useEffect(() => {
if (config && config.queryVariant === 'static') {
setupInitialDataSource(config.initialDataSource);
}
}, [config, setupInitialDataSource]);
const currentDataSource = useMemo(
() =>
(config && config.queryVariant === 'static' && config.initialDataSource) ||
null,
[config],
);
useEffect(() => {
handleSetPanelType(panelType);
}, [handleSetPanelType, panelType]);
if (currentDataSource !== initialDataSource || newPanelType !== panelType) {
handleSetConfig(newPanelType, currentDataSource);
}
}, [
handleSetConfig,
panelType,
initialDataSource,
currentDataSource,
newPanelType,
]);
const isDisabledQueryButton = useMemo(
() => currentQuery.builder.queryData.length >= MAX_QUERIES,
@ -48,7 +58,7 @@ export const QueryBuilder = memo(function QueryBuilder({
const isAvailableToDisableQuery = useMemo(
() =>
currentQuery.builder.queryData.length > 1 ||
currentQuery.builder.queryData.length > 0 ||
currentQuery.builder.queryFormulas.length > 0,
[currentQuery],
);

View File

@ -40,6 +40,7 @@ export const Query = memo(function Query({
const {
operators,
isMetricsDataSource,
isTracePanelType,
listOfAdditionalFilters,
handleChangeAggregatorAttribute,
handleChangeDataSource,
@ -196,7 +197,56 @@ export const Query = memo(function Query({
}
default: {
return null;
return (
<>
<Col span={11}>
<Row gutter={[11, 5]}>
<Col flex="5.93rem">
<FilterLabel label="Limit" />
</Col>
<Col flex="1 1 12.5rem">
<LimitFilter query={query} onChange={handleChangeLimit} />
</Col>
</Row>
</Col>
<Col span={11}>
<Row gutter={[11, 5]}>
<Col flex="5.93rem">
<FilterLabel label="HAVING" />
</Col>
<Col flex="1 1 12.5rem">
<HavingFilter onChange={handleChangeHavingFilter} query={query} />
</Col>
</Row>
</Col>
<Col span={11}>
<Row gutter={[11, 5]}>
<Col flex="5.93rem">
<FilterLabel label="Order by" />
</Col>
<Col flex="1 1 12.5rem">
<OrderByFilter query={query} onChange={handleChangeOrderByKeys} />
</Col>
</Row>
</Col>
{panelType !== PANEL_TYPES.LIST && (
<Col span={11}>
<Row gutter={[11, 5]}>
<Col flex="5.93rem">
<FilterLabel label="Aggregate Every" />
</Col>
<Col flex="1 1 6rem">
<AggregateEveryFilter
query={query}
onChange={handleChangeAggregateEvery}
/>
</Col>
</Row>
</Col>
)}
</>
);
}
}
}, [
@ -278,8 +328,11 @@ export const Query = memo(function Query({
</Col>
<Col flex="1 1 12.5rem">
<AggregatorFilter
onChange={handleChangeAggregatorAttribute}
query={query}
onChange={handleChangeAggregatorAttribute}
disabled={
panelType === PANEL_TYPES.LIST || panelType === PANEL_TYPES.TRACE
}
/>
</Col>
</Row>
@ -305,21 +358,25 @@ export const Query = memo(function Query({
</Col>
</Row>
</Col>
<Col span={24}>
<AdditionalFiltersToggler listOfAdditionalFilter={listOfAdditionalFilters}>
<Row gutter={[0, 11]} justify="space-between">
{renderAdditionalFilters()}
</Row>
</AdditionalFiltersToggler>
</Col>
<Row style={{ width: '100%' }}>
<Input
onChange={handleChangeQueryLegend}
size="middle"
value={query.legend}
addonBefore="Legend Format"
/>
</Row>
{!isTracePanelType && (
<Col span={24}>
<AdditionalFiltersToggler listOfAdditionalFilter={listOfAdditionalFilters}>
<Row gutter={[0, 11]} justify="space-between">
{renderAdditionalFilters()}
</Row>
</AdditionalFiltersToggler>
</Col>
)}
{panelType !== PANEL_TYPES.LIST && panelType !== PANEL_TYPES.TRACE && (
<Row style={{ width: '100%' }}>
<Input
onChange={handleChangeQueryLegend}
size="middle"
value={query.legend}
addonBefore="Legend Format"
/>
</Row>
)}
</ListItemWrapper>
);
});

View File

@ -1,11 +1,7 @@
import { Input } from 'antd';
import getStep from 'lib/getStep';
import { InputNumber, InputNumberProps } from 'antd';
import { useMemo } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { selectStyle } from '../QueryBuilderSearch/config';
@ -13,49 +9,27 @@ function AggregateEveryFilter({
onChange,
query,
}: AggregateEveryFilterProps): JSX.Element {
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const stepInterval = useMemo(
() =>
getStep({
start: minTime,
end: maxTime,
inputFormat: 'ns',
}),
[maxTime, minTime],
);
const handleKeyDown = (event: {
keyCode: number;
which: number;
preventDefault: () => void;
}): void => {
const keyCode = event.keyCode || event.which;
const isBackspace = keyCode === 8;
const isNumeric =
(keyCode >= 48 && keyCode <= 57) || (keyCode >= 96 && keyCode <= 105);
if (!isNumeric && !isBackspace) {
event.preventDefault();
}
};
const isMetricsDataSource = useMemo(
() => query.dataSource === DataSource.METRICS,
[query.dataSource],
);
const onChangeHandler: InputNumberProps<number>['onChange'] = (event) => {
if (event && event >= 0) {
onChange(event);
}
};
const isDisabled = isMetricsDataSource && !query.aggregateAttribute.key;
return (
<Input
type="text"
<InputNumber
placeholder="Enter in seconds"
disabled={isMetricsDataSource && !query.aggregateAttribute.key}
disabled={isDisabled}
style={selectStyle}
defaultValue={query.stepInterval ?? stepInterval}
onChange={(event): void => onChange(Number(event.target.value))}
onKeyDown={handleKeyDown}
value={query.stepInterval}
onChange={onChangeHandler}
min={0}
/>
);
}

View File

@ -1,7 +1,8 @@
import { AutoCompleteProps } from 'antd';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
export type AgregatorFilterProps = {
onChange: (value: BaseAutocompleteData) => void;
export type AgregatorFilterProps = Pick<AutoCompleteProps, 'disabled'> & {
query: IBuilderQuery;
onChange: (value: BaseAutocompleteData) => void;
};

View File

@ -28,8 +28,9 @@ import { selectStyle } from '../QueryBuilderSearch/config';
import { AgregatorFilterProps } from './AggregatorFilter.intefaces';
export const AggregatorFilter = memo(function AggregatorFilter({
onChange,
query,
disabled,
onChange,
}: AgregatorFilterProps): JSX.Element {
const [optionsData, setOptionsData] = useState<ExtendedSelectOption[]>([]);
const debouncedValue = useDebounce(query.aggregateAttribute.key, 300);
@ -119,6 +120,7 @@ export const AggregatorFilter = memo(function AggregatorFilter({
options={optionsData}
value={value}
onChange={handleChangeAttribute}
disabled={disabled}
/>
);
});

View File

@ -102,10 +102,12 @@ export const GroupByFilter = memo(function GroupByFilter({
return {
id,
key,
dataType: dataType as DataType,
type: type as AutocompleteType,
isColumn: isColumn === 'true',
key: key || currentValue,
dataType: (dataType as DataType) || initialAutocompleteData.dataType,
type: (type as AutocompleteType) || initialAutocompleteData.type,
isColumn: isColumn
? isColumn === 'true'
: initialAutocompleteData.isColumn,
};
}

View File

@ -1,6 +1,8 @@
import { Select, Spin } from 'antd';
import { getAggregateKeys } from 'api/queryBuilder/getAttributeKeys';
import { QueryBuilderKeys } from 'constants/queryBuilder';
import { IOption } from 'hooks/useResourceAttribute/types';
import { uniqWith } from 'lodash-es';
import * as Papa from 'papaparse';
import { useCallback, useMemo, useState } from 'react';
import { useQuery } from 'react-query';
@ -9,12 +11,14 @@ import { DataSource, MetricAggregateOperator } from 'types/common/queryBuilder';
import { selectStyle } from '../QueryBuilderSearch/config';
import { getRemoveOrderFromValue } from '../QueryBuilderSearch/utils';
import { FILTERS } from './config';
import { OrderByFilterProps } from './OrderByFilter.interfaces';
import {
checkIfKeyPresent,
getLabelFromValue,
mapLabelValuePairs,
orderByValueDelimiter,
transformToOrderByStringValues,
} from './utils';
export function OrderByFilter({
@ -22,7 +26,9 @@ export function OrderByFilter({
onChange,
}: OrderByFilterProps): JSX.Element {
const [searchText, setSearchText] = useState<string>('');
const [selectedValue, setSelectedValue] = useState<string[]>([]);
const [selectedValue, setSelectedValue] = useState<IOption[]>(
transformToOrderByStringValues(query.orderBy) || [],
);
const { data, isFetching } = useQuery(
[QueryBuilderKeys.GET_AGGREGATE_KEYS, searchText],
@ -55,23 +61,41 @@ export function OrderByFilter({
.flat()
.concat([
{
label: `${query.aggregateOperator}(${query.aggregateAttribute.key}) asc`,
value: `${query.aggregateOperator}(${query.aggregateAttribute.key})${orderByValueDelimiter}asc`,
label: `${query.aggregateOperator}(${query.aggregateAttribute.key}) ${FILTERS.ASC}`,
value: `${query.aggregateOperator}(${query.aggregateAttribute.key})${orderByValueDelimiter}${FILTERS.ASC}`,
},
{
label: `${query.aggregateOperator}(${query.aggregateAttribute.key}) desc`,
value: `${query.aggregateOperator}(${query.aggregateAttribute.key})${orderByValueDelimiter}desc`,
label: `${query.aggregateOperator}(${query.aggregateAttribute.key}) ${FILTERS.DESC}`,
value: `${query.aggregateOperator}(${query.aggregateAttribute.key})${orderByValueDelimiter}${FILTERS.DESC}`,
},
]),
[query.aggregateAttribute.key, query.aggregateOperator, query.groupBy],
);
const customValue: IOption[] = useMemo(() => {
if (!searchText) return [];
return [
{
label: `${searchText} ${FILTERS.ASC}`,
value: `${searchText}${orderByValueDelimiter}${FILTERS.ASC}`,
},
{
label: `${searchText} ${FILTERS.DESC}`,
value: `${searchText}${orderByValueDelimiter}${FILTERS.DESC}`,
},
];
}, [searchText]);
const optionsData = useMemo(() => {
const options =
query.aggregateOperator === MetricAggregateOperator.NOOP
? noAggregationOptions
: aggregationOptions;
return options.filter(
const resultOption = [...customValue, ...options];
return resultOption.filter(
(option) =>
!getLabelFromValue(selectedValue).includes(
getRemoveOrderFromValue(option.value),
@ -79,30 +103,58 @@ export function OrderByFilter({
);
}, [
aggregationOptions,
customValue,
noAggregationOptions,
query.aggregateOperator,
selectedValue,
]);
const handleChange = (values: string[]): void => {
setSelectedValue(values);
const orderByValues: OrderByPayload[] = values.map((item) => {
const match = Papa.parse(item, { delimiter: '|' });
const getUniqValues = useCallback((values: IOption[]): IOption[] => {
const modifiedValues = values.map((item) => {
const match = Papa.parse(item.value, { delimiter: orderByValueDelimiter });
if (!match) return { label: item.label, value: item.value };
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-unused-vars
const [_, order] = match.data.flat() as string[];
if (order) return { label: item.label, value: item.value };
return {
label: `${item.value} ${FILTERS.ASC}`,
value: `${item.value}${orderByValueDelimiter}${FILTERS.ASC}`,
};
});
return uniqWith(
modifiedValues,
(current, next) =>
getRemoveOrderFromValue(current.value) ===
getRemoveOrderFromValue(next.value),
);
}, []);
const handleChange = (values: IOption[]): void => {
const result = getUniqValues(values);
setSelectedValue(result);
const orderByValues: OrderByPayload[] = result.map((item) => {
const match = Papa.parse(item.value, { delimiter: orderByValueDelimiter });
if (match) {
const [columnName, order] = match.data.flat() as string[];
return {
columnName: checkIfKeyPresent(columnName, query.aggregateAttribute.key)
? '#SIGNOZ_VALUE'
: columnName,
order,
order: order ?? 'asc',
};
}
return {
columnName: item,
order: '',
columnName: item.value,
order: 'asc',
};
});
setSearchText('');
onChange(orderByValues);
};
@ -126,6 +178,8 @@ export function OrderByFilter({
showSearch
disabled={isMetricsDataSource && isDisabledSelect}
showArrow={false}
value={selectedValue}
labelInValue
filterOption={false}
options={optionsData}
notFoundContent={isFetching ? <Spin size="small" /> : null}

View File

@ -0,0 +1,4 @@
export const FILTERS = {
ASC: 'asc',
DESC: 'desc',
};

View File

@ -2,9 +2,18 @@ import { IOption } from 'hooks/useResourceAttribute/types';
import { transformStringWithPrefix } from 'lib/query/transformStringWithPrefix';
import * as Papa from 'papaparse';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { OrderByPayload } from 'types/api/queryBuilder/queryBuilderData';
export const orderByValueDelimiter = '|';
export const transformToOrderByStringValues = (
orderBy: OrderByPayload[],
): IOption[] =>
orderBy.map((item) => ({
label: `${item.columnName} ${item.order}`,
value: `${item.columnName}${orderByValueDelimiter}${item.order}`,
}));
export function mapLabelValuePairs(
arr: BaseAutocompleteData[],
): Array<IOption>[] {
@ -28,14 +37,15 @@ export function mapLabelValuePairs(
});
}
export function getLabelFromValue(arr: string[]): string[] {
export function getLabelFromValue(arr: IOption[]): string[] {
return arr.flat().map((item) => {
const match = Papa.parse(item, { delimiter: orderByValueDelimiter });
const match = Papa.parse(item.value, { delimiter: orderByValueDelimiter });
if (match) {
const [key] = match.data as string[];
return key[0];
}
return item;
return item.value;
});
}

View File

@ -0,0 +1,16 @@
import { TableProps } from 'antd';
import { ColumnsType } from 'antd/es/table';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { ReactNode } from 'react';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { QueryDataV3 } from 'types/api/widgets/getQuery';
export type QueryTableProps = Omit<
TableProps<RowData>,
'columns' | 'dataSource'
> & {
queryTableData: QueryDataV3[];
query: Query;
renderActionCell?: (record: RowData) => ReactNode;
modifyColumns?: (columns: ColumnsType<RowData>) => ColumnsType<RowData>;
};

View File

@ -0,0 +1,57 @@
import type { ColumnsType } from 'antd/es/table';
import { ResizeTable } from 'components/ResizeTable';
import dayjs from 'dayjs';
import {
createTableColumnsFromQuery,
RowData,
} from 'lib/query/createTableColumnsFromQuery';
import { useMemo } from 'react';
import { QueryTableProps } from './QueryTable.intefaces';
export function QueryTable({
queryTableData,
query,
renderActionCell,
modifyColumns,
...props
}: QueryTableProps): JSX.Element {
const { columns, dataSource } = useMemo(
() =>
createTableColumnsFromQuery({
query,
queryTableData,
renderActionCell,
}),
[query, queryTableData, renderActionCell],
);
const modifiedColumns = useMemo(() => {
const currentColumns: ColumnsType<RowData> = columns.map((column) =>
column.key === 'timestamp'
? {
...column,
render: (_, record): string =>
dayjs(new Date(record.timestamp)).format('MMM DD, YYYY, HH:mm:ss'),
}
: column,
);
return currentColumns;
}, [columns]);
const tableColumns = modifyColumns
? modifyColumns(modifiedColumns)
: modifiedColumns;
return (
<ResizeTable
columns={tableColumns}
tableLayout="fixed"
dataSource={dataSource}
scroll={{ x: true }}
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
/>
);
}

View File

@ -0,0 +1 @@
export { QueryTable } from './QueryTable';

View File

@ -0,0 +1,53 @@
import Graph from 'components/Graph';
import Spinner from 'components/Spinner';
import getChartData from 'lib/getChartData';
import { useMemo } from 'react';
import { SuccessResponse } from 'types/api';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { Container, ErrorText } from './styles';
function TimeSeriesView({
data,
isLoading,
isError,
}: TimeSeriesViewProps): JSX.Element {
const chartData = useMemo(
() =>
getChartData({
queryData: [
{
queryData: data?.payload?.data?.result || [],
},
],
}),
[data?.payload?.data?.result],
);
return (
<Container>
{isLoading && <Spinner height="50vh" size="small" tip="Loading..." />}
{isError && <ErrorText>{data?.error || 'Something went wrong'}</ErrorText>}
{!isLoading && !isError && (
<Graph
animate={false}
data={chartData}
name="tracesExplorerGraph"
type="line"
/>
)}
</Container>
);
}
interface TimeSeriesViewProps {
data?: SuccessResponse<MetricRangePayloadProps>;
isLoading: boolean;
isError: boolean;
}
TimeSeriesView.defaultProps = {
data: undefined,
};
export default TimeSeriesView;

View File

@ -0,0 +1,55 @@
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { DataSource } from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import TimeSeriesView from './TimeSeriesView';
function TimeSeriesViewContainer({
dataSource = DataSource.TRACES,
}: TimeSeriesViewProps): JSX.Element {
const { stagedQuery, panelType } = useQueryBuilder();
const { selectedTime: globalSelectedTime, maxTime, minTime } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
const { data, isLoading, isError } = useGetQueryRange(
{
query: stagedQuery || initialQueriesMap[dataSource],
graphType: panelType || PANEL_TYPES.TIME_SERIES,
selectedTime: 'GLOBAL_TIME',
globalSelectedInterval: globalSelectedTime,
params: {
dataSource,
},
},
{
queryKey: [
REACT_QUERY_KEY.GET_QUERY_RANGE,
globalSelectedTime,
maxTime,
minTime,
stagedQuery,
],
enabled: !!stagedQuery && panelType === PANEL_TYPES.TIME_SERIES,
},
);
return <TimeSeriesView isError={isError} isLoading={isLoading} data={data} />;
}
interface TimeSeriesViewProps {
dataSource?: DataSource;
}
TimeSeriesViewContainer.defaultProps = {
dataSource: DataSource.TRACES,
};
export default TimeSeriesViewContainer;

View File

@ -4,6 +4,9 @@ import getLocalStorageKey from 'api/browser/localstorage/get';
import setLocalStorageKey from 'api/browser/localstorage/set';
import { LOCALSTORAGE } from 'constants/localStorage';
import dayjs, { Dayjs } from 'dayjs';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { updateStepInterval } from 'hooks/queryBuilder/useStepInterval';
import GetMinMax from 'lib/getMinMax';
import getTimeString from 'lib/getTimeString';
import { useCallback, useEffect, useState } from 'react';
import { connect, useSelector } from 'react-redux';
@ -66,6 +69,8 @@ function DateTimeSelection({
false,
);
const { stagedQuery, initQueryBuilderData } = useQueryBuilder();
const { maxTime, minTime, selectedTime } = useSelector<
AppState,
GlobalReducer
@ -174,6 +179,14 @@ function DateTimeSelection({
setRefreshButtonHidden(true);
setCustomDTPickerVisible(true);
}
if (!stagedQuery) {
return;
}
const { maxTime, minTime } = GetMinMax(value, getTime());
initQueryBuilderData(updateStepInterval(stagedQuery, maxTime, minTime));
};
const onRefreshHandler = (): void => {

View File

@ -1,25 +1,51 @@
import Controls from 'container/Controls';
import Controls, { ControlsProps } from 'container/Controls';
import OptionsMenu from 'container/OptionsMenu';
import { OptionsMenuConfig } from 'container/OptionsMenu/types';
import useQueryPagination from 'hooks/queryPagination/useQueryPagination';
import { memo } from 'react';
import { Container } from './styles';
function TraceExplorerControls(): JSX.Element | null {
const handleCountItemsPerPageChange = (): void => {};
const handleNavigatePrevious = (): void => {};
const handleNavigateNext = (): void => {};
function TraceExplorerControls({
isLoading,
totalCount,
perPageOptions,
config,
}: TraceExplorerControlsProps): JSX.Element | null {
const {
pagination,
handleCountItemsPerPageChange,
handleNavigateNext,
handleNavigatePrevious,
} = useQueryPagination(totalCount, perPageOptions);
return (
<Container>
{config && <OptionsMenu config={{ addColumn: config?.addColumn }} />}
<Controls
isLoading={false}
count={0}
countPerPage={0}
handleNavigatePrevious={handleNavigatePrevious}
handleNavigateNext={handleNavigateNext}
isLoading={isLoading}
totalCount={totalCount}
offset={pagination.offset}
countPerPage={pagination.limit}
perPageOptions={perPageOptions}
handleCountItemsPerPageChange={handleCountItemsPerPageChange}
handleNavigateNext={handleNavigateNext}
handleNavigatePrevious={handleNavigatePrevious}
/>
</Container>
);
}
TraceExplorerControls.defaultProps = {
config: null,
};
type TraceExplorerControlsProps = Pick<
ControlsProps,
'isLoading' | 'totalCount' | 'perPageOptions'
> & {
config?: OptionsMenuConfig | null;
};
export default memo(TraceExplorerControls);

View File

@ -0,0 +1,11 @@
import { DEFAULT_PER_PAGE_OPTIONS } from 'hooks/queryPagination';
export const defaultSelectedColumns: string[] = [
'name',
'serviceName',
'responseStatusCode',
'httpMethod',
'durationNano',
];
export const PER_PAGE_OPTIONS: number[] = [10, ...DEFAULT_PER_PAGE_OPTIONS];

View File

@ -0,0 +1,134 @@
import { ColumnsType } from 'antd/es/table';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useOptionsMenu } from 'container/OptionsMenu';
import { QueryTable } from 'container/QueryTable';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { Pagination, URL_PAGINATION } from 'hooks/queryPagination';
import useUrlQueryData from 'hooks/useUrlQueryData';
import history from 'lib/history';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { HTMLAttributes, memo, useCallback, useMemo } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { DataSource } from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import TraceExplorerControls from '../Controls';
import { defaultSelectedColumns, PER_PAGE_OPTIONS } from './configs';
import { Container, ErrorText, tableStyles } from './styles';
import { getTraceLink, modifyColumns, transformDataWithDate } from './utils';
function ListView(): JSX.Element {
const { stagedQuery, panelType } = useQueryBuilder();
const { selectedTime: globalSelectedTime, maxTime, minTime } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
const { options, config } = useOptionsMenu({
dataSource: DataSource.TRACES,
aggregateOperator: 'count',
initialOptions: {
selectColumns: defaultSelectedColumns,
},
});
const { queryData: paginationQueryData } = useUrlQueryData<Pagination>(
URL_PAGINATION,
);
const { data, isFetching, isError } = useGetQueryRange(
{
query: stagedQuery || initialQueriesMap.traces,
graphType: panelType || PANEL_TYPES.LIST,
selectedTime: 'GLOBAL_TIME',
globalSelectedInterval: globalSelectedTime,
params: {
dataSource: 'traces',
},
tableParams: {
pagination: paginationQueryData,
selectColumns: options?.selectColumns,
},
},
{
queryKey: [
REACT_QUERY_KEY.GET_QUERY_RANGE,
globalSelectedTime,
maxTime,
minTime,
stagedQuery,
panelType,
paginationQueryData,
options?.selectColumns,
],
enabled:
!!stagedQuery && panelType === PANEL_TYPES.LIST && !!options?.selectColumns,
},
);
const dataLength =
data?.payload?.data?.newResult?.data?.result[0]?.list?.length;
const totalCount = useMemo(() => dataLength || 0, [dataLength]);
const queryTableDataResult = data?.payload.data.newResult.data.result;
const queryTableData = useMemo(() => queryTableDataResult || [], [
queryTableDataResult,
]);
const transformedQueryTableData = useMemo(
() => transformDataWithDate(queryTableData),
[queryTableData],
);
const handleModifyColumns = useCallback(
(columns: ColumnsType<RowData>) =>
modifyColumns(columns, options?.selectColumns || []),
[options?.selectColumns],
);
const handleRow = useCallback(
(record: RowData): HTMLAttributes<RowData> => ({
onClick: (event): void => {
event.preventDefault();
event.stopPropagation();
if (event.metaKey || event.ctrlKey) {
window.open(getTraceLink(record), '_blank');
} else {
history.push(getTraceLink(record));
}
},
}),
[],
);
return (
<Container>
<TraceExplorerControls
isLoading={isFetching}
totalCount={totalCount}
config={config}
perPageOptions={PER_PAGE_OPTIONS}
/>
{isError && <ErrorText>{data?.error || 'Something went wrong'}</ErrorText>}
{!isError && (
<QueryTable
query={stagedQuery || initialQueriesMap.traces}
queryTableData={transformedQueryTableData}
modifyColumns={handleModifyColumns}
loading={isFetching}
pagination={false}
style={tableStyles}
onRow={handleRow}
/>
)}
</Container>
);
}
export default memo(ListView);

View File

@ -0,0 +1,21 @@
import { Typography } from 'antd';
import { CSSProperties } from 'react';
import styled from 'styled-components';
export const tableStyles: CSSProperties = {
cursor: 'pointer',
};
export const Container = styled.div`
display: flex;
flex-direction: column;
gap: 15px;
`;
export const ErrorText = styled(Typography)`
text-align: center;
`;
export const DateText = styled(Typography)`
min-width: 145px;
`;

View File

@ -0,0 +1,100 @@
import { Tag } from 'antd';
import { ColumnsType } from 'antd/es/table';
import Typography from 'antd/es/typography/Typography';
import ROUTES from 'constants/routes';
import { getMs } from 'container/Trace/Filters/Panel/PanelBody/Duration/util';
import { formUrlParams } from 'container/TraceDetail/utils';
import dayjs from 'dayjs';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { QueryDataV3 } from 'types/api/widgets/getQuery';
import { DateText } from './styles';
export const transformDataWithDate = (data: QueryDataV3[]): QueryDataV3[] =>
data.map((query) => ({
...query,
list:
query?.list?.map((listItem) => ({
...listItem,
data: {
...listItem?.data,
date: listItem?.timestamp,
},
})) || null,
}));
export const modifyColumns = (
columns: ColumnsType<RowData>,
selectedColumns: BaseAutocompleteData[],
): ColumnsType<RowData> => {
const initialColumns = columns.filter(({ key }) => {
let isValidColumn = true;
const checkIsExistColumnByKey = (attributeKey: string): boolean =>
!selectedColumns.find(({ key }) => key === attributeKey) &&
attributeKey === key;
const isSelectedSpanId = checkIsExistColumnByKey('spanID');
const isSelectedTraceId = checkIsExistColumnByKey('traceID');
if (isSelectedSpanId || isSelectedTraceId || key === 'date')
isValidColumn = false;
return isValidColumn;
});
const dateColumn = columns.find(({ key }) => key === 'date');
if (dateColumn) {
initialColumns.unshift(dateColumn);
}
return initialColumns.map((column) => {
const key = column.key as string;
const getHttpMethodOrStatus = (value: string): JSX.Element => {
if (value === 'N/A') {
return <Typography>{value}</Typography>;
}
return <Tag color="magenta">{value}</Tag>;
};
if (key === 'durationNano') {
return {
...column,
render: (duration: string): JSX.Element => (
<Typography>{getMs(duration)}ms</Typography>
),
};
}
if (key === 'httpMethod' || key === 'responseStatusCode') {
return {
...column,
render: getHttpMethodOrStatus,
};
}
if (key === 'date') {
return {
...column,
width: 145,
render: (date: string): JSX.Element => {
const day = dayjs(date);
return <DateText>{day.format('YYYY/MM/DD HH:mm:ss')}</DateText>;
},
};
}
return column;
});
};
export const getTraceLink = (record: RowData): string =>
`${ROUTES.TRACE}/${record.traceID}${formUrlParams({
spanId: record.spanID,
levelUp: 0,
levelDown: 0,
})}`;

View File

@ -1,7 +1,9 @@
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 { memo } from 'react';
import { DataSource } from 'types/common/queryBuilder';
import { ButtonWrapper, Container } from './styles';
@ -9,10 +11,12 @@ import { ButtonWrapper, Container } from './styles';
function QuerySection(): JSX.Element {
const { handleRunQuery } = useQueryBuilder();
const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
return (
<Container>
<QueryBuilder
panelType={PANEL_TYPES.TIME_SERIES}
panelType={panelTypes}
config={{
queryVariant: 'static',
initialDataSource: DataSource.TRACES,
@ -29,4 +33,4 @@ function QuerySection(): JSX.Element {
);
}
export default QuerySection;
export default memo(QuerySection);

View File

@ -1,73 +0,0 @@
import Graph from 'components/Graph';
import Spinner from 'components/Spinner';
import { initialQueriesMap } from 'constants/queryBuilder';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import getChartData from 'lib/getChartData';
import { useMemo } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { Container, ErrorText } from './styles';
function TimeSeriesView(): JSX.Element {
const { stagedQuery } = useQueryBuilder();
const { selectedTime: globalSelectedTime, maxTime, minTime } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
const { data, isLoading, isError } = useGetQueryRange(
{
query: stagedQuery || initialQueriesMap.traces,
graphType: 'graph',
selectedTime: 'GLOBAL_TIME',
globalSelectedInterval: globalSelectedTime,
params: {
dataSource: 'traces',
},
},
{
queryKey: [
REACT_QUERY_KEY.GET_QUERY_RANGE,
globalSelectedTime,
stagedQuery,
maxTime,
minTime,
],
enabled: !!stagedQuery,
},
);
const chartData = useMemo(
() =>
getChartData({
queryData: [
{
queryData: data?.payload?.data?.result || [],
},
],
}),
[data],
);
return (
<Container>
{isLoading && <Spinner height="50vh" size="small" tip="Loading..." />}
{isError && <ErrorText>{data?.error || 'Something went wrong'}</ErrorText>}
{!isLoading && !isError && (
<Graph
animate={false}
data={chartData}
name="tracesExplorerGraph"
type="line"
/>
)}
</Container>
);
}
export default TimeSeriesView;

View File

@ -0,0 +1,49 @@
import { Typography } from 'antd';
import { ColumnsType } from 'antd/es/table';
import ROUTES from 'constants/routes';
import { getMs } from 'container/Trace/Filters/Panel/PanelBody/Duration/util';
import { DEFAULT_PER_PAGE_OPTIONS } from 'hooks/queryPagination';
import { generatePath } from 'react-router-dom';
import { ListItem } from 'types/api/widgets/getQuery';
export const PER_PAGE_OPTIONS: number[] = [10, ...DEFAULT_PER_PAGE_OPTIONS];
export const columns: ColumnsType<ListItem['data']> = [
{
title: 'Root Service Name',
dataIndex: 'subQuery.serviceName',
key: 'serviceName',
},
{
title: 'Root Operation Name',
dataIndex: 'subQuery.name',
key: 'name',
},
{
title: 'Root Duration (in ms)',
dataIndex: 'subQuery.durationNano',
key: 'durationNano',
render: (duration: number): JSX.Element => (
<Typography>{getMs(String(duration))}ms</Typography>
),
},
{
title: 'No of Spans',
dataIndex: 'span_count',
key: 'span_count',
},
{
title: 'TraceID',
dataIndex: 'traceID',
key: 'traceID',
render: (traceID: string): JSX.Element => (
<Typography.Link
href={generatePath(ROUTES.TRACE_DETAIL, {
id: traceID,
})}
>
{traceID}
</Typography.Link>
),
},
];

View File

@ -0,0 +1,87 @@
import Typography from 'antd/es/typography/Typography';
import { ResizeTable } from 'components/ResizeTable';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { Pagination, URL_PAGINATION } from 'hooks/queryPagination';
import useUrlQueryData from 'hooks/useUrlQueryData';
import { memo, useMemo } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import TraceExplorerControls from '../Controls';
import { columns, PER_PAGE_OPTIONS } from './configs';
import { ActionsContainer, Container } from './styles';
function TracesView(): JSX.Element {
const { stagedQuery, panelType } = useQueryBuilder();
const { selectedTime: globalSelectedTime, maxTime, minTime } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
const { queryData: paginationQueryData } = useUrlQueryData<Pagination>(
URL_PAGINATION,
);
const { data, isLoading } = useGetQueryRange(
{
query: stagedQuery || initialQueriesMap.traces,
graphType: panelType || PANEL_TYPES.TRACE,
selectedTime: 'GLOBAL_TIME',
globalSelectedInterval: globalSelectedTime,
params: {
dataSource: 'traces',
},
tableParams: {
pagination: paginationQueryData,
},
},
{
queryKey: [
REACT_QUERY_KEY.GET_QUERY_RANGE,
globalSelectedTime,
maxTime,
minTime,
stagedQuery,
panelType,
paginationQueryData,
],
enabled: !!stagedQuery && panelType === PANEL_TYPES.TRACE,
},
);
const responseData = data?.payload?.data?.newResult?.data?.result[0]?.list;
const tableData = useMemo(
() => responseData?.map((listItem) => listItem.data),
[responseData],
);
return (
<Container>
<ActionsContainer>
<Typography>
Showing up to X of the slowest traces form the selected time range
</Typography>
<TraceExplorerControls
isLoading={isLoading}
totalCount={responseData?.length || 0}
perPageOptions={PER_PAGE_OPTIONS}
/>
</ActionsContainer>
<ResizeTable
loading={isLoading}
columns={columns}
tableLayout="fixed"
dataSource={tableData}
scroll={{ x: true }}
pagination={false}
/>
</Container>
);
}
export default memo(TracesView);

View File

@ -0,0 +1,13 @@
import styled from 'styled-components';
export const Container = styled.div`
display: flex;
flex-direction: column;
gap: 15px;
`;
export const ActionsContainer = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
`;

View File

@ -0,0 +1,16 @@
import getAll from 'api/dashboard/getAll';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useQuery, UseQueryResult } from 'react-query';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps } from 'types/api/dashboard/getAll';
export const useGetAllDashboard = (): DashboardProps =>
useQuery({
queryFn: getAll,
queryKey: REACT_QUERY_KEY.GET_ALL_DASHBOARDS,
});
type DashboardProps = UseQueryResult<
SuccessResponse<PayloadProps> | ErrorResponse,
unknown
>;

View File

@ -0,0 +1,14 @@
import update from 'api/dashboard/update';
import { useMutation, UseMutationResult } from 'react-query';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { Dashboard } from 'types/api/dashboard/getAll';
import { Props } from 'types/api/dashboard/update';
export const useUpdateDashboard = (): UseUpdateDashboard => useMutation(update);
type UseUpdateDashboard = UseMutationResult<
SuccessResponse<Dashboard> | ErrorResponse,
unknown,
Props,
unknown
>;

View File

@ -0,0 +1,37 @@
import { PANEL_TYPES } from 'constants/queryBuilder';
import { Dashboard } from 'types/api/dashboard/getAll';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
export const addEmptyWidgetInDashboardJSONWithQuery = (
dashboard: Dashboard,
query: Query,
): Dashboard => ({
...dashboard,
data: {
...dashboard.data,
layout: [
{
i: 'empty',
w: 6,
x: 0,
h: 2,
y: 0,
},
...(dashboard?.data?.layout || []),
],
widgets: [
...(dashboard?.data?.widgets || []),
{
id: 'empty',
query,
description: '',
isStacked: false,
nullZeroValues: '',
opacity: '',
title: '',
timePreferance: 'GLOBAL_TIME',
panelTypes: PANEL_TYPES.TIME_SERIES,
},
],
},
});

View File

@ -8,7 +8,16 @@ export const useGetCompositeQueryParam = (): Query | null => {
return useMemo(() => {
const compositeQuery = urlQuery.get(COMPOSITE_QUERY);
let parsedCompositeQuery: Query | null = null;
return compositeQuery ? JSON.parse(compositeQuery) : null;
try {
if (!compositeQuery) return null;
parsedCompositeQuery = JSON.parse(decodeURIComponent(compositeQuery));
} catch (e) {
parsedCompositeQuery = null;
}
return parsedCompositeQuery;
}, [urlQuery]);
};

View File

@ -1,6 +1,5 @@
import { COMPOSITE_QUERY } from 'constants/queryBuilderQueryNames';
import { initialQueriesMap } from 'constants/queryBuilder';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import useUrlQuery from 'hooks/useUrlQuery';
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
import { UseQueryOptions, UseQueryResult } from 'react-query';
import { useSelector } from 'react-redux';
@ -11,6 +10,7 @@ import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { GlobalReducer } from 'types/reducer/globalTime';
import { useGetQueryRange } from './useGetQueryRange';
import { useQueryBuilder } from './useQueryBuilder';
export const useGetWidgetQueryRange = (
{
@ -19,31 +19,29 @@ export const useGetWidgetQueryRange = (
}: Pick<GetQueryResultsProps, 'graphType' | 'selectedTime'>,
options?: UseQueryOptions<SuccessResponse<MetricRangePayloadProps>, Error>,
): UseQueryResult<SuccessResponse<MetricRangePayloadProps>, Error> => {
const urlQuery = useUrlQuery();
const { selectedTime: globalSelectedInterval } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
const compositeQuery = urlQuery.get(COMPOSITE_QUERY);
const { stagedQuery } = useQueryBuilder();
return useGetQueryRange(
{
graphType,
selectedTime,
globalSelectedInterval,
query: JSON.parse(compositeQuery || '{}'),
query: stagedQuery || initialQueriesMap.metrics,
variables: getDashboardVariables(),
},
{
enabled: !!compositeQuery,
enabled: !!stagedQuery,
retry: false,
queryKey: [
REACT_QUERY_KEY.GET_QUERY_RANGE,
selectedTime,
globalSelectedInterval,
compositeQuery,
stagedQuery,
],
...options,
},

View File

@ -2,6 +2,7 @@ import {
initialAutocompleteData,
initialQueryBuilderFormValuesMap,
mapOfFilters,
PANEL_TYPES,
} from 'constants/queryBuilder';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { getOperatorsBySourceAndPanelType } from 'lib/newQueryBuilder/getOperatorsBySourceAndPanelType';
@ -56,9 +57,16 @@ export const useQueryOperations: UseQueryOperations = ({ query, index }) => {
);
const getNewListOfAdditionalFilters = useCallback(
(dataSource: DataSource): string[] =>
mapOfFilters[dataSource].map((item) => item.text),
[],
(dataSource: DataSource): string[] => {
const listOfFilters = mapOfFilters[dataSource].map((item) => item.text);
if (panelType === PANEL_TYPES.LIST) {
return listOfFilters.filter((filter) => filter !== 'Aggregation interval');
}
return listOfFilters;
},
[panelType],
);
const handleChangeAggregatorAttribute = useCallback(
@ -78,7 +86,7 @@ export const useQueryOperations: UseQueryOperations = ({ query, index }) => {
(nextSource: DataSource): void => {
const newOperators = getOperatorsBySourceAndPanelType({
dataSource: nextSource,
panelType,
panelType: panelType || PANEL_TYPES.TIME_SERIES,
});
const entries = Object.entries(
@ -121,33 +129,22 @@ export const useQueryOperations: UseQueryOperations = ({ query, index }) => {
[query.dataSource],
);
const isTracePanelType = useMemo(() => panelType === PANEL_TYPES.TRACE, [
panelType,
]);
useEffect(() => {
if (initialDataSource && dataSource !== initialDataSource) return;
const initialOperators = getOperatorsBySourceAndPanelType({
dataSource,
panelType,
panelType: panelType || PANEL_TYPES.TIME_SERIES,
});
if (JSON.stringify(operators) === JSON.stringify(initialOperators)) return;
setOperators(initialOperators);
const isCurrentOperatorAvailableInList = initialOperators
.map((operator) => operator.value)
.includes(aggregateOperator);
if (!isCurrentOperatorAvailableInList) {
handleChangeOperator(initialOperators[0].value);
}
}, [
dataSource,
initialDataSource,
panelType,
operators,
aggregateOperator,
handleChangeOperator,
]);
}, [dataSource, initialDataSource, panelType, operators]);
useEffect(() => {
const additionalFilters = getNewListOfAdditionalFilters(dataSource);
@ -156,6 +153,7 @@ export const useQueryOperations: UseQueryOperations = ({ query, index }) => {
}, [dataSource, aggregateOperator, getNewListOfAdditionalFilters]);
return {
isTracePanelType,
isMetricsDataSource,
operators,
listOfAdditionalFilters,

View File

@ -5,11 +5,9 @@ import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { useGetCompositeQueryParam } from './useGetCompositeQueryParam';
import { useQueryBuilder } from './useQueryBuilder';
type UseShareBuilderUrlParams = { defaultValue: Query };
export type UseShareBuilderUrlParams = { defaultValue: Query };
export const useShareBuilderUrl = ({
defaultValue,
}: UseShareBuilderUrlParams): void => {
export const useShareBuilderUrl = (defaultQuery: Query): void => {
const { redirectWithQueryBuilderData, resetStagedQuery } = useQueryBuilder();
const urlQuery = useUrlQuery();
@ -17,9 +15,9 @@ export const useShareBuilderUrl = ({
useEffect(() => {
if (!compositeQuery) {
redirectWithQueryBuilderData(defaultValue);
redirectWithQueryBuilderData(defaultQuery);
}
}, [defaultValue, urlQuery, redirectWithQueryBuilderData, compositeQuery]);
}, [defaultQuery, urlQuery, redirectWithQueryBuilderData, compositeQuery]);
useEffect(
() => (): void => {

View File

@ -0,0 +1,37 @@
import getStep from 'lib/getStep';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { Widgets } from 'types/api/dashboard/getAll';
import { GlobalReducer } from 'types/reducer/globalTime';
export const updateStepInterval = (
query: Widgets['query'],
maxTime: number,
minTime: number,
): Widgets['query'] => {
const stepInterval = getStep({
start: minTime,
end: maxTime,
inputFormat: 'ns',
});
return {
...query,
builder: {
...query?.builder,
queryData:
query?.builder?.queryData?.map((item) => ({
...item,
stepInterval,
})) || [],
},
};
};
export const useStepInterval = (query: Widgets['query']): Widgets['query'] => {
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
return updateStepInterval(query, maxTime, minTime);
};

View File

@ -0,0 +1,3 @@
export const URL_PAGINATION = 'pagination';
export const DEFAULT_PER_PAGE_OPTIONS: number[] = [25, 50, 100, 200];

View File

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

View File

@ -0,0 +1,4 @@
export interface Pagination {
offset: number;
limit: number;
}

View File

@ -0,0 +1,88 @@
import { ControlsProps } from 'container/Controls';
import useUrlQueryData from 'hooks/useUrlQueryData';
import { useCallback, useEffect, useMemo } from 'react';
import { DEFAULT_PER_PAGE_OPTIONS, URL_PAGINATION } from './config';
import { Pagination } from './types';
import {
checkIsValidPaginationData,
getDefaultPaginationConfig,
} from './utils';
const useQueryPagination = (
totalCount: number,
perPageOptions: number[] = DEFAULT_PER_PAGE_OPTIONS,
): UseQueryPagination => {
const defaultPaginationConfig = useMemo(
() => getDefaultPaginationConfig(perPageOptions),
[perPageOptions],
);
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,
perPageOptions,
);
if (paginationQuery && isValidPaginationData) return;
redirectWithCurrentPagination(defaultPaginationConfig);
}, [
defaultPaginationConfig,
perPageOptions,
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,20 @@
import { DEFAULT_PER_PAGE_OPTIONS } from './config';
import { Pagination } from './types';
export const checkIsValidPaginationData = (
{ limit, offset }: Pagination,
perPageOptions: number[],
): boolean =>
Boolean(
Number.isInteger(limit) &&
limit > 0 &&
offset >= 0 &&
perPageOptions.find((option) => option === limit),
);
export const getDefaultPaginationConfig = (
perPageOptions = DEFAULT_PER_PAGE_OPTIONS,
): Pagination => ({
offset: 0,
limit: perPageOptions[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 = useMemo(() => urlQuery.get(queryKey), [queryKey, urlQuery]);
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

@ -39,7 +39,12 @@ describe('lib/getStep', () => {
const startUnix = start.valueOf();
const endUnix = end.valueOf();
const expectedStepSize = Math.floor(end.diff(start, 's') / MaxDataPoints);
let expectedStepSize = Math.max(
Math.floor(end.diff(start, 's') / MaxDataPoints),
DefaultStepSize,
);
expectedStepSize -= expectedStepSize % 60;
expect(
getStep({

View File

@ -7,7 +7,7 @@ export const getDashboardVariables = (): Record<string, unknown> => {
globalTime,
dashboards: { dashboards },
} = store.getState();
const [selectedDashboard] = dashboards;
const [selectedDashboard] = dashboards || [];
const {
data: { variables = {} },
} = selectedDashboard;

View File

@ -0,0 +1,137 @@
import dayjs from 'dayjs';
import getStep, { DefaultStepSize, MaxDataPoints } from './getStep';
describe('get dynamic step size', () => {
test('should return default step size if diffSec is less than MaxDataPoints', () => {
const start = dayjs().subtract(1, 'minute').valueOf();
const end = dayjs().valueOf();
const step = getStep({
start,
end,
inputFormat: 'ms',
});
expect(step).toBe(DefaultStepSize);
});
test('should return appropriate step size if diffSec is more than MaxDataPoints', () => {
const start = dayjs().subtract(4, 'hour').valueOf();
const end = dayjs().valueOf();
const step = getStep({
start,
end,
inputFormat: 'ms',
});
// the expected step size should be no less than DefaultStepSize
const diffSec = Math.abs(dayjs(end).diff(dayjs(start), 's'));
const expectedStep = Math.max(
Math.floor(diffSec / MaxDataPoints),
DefaultStepSize,
);
expect(step).toBe(expectedStep);
});
test('should correctly handle different input formats', () => {
const endSec = dayjs().unix();
const startSec = endSec - 4 * 3600; // 4 hours earlier
const stepSec = getStep({
start: startSec,
end: endSec,
inputFormat: 's',
});
const diffSec = Math.abs(dayjs.unix(endSec).diff(dayjs.unix(startSec), 's'));
const expectedStep = Math.max(
Math.floor(diffSec / MaxDataPoints),
DefaultStepSize,
);
expect(stepSec).toBe(expectedStep);
const startNs = startSec * 1e9; // convert to nanoseconds
const endNs = endSec * 1e9; // convert to nanoseconds
const stepNs = getStep({
start: startNs,
end: endNs,
inputFormat: 'ns',
});
expect(stepNs).toBe(expectedStep); // Expect the same result as 's' inputFormat
});
test('should throw an error for invalid input format', () => {
const start = dayjs().valueOf();
const end = dayjs().valueOf();
expect(() => {
getStep({
start,
end,
inputFormat: 'invalid' as never,
});
}).toThrow('invalid format');
});
test('should return DefaultStepSize when start and end are the same', () => {
const start = dayjs().valueOf();
const end = start; // same as start
const step = getStep({
start,
end,
inputFormat: 'ms',
});
expect(step).toBe(DefaultStepSize);
});
test('should return DefaultStepSize if diffSec is exactly MaxDataPoints', () => {
const endMs = dayjs().valueOf();
const startMs = endMs - MaxDataPoints * 1000; // exactly MaxDataPoints seconds earlier
const step = getStep({
start: startMs,
end: endMs,
inputFormat: 'ms',
});
expect(step).toBe(DefaultStepSize); // since calculated step size is less than DefaultStepSize, it should return DefaultStepSize
});
test('should return DefaultStepSize for future dates less than (MaxDataPoints * DefaultStepSize) seconds ahead', () => {
const start = dayjs().valueOf();
const end = start + MaxDataPoints * DefaultStepSize * 1000 - 1; // just one millisecond less than (MaxDataPoints * DefaultStepSize) seconds ahead
const step = getStep({
start,
end,
inputFormat: 'ms',
});
expect(step).toBe(DefaultStepSize);
});
test('should handle string inputs correctly for a time range greater than (MaxDataPoints * DefaultStepSize) seconds', () => {
const endMs = dayjs().valueOf();
const startMs = endMs - (MaxDataPoints * DefaultStepSize * 1000 + 1); // one millisecond more than (MaxDataPoints * DefaultStepSize) seconds earlier
const step = getStep({
start: startMs.toString(),
end: endMs.toString(),
inputFormat: 'ms',
});
const diffSec = Math.abs(
dayjs(Number(endMs)).diff(dayjs(Number(startMs)), 's'),
);
expect(step).toBe(Math.floor(diffSec / MaxDataPoints));
});
});

View File

@ -30,7 +30,7 @@ const convertToMs = (
};
export const DefaultStepSize = 60;
export const MaxDataPoints = 200;
export const MaxDataPoints = 300;
/**
* Returns relevant step size based on given start and end date.
@ -40,7 +40,13 @@ const getStep = ({ start, end, inputFormat = 'ms' }: GetStepInput): number => {
const endDate = dayjs(convertToMs(Number(end), inputFormat));
const diffSec = Math.abs(endDate.diff(startDate, 's'));
return Math.max(Math.floor(diffSec / MaxDataPoints), DefaultStepSize);
let result =
Math.max(Math.floor(diffSec / MaxDataPoints), DefaultStepSize) ||
DefaultStepSize;
result -= result % 60;
return result;
};
export default getStep;

View File

@ -35,5 +35,9 @@ export const convertNewDataToOld = (
});
const oldResultType = resultType;
return { data: { result: oldResult, resultType: oldResultType } };
// TODO: fix it later for using only v3 version of api
return {
data: { result: oldResult, resultType: oldResultType, newResult: newData },
};
};

Some files were not shown because too many files have changed in this diff Show More