mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-06-22 03:31:46 +08:00
commit
c8f3e9024c
@ -137,7 +137,7 @@ services:
|
||||
condition: on-failure
|
||||
|
||||
query-service:
|
||||
image: signoz/query-service:0.18.0
|
||||
image: signoz/query-service:0.18.1
|
||||
command: ["-config=/root/config/prometheus.yml"]
|
||||
# ports:
|
||||
# - "6060:6060" # pprof port
|
||||
@ -166,7 +166,7 @@ services:
|
||||
<<: *clickhouse-depend
|
||||
|
||||
frontend:
|
||||
image: signoz/frontend:0.18.0
|
||||
image: signoz/frontend:0.18.1
|
||||
deploy:
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
|
@ -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.18.0}
|
||||
image: signoz/query-service:${DOCKER_TAG:-0.18.1}
|
||||
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.18.0}
|
||||
image: signoz/frontend:${DOCKER_TAG:-0.18.1}
|
||||
container_name: frontend
|
||||
restart: on-failure
|
||||
depends_on:
|
||||
|
@ -1,6 +1,7 @@
|
||||
const apiV1 = '/api/v1/';
|
||||
|
||||
export const apiV2 = '/api/v2/';
|
||||
export const apiV3 = '/api/v3/';
|
||||
export const apiAlertManager = '/api/alertmanager';
|
||||
|
||||
export default apiV1;
|
||||
|
@ -9,7 +9,7 @@ import { ENVIRONMENT } from 'constants/env';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import store from 'store';
|
||||
|
||||
import apiV1, { apiAlertManager, apiV2 } from './apiV1';
|
||||
import apiV1, { apiAlertManager, apiV2, apiV3 } from './apiV1';
|
||||
import { Logout } from './utils';
|
||||
|
||||
const interceptorsResponse = (
|
||||
@ -109,6 +109,17 @@ ApiV2Instance.interceptors.response.use(
|
||||
);
|
||||
ApiV2Instance.interceptors.request.use(interceptorsRequestResponse);
|
||||
|
||||
// axios V3
|
||||
export const ApiV3Instance = axios.create({
|
||||
baseURL: `${ENVIRONMENT.baseURL}${apiV3}`,
|
||||
});
|
||||
ApiV3Instance.interceptors.response.use(
|
||||
interceptorsResponse,
|
||||
interceptorRejected,
|
||||
);
|
||||
ApiV3Instance.interceptors.request.use(interceptorsRequestResponse);
|
||||
//
|
||||
|
||||
AxiosAlertManagerInstance.interceptors.response.use(
|
||||
interceptorsResponse,
|
||||
interceptorRejected,
|
||||
|
33
frontend/src/api/queryBuilder/getAggregateAttribute.ts
Normal file
33
frontend/src/api/queryBuilder/getAggregateAttribute.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { ApiV3Instance } from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError, AxiosResponse } from 'axios';
|
||||
// ** Helpers
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
// ** Types
|
||||
import { IGetAggregateAttributePayload } from 'types/api/queryBuilder/getAggregatorAttribute';
|
||||
import { IQueryAutocompleteResponse } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
export const getAggregateAttribute = async ({
|
||||
aggregateOperator,
|
||||
searchText,
|
||||
dataSource,
|
||||
}: IGetAggregateAttributePayload): Promise<
|
||||
SuccessResponse<IQueryAutocompleteResponse> | ErrorResponse
|
||||
> => {
|
||||
try {
|
||||
const response: AxiosResponse<{
|
||||
data: IQueryAutocompleteResponse;
|
||||
}> = await ApiV3Instance.get(
|
||||
`autocomplete/aggregate_attributes?aggregateOperator=${aggregateOperator}&dataSource=${dataSource}&searchText=${searchText}`,
|
||||
);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.statusText,
|
||||
payload: response.data.data,
|
||||
};
|
||||
} catch (e) {
|
||||
return ErrorResponseHandler(e as AxiosError);
|
||||
}
|
||||
};
|
33
frontend/src/api/queryBuilder/getAttributeKeys.ts
Normal file
33
frontend/src/api/queryBuilder/getAttributeKeys.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { ApiV3Instance } from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError, AxiosResponse } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
// ** Types
|
||||
import { IGetAttributeKeysPayload } from 'types/api/queryBuilder/getAttributeKeys';
|
||||
import { IQueryAutocompleteResponse } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
export const getAggregateKeys = async ({
|
||||
aggregateOperator,
|
||||
searchText,
|
||||
dataSource,
|
||||
aggregateAttribute,
|
||||
}: IGetAttributeKeysPayload): Promise<
|
||||
SuccessResponse<IQueryAutocompleteResponse> | ErrorResponse
|
||||
> => {
|
||||
try {
|
||||
const response: AxiosResponse<{
|
||||
data: IQueryAutocompleteResponse;
|
||||
}> = await ApiV3Instance.get(
|
||||
`autocomplete/attribute_keys?aggregateOperator=${aggregateOperator}&dataSource=${dataSource}&aggregateAttribute=${aggregateAttribute}&searchText=${searchText}`,
|
||||
);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.statusText,
|
||||
payload: response.data.data,
|
||||
};
|
||||
} catch (e) {
|
||||
return ErrorResponseHandler(e as AxiosError);
|
||||
}
|
||||
};
|
3
frontend/src/constants/useQueryKeys.ts
Normal file
3
frontend/src/constants/useQueryKeys.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export enum QueryBuilderKeys {
|
||||
GET_AGGREGATE_ATTRIBUTE = 'GET_AGGREGATE_ATTRIBUTE',
|
||||
}
|
@ -61,7 +61,7 @@ export function onGraphClickHandler(
|
||||
const points = chart.getElementsAtEventForMode(
|
||||
event.native,
|
||||
'nearest',
|
||||
{ intersect: true },
|
||||
{ intersect: false },
|
||||
true,
|
||||
);
|
||||
const id = `${from}_button`;
|
||||
|
@ -1,10 +1,9 @@
|
||||
/* eslint-disable */
|
||||
//@ts-nocheck
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { Button, Tabs } from 'antd';
|
||||
import TextToolTip from 'components/TextToolTip';
|
||||
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
|
||||
import { timePreferance } from 'container/NewWidget/RightContainer/timeItems';
|
||||
import { QueryBuilder } from 'container/QueryBuilder';
|
||||
import { cloneDeep, isEqual } from 'lodash-es';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { connect, useSelector } from 'react-redux';
|
||||
@ -31,6 +30,7 @@ import ClickHouseQueryContainer from './QueryBuilder/clickHouse';
|
||||
import PromQLQueryContainer from './QueryBuilder/promQL';
|
||||
import QueryBuilderQueryContainer from './QueryBuilder/queryBuilder';
|
||||
import TabHeader from './TabHeader';
|
||||
import { IHandleUpdatedQuery } from './types';
|
||||
import { getQueryKey } from './utils/getQueryKey';
|
||||
import { showUnstagedStashConfirmBox } from './utils/userSettings';
|
||||
|
||||
@ -54,9 +54,7 @@ function QuerySection({
|
||||
const { search } = useLocation();
|
||||
const { widgets } = selectedDashboards.data;
|
||||
|
||||
const urlQuery = useMemo(() => {
|
||||
return new URLSearchParams(search);
|
||||
}, [search]);
|
||||
const urlQuery = useMemo(() => new URLSearchParams(search), [search]);
|
||||
|
||||
const getWidget = useCallback(() => {
|
||||
const widgetId = urlQuery.get('widgetId');
|
||||
@ -169,6 +167,9 @@ function QuerySection({
|
||||
}
|
||||
selectedGraph={selectedGraph}
|
||||
/>
|
||||
|
||||
// TODO: uncomment for testing new QueryBuilder
|
||||
// <QueryBuilder />
|
||||
),
|
||||
},
|
||||
{
|
||||
|
@ -1,9 +1,12 @@
|
||||
import {
|
||||
IBuilderFormula,
|
||||
IBuilderQuery,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
export type QueryBuilderConfig =
|
||||
| {
|
||||
queryVariant: 'static';
|
||||
initialDataSource: DataSource;
|
||||
}
|
||||
| { queryVariant: 'dropdown' };
|
||||
|
||||
export type QueryBuilderProps = {
|
||||
queryData: IBuilderQuery[];
|
||||
queryFormula: IBuilderFormula[];
|
||||
config?: QueryBuilderConfig;
|
||||
};
|
||||
|
@ -2,14 +2,14 @@
|
||||
import { useQueryBuilder } from 'hooks/useQueryBuilder';
|
||||
import React from 'react';
|
||||
|
||||
// ** Components
|
||||
import { Query } from './components';
|
||||
// ** Types
|
||||
import { QueryBuilderProps } from './QueryBuilder.interfaces';
|
||||
|
||||
// TODO: temporary eslint disable while variable isn't used
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export function QueryBuilder(props: QueryBuilderProps): JSX.Element {
|
||||
// TODO: temporary doesn't use
|
||||
// TODO: I think it can be components switcher, because if we have different views based on the data source, we can render based on source
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export function QueryBuilder({ config }: QueryBuilderProps): JSX.Element {
|
||||
const { queryBuilderData } = useQueryBuilder();
|
||||
|
||||
// Here we can use Form from antd library and fill context data or edit
|
||||
@ -19,5 +19,17 @@ export function QueryBuilder(props: QueryBuilderProps): JSX.Element {
|
||||
// Each component can be part of antd Form list where we can add or remove items
|
||||
// Also need decide to make a copy of queryData for working with form or not and after it set the full new list with formulas or queries to the context
|
||||
// With button to add him
|
||||
return <div>null</div>;
|
||||
return (
|
||||
<div>
|
||||
{queryBuilderData.queryData.map((query, index) => (
|
||||
<Query
|
||||
key={query.queryName}
|
||||
index={index}
|
||||
isAvailableToDisable={queryBuilderData.queryData.length > 1}
|
||||
queryVariant={config?.queryVariant || 'dropdown'}
|
||||
query={query}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -0,0 +1,6 @@
|
||||
import { SelectProps } from 'antd';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
export type QueryLabelProps = {
|
||||
onChange: (value: DataSource) => void;
|
||||
} & Omit<SelectProps, 'onChange'>;
|
@ -0,0 +1,33 @@
|
||||
import { Select } from 'antd';
|
||||
import React from 'react';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { SelectOption } from 'types/common/select';
|
||||
// ** Helpers
|
||||
import { transformToUpperCase } from 'utils/transformToUpperCase';
|
||||
|
||||
// ** Types
|
||||
import { QueryLabelProps } from './DataSourceDropdown.interfaces';
|
||||
|
||||
const dataSourceMap = [DataSource.LOGS, DataSource.METRICS, DataSource.TRACES];
|
||||
|
||||
export function DataSourceDropdown(props: QueryLabelProps): JSX.Element {
|
||||
const { onChange, value, style } = props;
|
||||
|
||||
const dataSourceOptions: SelectOption<
|
||||
DataSource,
|
||||
string
|
||||
>[] = dataSourceMap.map((source) => ({
|
||||
label: transformToUpperCase(source),
|
||||
value: source,
|
||||
}));
|
||||
|
||||
return (
|
||||
<Select
|
||||
defaultValue={dataSourceOptions[0].value}
|
||||
options={dataSourceOptions}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
}
|
@ -0,0 +1 @@
|
||||
export { DataSourceDropdown } from './DataSourceDropdown';
|
@ -0,0 +1,6 @@
|
||||
import { CSSProperties } from 'react';
|
||||
|
||||
export type FilterLabelProps = {
|
||||
label: string;
|
||||
style?: CSSProperties;
|
||||
};
|
@ -0,0 +1,13 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const StyledLabel = styled.div`
|
||||
padding: 0 0.6875rem;
|
||||
min-width: 6.5rem;
|
||||
width: fit-content;
|
||||
min-height: 2rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
border-radius: 0.125rem;
|
||||
border: 0.0625rem solid #434343;
|
||||
background-color: #141414;
|
||||
`;
|
@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
|
||||
// ** Types
|
||||
import { FilterLabelProps } from './FilterLabel.interfaces';
|
||||
// ** Styles
|
||||
import { StyledLabel } from './FilterLabel.styled';
|
||||
|
||||
export function FilterLabel({ label }: FilterLabelProps): JSX.Element {
|
||||
return <StyledLabel>{label}</StyledLabel>;
|
||||
}
|
@ -0,0 +1 @@
|
||||
export { FilterLabel } from './FilterLabel';
|
@ -0,0 +1,2 @@
|
||||
// TODO: temporary type
|
||||
export type FormulaProps = { test: string };
|
@ -0,0 +1,5 @@
|
||||
import React from 'react';
|
||||
|
||||
export function Formula(): JSX.Element {
|
||||
return <div>null</div>;
|
||||
}
|
@ -0,0 +1 @@
|
||||
export { Formula } from './Formula';
|
@ -1,3 +1,5 @@
|
||||
import { CSSProperties } from 'react';
|
||||
|
||||
export type ListMarkerProps = {
|
||||
isDisabled: boolean;
|
||||
labelName: string;
|
||||
@ -5,4 +7,5 @@ export type ListMarkerProps = {
|
||||
className?: string;
|
||||
isAvailableToDisable: boolean;
|
||||
toggleDisabled: (index: number) => void;
|
||||
style?: CSSProperties;
|
||||
};
|
||||
|
@ -14,6 +14,7 @@ export function ListMarker({
|
||||
isAvailableToDisable,
|
||||
className,
|
||||
toggleDisabled,
|
||||
style,
|
||||
}: ListMarkerProps): JSX.Element {
|
||||
const buttonProps: Partial<ButtonProps> = isAvailableToDisable
|
||||
? {
|
||||
@ -29,6 +30,7 @@ export function ListMarker({
|
||||
icon={buttonProps.icon}
|
||||
onClick={buttonProps.onClick}
|
||||
className={className}
|
||||
style={{ marginRight: '0.1rem', ...style }}
|
||||
>
|
||||
{labelName}
|
||||
</StyledButton>
|
||||
|
@ -0,0 +1,8 @@
|
||||
import { IBuilderQueryForm } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
export type QueryProps = {
|
||||
index: number;
|
||||
isAvailableToDisable: boolean;
|
||||
query: IBuilderQueryForm;
|
||||
queryVariant: 'static' | 'dropdown';
|
||||
};
|
@ -0,0 +1,95 @@
|
||||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
import { Col, Row } from 'antd';
|
||||
// ** Components
|
||||
import {
|
||||
DataSourceDropdown,
|
||||
FilterLabel,
|
||||
ListMarker,
|
||||
} from 'container/QueryBuilder/components';
|
||||
import {
|
||||
AggregatorFilter,
|
||||
OperatorsSelect,
|
||||
} from 'container/QueryBuilder/filters';
|
||||
// Context
|
||||
import { useQueryBuilder } from 'hooks/useQueryBuilder';
|
||||
// ** Hooks
|
||||
import React from 'react';
|
||||
import { AutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
// ** Constants
|
||||
import {
|
||||
LogsAggregatorOperator,
|
||||
MetricAggregateOperator,
|
||||
TracesAggregatorOperator,
|
||||
} from 'types/common/queryBuilder';
|
||||
import { transformToUpperCase } from 'utils/transformToUpperCase';
|
||||
|
||||
// ** Types
|
||||
import { QueryProps } from './Query.interfaces';
|
||||
|
||||
const mapOfOperators: Record<DataSource, string[]> = {
|
||||
metrics: Object.values(MetricAggregateOperator),
|
||||
logs: Object.values(LogsAggregatorOperator),
|
||||
traces: Object.values(TracesAggregatorOperator),
|
||||
};
|
||||
|
||||
export function Query({
|
||||
index,
|
||||
isAvailableToDisable,
|
||||
queryVariant,
|
||||
query,
|
||||
}: QueryProps): JSX.Element {
|
||||
const { handleSetQueryData } = useQueryBuilder();
|
||||
|
||||
const currentListOfOperators = mapOfOperators[query.dataSource];
|
||||
|
||||
const handleChangeOperator = (value: string): void => {
|
||||
handleSetQueryData(index, { aggregateOperator: value });
|
||||
};
|
||||
|
||||
const handleChangeDataSource = (nextSource: DataSource): void => {
|
||||
handleSetQueryData(index, { dataSource: nextSource });
|
||||
};
|
||||
|
||||
const handleToggleDisableQuery = (): void => {
|
||||
handleSetQueryData(index, { disabled: !query.disabled });
|
||||
};
|
||||
|
||||
const handleChangeAggregatorAttribute = (value: AutocompleteData): void => {
|
||||
handleSetQueryData(index, { aggregateAttribute: value });
|
||||
};
|
||||
|
||||
return (
|
||||
<Row style={{ gap: '0.75rem' }}>
|
||||
<Col span={12}>
|
||||
<ListMarker
|
||||
isDisabled={query.disabled}
|
||||
toggleDisabled={handleToggleDisableQuery}
|
||||
labelName={query.queryName}
|
||||
index={index}
|
||||
isAvailableToDisable={isAvailableToDisable}
|
||||
/>
|
||||
{queryVariant === 'dropdown' ? (
|
||||
<DataSourceDropdown
|
||||
onChange={handleChangeDataSource}
|
||||
value={query.dataSource}
|
||||
/>
|
||||
) : (
|
||||
<FilterLabel label={transformToUpperCase(query.dataSource)} />
|
||||
)}
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<OperatorsSelect
|
||||
value={query.aggregateOperator || currentListOfOperators[0]}
|
||||
onChange={handleChangeOperator}
|
||||
operators={currentListOfOperators}
|
||||
style={{ minWidth: 104, marginRight: '0.75rem' }}
|
||||
/>
|
||||
<AggregatorFilter
|
||||
onChange={handleChangeAggregatorAttribute}
|
||||
query={query}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
}
|
@ -0,0 +1 @@
|
||||
export { Query } from './Query';
|
@ -1,17 +0,0 @@
|
||||
import { SelectProps } from 'antd';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
type StaticLabel = { variant: 'static'; dataSource: DataSource };
|
||||
|
||||
export type DropdownLabel = {
|
||||
variant: 'dropdown';
|
||||
onChange: (value: DataSource) => void;
|
||||
} & Omit<SelectProps, 'onChange'>;
|
||||
|
||||
export type QueryLabelProps = StaticLabel | DropdownLabel;
|
||||
|
||||
export function isLabelDropdown(
|
||||
label: QueryLabelProps,
|
||||
): label is DropdownLabel {
|
||||
return label.variant === 'dropdown';
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
import { Select } from 'antd';
|
||||
import styled, { css } from 'styled-components';
|
||||
|
||||
// ** Types
|
||||
import { DropdownLabel } from './QueryLabel.interfaces';
|
||||
|
||||
const LabelStyle = css`
|
||||
width: fit-content;
|
||||
min-width: 5.75rem;
|
||||
`;
|
||||
|
||||
export const StyledSingleLabel = styled(Select)`
|
||||
pointer-events: none;
|
||||
${LabelStyle}
|
||||
`;
|
||||
|
||||
export const StyledDropdownLabel = styled(Select)<DropdownLabel>`
|
||||
${LabelStyle}
|
||||
`;
|
@ -1,49 +0,0 @@
|
||||
import { Select } from 'antd';
|
||||
import React from 'react';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { SelectOption } from 'types/common/select';
|
||||
|
||||
// ** Types
|
||||
import { isLabelDropdown, QueryLabelProps } from './QueryLabel.interfaces';
|
||||
// ** Styles
|
||||
import { StyledSingleLabel } from './QueryLabel.styled';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
const dataSourceMap = [DataSource.LOGS, DataSource.METRICS, DataSource.TRACES];
|
||||
|
||||
export function QueryLabel(props: QueryLabelProps): JSX.Element {
|
||||
const isDropdown = isLabelDropdown(props);
|
||||
|
||||
if (!isDropdown) {
|
||||
const { dataSource } = props;
|
||||
|
||||
return (
|
||||
<StyledSingleLabel
|
||||
defaultValue={dataSource}
|
||||
showArrow={false}
|
||||
dropdownStyle={{ display: 'none' }}
|
||||
>
|
||||
<Option value={dataSource}>{dataSource}</Option>
|
||||
</StyledSingleLabel>
|
||||
);
|
||||
}
|
||||
|
||||
const { onChange } = props;
|
||||
|
||||
const dataSourceOptions: SelectOption<
|
||||
DataSource,
|
||||
string
|
||||
>[] = dataSourceMap.map((source) => ({
|
||||
label: source.charAt(0).toUpperCase() + source.slice(1),
|
||||
value: source,
|
||||
}));
|
||||
|
||||
return (
|
||||
<Select
|
||||
defaultValue={dataSourceOptions[0].value}
|
||||
options={dataSourceOptions}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
}
|
@ -1 +0,0 @@
|
||||
export { QueryLabel } from './QueryLabel';
|
@ -1,2 +1,5 @@
|
||||
export { DataSourceDropdown } from './DataSourceDropdown';
|
||||
export { FilterLabel } from './FilterLabel';
|
||||
export { Formula } from './Formula';
|
||||
export { ListMarker } from './ListMarker';
|
||||
export { QueryLabel } from './QueryLabel';
|
||||
export { Query } from './Query';
|
||||
|
@ -0,0 +1,7 @@
|
||||
import { AutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { IBuilderQueryForm } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
export type AgregatorFilterProps = {
|
||||
onChange: (value: AutocompleteData) => void;
|
||||
query: IBuilderQueryForm;
|
||||
};
|
@ -0,0 +1,84 @@
|
||||
// ** Components
|
||||
import { AutoComplete, Spin } from 'antd';
|
||||
// ** Api
|
||||
import { getAggregateAttribute } from 'api/queryBuilder/getAggregateAttribute';
|
||||
import { transformStringWithPrefix } from 'lib/query/transformStringWithPrefix';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { SelectOption } from 'types/common/select';
|
||||
import { transformToUpperCase } from 'utils/transformToUpperCase';
|
||||
|
||||
// ** Types
|
||||
import { AgregatorFilterProps } from './AggregatorFilter.intefaces';
|
||||
|
||||
export function AggregatorFilter({
|
||||
onChange,
|
||||
query,
|
||||
}: AgregatorFilterProps): JSX.Element {
|
||||
const [searchText, setSearchText] = useState<string>('');
|
||||
|
||||
const { data, isFetching } = useQuery(
|
||||
[
|
||||
'GET_AGGREGATE_ATTRIBUTE',
|
||||
searchText,
|
||||
query.aggregateOperator,
|
||||
query.dataSource,
|
||||
],
|
||||
async () =>
|
||||
getAggregateAttribute({
|
||||
aggregateOperator: query.aggregateOperator,
|
||||
dataSource: query.dataSource,
|
||||
searchText,
|
||||
}),
|
||||
{ enabled: !!query.aggregateOperator && !!query.dataSource },
|
||||
);
|
||||
|
||||
const handleSearchAttribute = (searchText: string): void => {
|
||||
setSearchText(searchText);
|
||||
};
|
||||
|
||||
const optionsData: SelectOption<string, string>[] =
|
||||
data?.payload?.attributeKeys?.map((item) => ({
|
||||
label: transformStringWithPrefix({
|
||||
str: item.key,
|
||||
prefix: item.type || '',
|
||||
condition: !item.isColumn,
|
||||
}),
|
||||
value: item.key,
|
||||
})) || [];
|
||||
|
||||
const handleChangeAttribute = (value: string): void => {
|
||||
const currentAttributeObj = data?.payload?.attributeKeys?.find(
|
||||
(item) => item.key === value,
|
||||
) || { key: value, type: null, dataType: null, isColumn: null };
|
||||
|
||||
onChange(currentAttributeObj);
|
||||
};
|
||||
|
||||
const value = useMemo(
|
||||
() =>
|
||||
transformStringWithPrefix({
|
||||
str: query.aggregateAttribute.key,
|
||||
prefix: query.aggregateAttribute.type || '',
|
||||
condition: !query.aggregateAttribute.isColumn,
|
||||
}),
|
||||
[query],
|
||||
);
|
||||
|
||||
return (
|
||||
<AutoComplete
|
||||
showSearch
|
||||
placeholder={`${transformToUpperCase(
|
||||
query.dataSource,
|
||||
)} Name (Start typing to get suggestions)`}
|
||||
style={{ flex: 1, minWidth: 200 }}
|
||||
showArrow={false}
|
||||
filterOption={false}
|
||||
onSearch={handleSearchAttribute}
|
||||
notFoundContent={isFetching ? <Spin size="small" /> : null}
|
||||
options={optionsData}
|
||||
value={value}
|
||||
onChange={handleChangeAttribute}
|
||||
/>
|
||||
);
|
||||
}
|
@ -0,0 +1 @@
|
||||
export { AggregatorFilter } from './AggregatorFilter';
|
@ -0,0 +1,7 @@
|
||||
import { SelectProps } from 'antd';
|
||||
|
||||
export type OperatorsSelectProps = Omit<SelectProps, 'onChange' | 'value'> & {
|
||||
operators: string[];
|
||||
onChange: (value: string) => void;
|
||||
value: string;
|
||||
};
|
@ -0,0 +1,32 @@
|
||||
import { Select } from 'antd';
|
||||
import React from 'react';
|
||||
// ** Types
|
||||
import { SelectOption } from 'types/common/select';
|
||||
// ** Helpers
|
||||
import { transformToUpperCase } from 'utils/transformToUpperCase';
|
||||
|
||||
import { OperatorsSelectProps } from './OperatorsSelect.interfaces';
|
||||
|
||||
export function OperatorsSelect({
|
||||
operators,
|
||||
value,
|
||||
onChange,
|
||||
...props
|
||||
}: OperatorsSelectProps): JSX.Element {
|
||||
const operatorsOptions: SelectOption<string, string>[] = operators.map(
|
||||
(operator) => ({
|
||||
label: transformToUpperCase(operator),
|
||||
value: operator,
|
||||
}),
|
||||
);
|
||||
|
||||
return (
|
||||
<Select
|
||||
options={operatorsOptions}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
@ -0,0 +1 @@
|
||||
export { OperatorsSelect } from './OperatorsSelect';
|
2
frontend/src/container/QueryBuilder/filters/index.ts
Normal file
2
frontend/src/container/QueryBuilder/filters/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export { AggregatorFilter } from './AggregatorFilter';
|
||||
export { OperatorsSelect } from './OperatorsSelect';
|
@ -29,7 +29,6 @@ function SideNav(): JSX.Element {
|
||||
const [collapsed, setCollapsed] = useState<boolean>(
|
||||
getLocalStorageKey(IS_SIDEBAR_COLLAPSED) === 'true',
|
||||
);
|
||||
const { search } = useLocation();
|
||||
const { currentVersion, latestVersion, isCurrentVersionError } = useSelector<
|
||||
AppState,
|
||||
AppReducer
|
||||
@ -48,15 +47,11 @@ function SideNav(): JSX.Element {
|
||||
|
||||
const onClickHandler = useCallback(
|
||||
(to: string) => {
|
||||
const queryParams = new URLSearchParams(search);
|
||||
|
||||
const url = queryParams.toString();
|
||||
|
||||
if (pathname !== to) {
|
||||
history.push(`${to}?${url}`);
|
||||
history.push(`${to}`);
|
||||
}
|
||||
},
|
||||
[pathname, search],
|
||||
[pathname],
|
||||
);
|
||||
|
||||
const onClickSlackHandler = (): void => {
|
||||
|
@ -178,7 +178,7 @@ function Duration(): JSX.Element {
|
||||
if (value === undefined) {
|
||||
return <div />;
|
||||
}
|
||||
return <div>{`${getMs(value?.toString())}ms`}</div>;
|
||||
return <div>{`${value?.toString()}ms`}</div>;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
|
@ -1,8 +1,6 @@
|
||||
import {
|
||||
QueryBuilderContext,
|
||||
QueryBuilderContextType,
|
||||
} from 'providers/QueryBuilder';
|
||||
import { QueryBuilderContext } from 'providers/QueryBuilder';
|
||||
import { useContext } from 'react';
|
||||
import { QueryBuilderContextType } from 'types/common/queryBuilder';
|
||||
|
||||
export function useQueryBuilder(): QueryBuilderContextType {
|
||||
return useContext(QueryBuilderContext);
|
||||
|
16
frontend/src/lib/query/transformStringWithPrefix.ts
Normal file
16
frontend/src/lib/query/transformStringWithPrefix.ts
Normal file
@ -0,0 +1,16 @@
|
||||
type TransformStringWithPrefixParams = {
|
||||
str: string;
|
||||
prefix: string;
|
||||
condition: boolean;
|
||||
};
|
||||
|
||||
export const transformStringWithPrefix = ({
|
||||
str,
|
||||
prefix,
|
||||
condition,
|
||||
}: TransformStringWithPrefixParams): string => {
|
||||
if (prefix) {
|
||||
return condition ? `${prefix}_${str}` : str;
|
||||
}
|
||||
return str;
|
||||
};
|
@ -1,3 +1,4 @@
|
||||
// ** Helpers
|
||||
import React, {
|
||||
createContext,
|
||||
PropsWithChildren,
|
||||
@ -9,27 +10,21 @@ import React, {
|
||||
// TODO: Rename Types on the Reusable type for any source
|
||||
import {
|
||||
IBuilderFormula,
|
||||
IBuilderQuery,
|
||||
IBuilderQueryForm,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
export type QueryBuilderData = {
|
||||
queryData: IBuilderQuery[];
|
||||
queryFormulas: IBuilderFormula[];
|
||||
};
|
||||
|
||||
// ** TODO: temporary types for context, fix it during development
|
||||
export type QueryBuilderContextType = {
|
||||
queryBuilderData: QueryBuilderData;
|
||||
resetQueryBuilderData: () => void;
|
||||
handleSetQueryData: (index: number, queryData: IBuilderQuery) => void;
|
||||
handleSetFormulaData: (index: number, formulaData: IBuilderFormula) => void;
|
||||
};
|
||||
import {
|
||||
DataSource,
|
||||
MetricAggregateOperator,
|
||||
QueryBuilderContextType,
|
||||
QueryBuilderData,
|
||||
} from 'types/common/queryBuilder';
|
||||
|
||||
export const QueryBuilderContext = createContext<QueryBuilderContextType>({
|
||||
queryBuilderData: { queryData: [], queryFormulas: [] },
|
||||
resetQueryBuilderData: () => {},
|
||||
handleSetQueryData: () => {},
|
||||
handleSetFormulaData: () => {},
|
||||
initQueryBuilderData: () => {},
|
||||
});
|
||||
|
||||
const initialQueryBuilderData: QueryBuilderData = {
|
||||
@ -44,21 +39,51 @@ export function QueryBuilderProvider({
|
||||
// ** TODO: type the params which will be used for request of the data for query builder
|
||||
|
||||
const [queryBuilderData, setQueryBuilderData] = useState<QueryBuilderData>({
|
||||
queryData: [],
|
||||
// ** TODO temporary initial value for first query for testing first filters
|
||||
queryData: [
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
{
|
||||
dataSource: DataSource.METRICS,
|
||||
queryName: 'A',
|
||||
aggregateOperator: Object.values(MetricAggregateOperator)[0],
|
||||
aggregateAttribute: {
|
||||
dataType: null,
|
||||
key: '',
|
||||
isColumn: null,
|
||||
type: null,
|
||||
},
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
});
|
||||
|
||||
// ** TODO: Also in the future need to add AddFormula and AddQuery and remove them.
|
||||
|
||||
// ** Method for resetting query builder data
|
||||
const resetQueryBuilderData = useCallback((): void => {
|
||||
setQueryBuilderData(initialQueryBuilderData);
|
||||
}, []);
|
||||
|
||||
const handleSetQueryData = useCallback(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
(index: number, queryData: IBuilderQuery): void => {},
|
||||
// ** Method for setupping query builder data
|
||||
const initQueryBuilderData = useCallback(
|
||||
(queryBuilderData: QueryBuilderData): void => {
|
||||
setQueryBuilderData(queryBuilderData);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleSetQueryData = useCallback(
|
||||
(index: number, newQueryData: Partial<IBuilderQueryForm>): void => {
|
||||
const updatedQueryBuilderData = queryBuilderData.queryData.map((item, idx) =>
|
||||
index === idx ? { ...item, ...newQueryData } : item,
|
||||
);
|
||||
|
||||
setQueryBuilderData((prevState) => ({
|
||||
...prevState,
|
||||
queryData: updatedQueryBuilderData,
|
||||
}));
|
||||
},
|
||||
[queryBuilderData],
|
||||
);
|
||||
const handleSetFormulaData = useCallback(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
(index: number, formulaData: IBuilderFormula): void => {},
|
||||
@ -76,12 +101,14 @@ export function QueryBuilderProvider({
|
||||
resetQueryBuilderData,
|
||||
handleSetQueryData,
|
||||
handleSetFormulaData,
|
||||
initQueryBuilderData,
|
||||
}),
|
||||
[
|
||||
queryBuilderData,
|
||||
resetQueryBuilderData,
|
||||
handleSetQueryData,
|
||||
handleSetFormulaData,
|
||||
initQueryBuilderData,
|
||||
],
|
||||
);
|
||||
|
||||
|
@ -0,0 +1,7 @@
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
export interface IGetAggregateAttributePayload {
|
||||
aggregateOperator: string;
|
||||
dataSource: DataSource;
|
||||
searchText: string;
|
||||
}
|
8
frontend/src/types/api/queryBuilder/getAttributeKeys.ts
Normal file
8
frontend/src/types/api/queryBuilder/getAttributeKeys.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
export interface IGetAttributeKeysPayload {
|
||||
aggregateOperator: string;
|
||||
dataSource: DataSource;
|
||||
searchText: string;
|
||||
aggregateAttribute: string;
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
export interface AutocompleteData {
|
||||
dataType: 'number' | 'string' | 'boolean' | null;
|
||||
isColumn: boolean | null;
|
||||
key: string;
|
||||
type: 'tag' | 'resource' | null;
|
||||
}
|
||||
|
||||
export interface IQueryAutocompleteResponse {
|
||||
attributeKeys: AutocompleteData[];
|
||||
}
|
@ -1,22 +1,44 @@
|
||||
import { EAggregateOperator, EReduceOperator } from 'types/common/dashboard';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { IQueryBuilderTagFilters } from '../dashboard/getAll';
|
||||
|
||||
export interface IBuilderQuery {
|
||||
// TODO: add another list of operator depended from data source
|
||||
aggregateOperator: EAggregateOperator;
|
||||
disabled: boolean;
|
||||
label: string;
|
||||
legend: string;
|
||||
attribute: string;
|
||||
groupBy: string[];
|
||||
tagFilters: IQueryBuilderTagFilters;
|
||||
reduceTo?: EReduceOperator;
|
||||
}
|
||||
import { AutocompleteData } from './queryAutocompleteResponse';
|
||||
|
||||
// Type for Formula
|
||||
export interface IBuilderFormula {
|
||||
expression: string;
|
||||
disabled: boolean;
|
||||
label: string;
|
||||
legend: string;
|
||||
}
|
||||
|
||||
export interface TagFilterItem {
|
||||
key: string;
|
||||
// TODO: type it in the future
|
||||
op: string;
|
||||
value: string[];
|
||||
}
|
||||
|
||||
export interface TagFilter {
|
||||
items: TagFilterItem[];
|
||||
// TODO: type it in the future
|
||||
op: string;
|
||||
}
|
||||
|
||||
// Type for query builder
|
||||
export type IBuilderQuery = {
|
||||
queryName: string;
|
||||
dataSource: DataSource;
|
||||
aggregateOperator: string;
|
||||
aggregateAttribute: string;
|
||||
tagFilters: TagFilter[];
|
||||
groupBy: string[];
|
||||
expression: string;
|
||||
disabled: boolean;
|
||||
having?: string;
|
||||
limit?: number;
|
||||
orderBy?: string[];
|
||||
reduceTo?: string;
|
||||
};
|
||||
|
||||
export type IBuilderQueryForm = Omit<IBuilderQuery, 'aggregateAttribute'> & {
|
||||
aggregateAttribute: AutocompleteData;
|
||||
};
|
||||
|
@ -1,5 +1,100 @@
|
||||
import {
|
||||
IBuilderFormula,
|
||||
IBuilderQueryForm,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
export enum DataSource {
|
||||
METRICS = 'metrics',
|
||||
TRACES = 'traces',
|
||||
LOGS = 'logs',
|
||||
}
|
||||
|
||||
export enum MetricAggregateOperator {
|
||||
NOOP = 'noop',
|
||||
COUNT = 'count',
|
||||
COUNT_DISTINCT = 'count_distinct',
|
||||
SUM = 'sum',
|
||||
AVG = 'avg',
|
||||
MAX = 'max',
|
||||
MIN = 'min',
|
||||
P05 = 'p05',
|
||||
P10 = 'p10',
|
||||
P20 = 'p20',
|
||||
P25 = 'p25',
|
||||
P50 = 'p50',
|
||||
P75 = 'p75',
|
||||
P90 = 'p90',
|
||||
P95 = 'p95',
|
||||
P99 = 'p99',
|
||||
RATE = 'rate',
|
||||
SUM_RATE = 'sum_rate',
|
||||
AVG_RATE = 'avg_rate',
|
||||
MAX_RATE = 'max_rate',
|
||||
MIN_RATE = 'min_rate',
|
||||
RATE_SUM = 'rate_sum',
|
||||
RATE_AVG = 'rate_avg',
|
||||
RATE_MIN = 'rate_min',
|
||||
RATE_MAX = 'rate_max',
|
||||
HIST_QUANTILE_50 = 'hist_quantile_50',
|
||||
HIST_QUANTILE_75 = 'hist_quantile_75',
|
||||
HIST_QUANTILE_90 = 'hist_quantile_90',
|
||||
HIST_QUANTILE_95 = 'hist_quantile_95',
|
||||
HIST_QUANTILE_99 = 'hist_quantile_99',
|
||||
}
|
||||
|
||||
export enum TracesAggregatorOperator {
|
||||
NOOP = 'noop',
|
||||
COUNT = 'count',
|
||||
COUNT_DISTINCT = 'count_distinct',
|
||||
SUM = 'sum',
|
||||
AVG = 'avg',
|
||||
MAX = 'max',
|
||||
MIN = 'min',
|
||||
P05 = 'p05',
|
||||
P10 = 'p10',
|
||||
P20 = 'p20',
|
||||
P25 = 'p25',
|
||||
P50 = 'p50',
|
||||
P75 = 'p75',
|
||||
P90 = 'p90',
|
||||
P95 = 'p95',
|
||||
P99 = 'p99',
|
||||
RATE = 'rate',
|
||||
}
|
||||
|
||||
export enum LogsAggregatorOperator {
|
||||
NOOP = 'noop',
|
||||
COUNT = 'count',
|
||||
COUNT_DISTINCT = 'count_distinct',
|
||||
SUM = 'sum',
|
||||
AVG = 'avg',
|
||||
MAX = 'max',
|
||||
MIN = 'min',
|
||||
P05 = 'p05',
|
||||
P10 = 'p10',
|
||||
P20 = 'p20',
|
||||
P25 = 'p25',
|
||||
P50 = 'p50',
|
||||
P75 = 'p75',
|
||||
P90 = 'p90',
|
||||
P95 = 'p95',
|
||||
P99 = 'p99',
|
||||
RATE = 'rate',
|
||||
}
|
||||
|
||||
export type QueryBuilderData = {
|
||||
queryData: IBuilderQueryForm[];
|
||||
queryFormulas: IBuilderFormula[];
|
||||
};
|
||||
|
||||
// ** TODO: temporary types for context, fix it during development
|
||||
export type QueryBuilderContextType = {
|
||||
queryBuilderData: QueryBuilderData;
|
||||
resetQueryBuilderData: () => void;
|
||||
handleSetQueryData: (
|
||||
index: number,
|
||||
queryData: Partial<IBuilderQueryForm>,
|
||||
) => void;
|
||||
handleSetFormulaData: (index: number, formulaData: IBuilderFormula) => void;
|
||||
initQueryBuilderData: (queryBuilderData: QueryBuilderData) => void;
|
||||
};
|
||||
|
2
frontend/src/utils/transformToUpperCase.ts
Normal file
2
frontend/src/utils/transformToUpperCase.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export const transformToUpperCase = (str: string): string =>
|
||||
str.charAt(0).toUpperCase() + str.slice(1);
|
Loading…
x
Reference in New Issue
Block a user