From 192d3881a1dfab46c8cb4478ca3294d2ad8ccff1 Mon Sep 17 00:00:00 2001 From: Chintan Sudani <46838508+techchintan@users.noreply.github.com> Date: Tue, 28 Mar 2023 21:22:06 +0530 Subject: [PATCH 01/44] fix: commented unwanted sidebar menu option (#2513) * fix: Removed Strict mode to stop render twice * fix: commented unwanted sidebar menuoption --- frontend/src/container/SideNav/menuItems.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/frontend/src/container/SideNav/menuItems.ts b/frontend/src/container/SideNav/menuItems.ts index 5be9c9b9a1..2724878ffc 100644 --- a/frontend/src/container/SideNav/menuItems.ts +++ b/frontend/src/container/SideNav/menuItems.ts @@ -29,12 +29,12 @@ const menus: SidebarMenu[] = [ to: ROUTES.LOGS, name: 'Logs', // tags: ['Beta'], - children: [ - { - key: ROUTES.LOGS, - label: 'Search', - }, - ], + // children: [ + // { + // key: ROUTES.LOGS, + // label: 'Search', + // }, + // ], }, { Icon: DashboardFilled, From c4944370ce832e4abf1a54c7816f0129a7deb92e Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Tue, 28 Mar 2023 22:15:46 +0530 Subject: [PATCH 02/44] feat: support environment filtering in service map (#2481) --- .../app/clickhouseReader/options.go | 2 +- .../app/clickhouseReader/reader.go | 13 ++-- pkg/query-service/app/services/map.go | 63 +++++++++++++++++++ 3 files changed, 72 insertions(+), 6 deletions(-) create mode 100644 pkg/query-service/app/services/map.go diff --git a/pkg/query-service/app/clickhouseReader/options.go b/pkg/query-service/app/clickhouseReader/options.go index da5a87ecd2..56dc36ff98 100644 --- a/pkg/query-service/app/clickhouseReader/options.go +++ b/pkg/query-service/app/clickhouseReader/options.go @@ -26,7 +26,7 @@ const ( defaultDurationTable string = "distributed_durationSort" defaultUsageExplorerTable string = "distributed_usage_explorer" defaultSpansTable string = "distributed_signoz_spans" - defaultDependencyGraphTable string = "distributed_dependency_graph_minutes" + defaultDependencyGraphTable string = "distributed_dependency_graph_minutes_v2" defaultTopLevelOperationsTable string = "distributed_top_level_operations" defaultLogsDB string = "signoz_logs" defaultLogsTable string = "distributed_logs" diff --git a/pkg/query-service/app/clickhouseReader/reader.go b/pkg/query-service/app/clickhouseReader/reader.go index e3e5fc47d7..c0545ef80a 100644 --- a/pkg/query-service/app/clickhouseReader/reader.go +++ b/pkg/query-service/app/clickhouseReader/reader.go @@ -41,6 +41,7 @@ import ( promModel "github.com/prometheus/common/model" "go.signoz.io/signoz/pkg/query-service/app/logs" + "go.signoz.io/signoz/pkg/query-service/app/services" "go.signoz.io/signoz/pkg/query-service/constants" am "go.signoz.io/signoz/pkg/query-service/integrations/alertManager" "go.signoz.io/signoz/pkg/query-service/interfaces" @@ -1996,20 +1997,22 @@ func (r *ClickHouseReader) GetDependencyGraph(ctx context.Context, queryParams * sum(total_count)/ @duration AS callRate, sum(error_count)/sum(total_count) * 100 as errorRate FROM %s.%s - WHERE toUInt64(toDateTime(timestamp)) >= @start AND toUInt64(toDateTime(timestamp)) <= @end - GROUP BY - src, - dest`, + WHERE toUInt64(toDateTime(timestamp)) >= @start AND toUInt64(toDateTime(timestamp)) <= @end`, r.TraceDB, r.dependencyGraphTable, ) + tags := createTagQueryFromTagQueryParams(queryParams.Tags) + filterQuery, filterArgs := services.BuildServiceMapQuery(tags) + query += filterQuery + " GROUP BY src, dest;" + args = append(args, filterArgs...) + zap.S().Debug(query, args) err := r.db.Select(ctx, &response, query, args...) if err != nil { zap.S().Error("Error in processing sql query: ", err) - return nil, fmt.Errorf("Error in processing sql query") + return nil, fmt.Errorf("error in processing sql query %w", err) } return &response, nil diff --git a/pkg/query-service/app/services/map.go b/pkg/query-service/app/services/map.go new file mode 100644 index 0000000000..ff0a7c2b3c --- /dev/null +++ b/pkg/query-service/app/services/map.go @@ -0,0 +1,63 @@ +package services + +import ( + "fmt" + "strings" + + "github.com/ClickHouse/clickhouse-go/v2" + "go.signoz.io/signoz/pkg/query-service/model" +) + +var ( + columns = map[string]struct{}{ + "deployment_environment": {}, + "k8s_cluster_name": {}, + "k8s_namespace_name": {}, + } +) + +func BuildServiceMapQuery(tags []model.TagQuery) (string, []interface{}) { + var filterQuery string + var namedArgs []interface{} + for _, tag := range tags { + key := strings.ReplaceAll(tag.GetKey(), ".", "_") + operator := tag.GetOperator() + value := tag.GetValues() + + if _, ok := columns[key]; !ok { + continue + } + + switch operator { + case model.InOperator: + filterQuery += fmt.Sprintf(" AND %s IN @%s", key, key) + namedArgs = append(namedArgs, clickhouse.Named(key, value)) + case model.NotInOperator: + filterQuery += fmt.Sprintf(" AND %s NOT IN @%s", key, key) + namedArgs = append(namedArgs, clickhouse.Named(key, value)) + case model.EqualOperator: + filterQuery += fmt.Sprintf(" AND %s = @%s", key, key) + namedArgs = append(namedArgs, clickhouse.Named(key, value)) + case model.NotEqualOperator: + filterQuery += fmt.Sprintf(" AND %s != @%s", key, key) + namedArgs = append(namedArgs, clickhouse.Named(key, value)) + case model.ContainsOperator: + filterQuery += fmt.Sprintf(" AND %s LIKE @%s", key, key) + namedArgs = append(namedArgs, clickhouse.Named(key, fmt.Sprintf("%%%s%%", value))) + case model.NotContainsOperator: + filterQuery += fmt.Sprintf(" AND %s NOT LIKE @%s", key, key) + namedArgs = append(namedArgs, clickhouse.Named(key, fmt.Sprintf("%%%s%%", value))) + case model.StartsWithOperator: + filterQuery += fmt.Sprintf(" AND %s LIKE @%s", key, key) + namedArgs = append(namedArgs, clickhouse.Named(key, fmt.Sprintf("%s%%", value))) + case model.NotStartsWithOperator: + filterQuery += fmt.Sprintf(" AND %s NOT LIKE @%s", key, key) + namedArgs = append(namedArgs, clickhouse.Named(key, fmt.Sprintf("%s%%", value))) + case model.ExistsOperator: + filterQuery += fmt.Sprintf(" AND %s IS NOT NULL", key) + case model.NotExistsOperator: + filterQuery += fmt.Sprintf(" AND %s IS NULL", key) + } + } + return filterQuery, namedArgs +} From 12e56932eed1f0925a3903c9e0f529134a0c9438 Mon Sep 17 00:00:00 2001 From: Vishal Sharma Date: Wed, 29 Mar 2023 07:32:47 +0530 Subject: [PATCH 03/44] fix: exception detail broken APIs due to resourceTagsMap (#2514) --- pkg/query-service/app/clickhouseReader/reader.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/query-service/app/clickhouseReader/reader.go b/pkg/query-service/app/clickhouseReader/reader.go index c0545ef80a..abfa0c8dfa 100644 --- a/pkg/query-service/app/clickhouseReader/reader.go +++ b/pkg/query-service/app/clickhouseReader/reader.go @@ -2781,7 +2781,7 @@ func (r *ClickHouseReader) GetErrorFromErrorID(ctx context.Context, queryParams } var getErrorWithSpanReponse []model.ErrorWithSpan - query := fmt.Sprintf("SELECT * FROM %s.%s WHERE timestamp = @timestamp AND groupID = @groupID AND errorID = @errorID LIMIT 1", r.TraceDB, r.errorTable) + query := fmt.Sprintf("SELECT errorID, exceptionType, exceptionStacktrace, exceptionEscaped, exceptionMessage, timestamp, spanID, traceID, serviceName, groupID FROM %s.%s WHERE timestamp = @timestamp AND groupID = @groupID AND errorID = @errorID LIMIT 1", r.TraceDB, r.errorTable) args := []interface{}{clickhouse.Named("errorID", queryParams.ErrorID), clickhouse.Named("groupID", queryParams.GroupID), clickhouse.Named("timestamp", strconv.FormatInt(queryParams.Timestamp.UnixNano(), 10))} err := r.db.Select(ctx, &getErrorWithSpanReponse, query, args...) @@ -2804,7 +2804,7 @@ func (r *ClickHouseReader) GetErrorFromGroupID(ctx context.Context, queryParams var getErrorWithSpanReponse []model.ErrorWithSpan - query := fmt.Sprintf("SELECT * FROM %s.%s WHERE timestamp = @timestamp AND groupID = @groupID LIMIT 1", r.TraceDB, r.errorTable) + query := fmt.Sprintf("SELECT errorID, exceptionType, exceptionStacktrace, exceptionEscaped, exceptionMessage, timestamp, spanID, traceID, serviceName, groupID FROM %s.%s WHERE timestamp = @timestamp AND groupID = @groupID LIMIT 1", r.TraceDB, r.errorTable) args := []interface{}{clickhouse.Named("groupID", queryParams.GroupID), clickhouse.Named("timestamp", strconv.FormatInt(queryParams.Timestamp.UnixNano(), 10))} err := r.db.Select(ctx, &getErrorWithSpanReponse, query, args...) From 99ed314fc9b7fa4b8ce5406c85bdef30cfbed0ae Mon Sep 17 00:00:00 2001 From: Palash Gupta Date: Wed, 29 Mar 2023 14:45:58 +0530 Subject: [PATCH 04/44] feat: resource attribute is added in the exception (#2491) * feat: resource attribute is added in the exception * fix: build is fixed * chore: methods is updated to post * fix: build is fixed * fix: listErrors, countErrors API request body * chore: type of the function is updated * chore: convertRawQueriesToTraceSelectedTags is updated * fix: resource attribute is updated * chore: selected tags is updated * feat: key is updated --------- Co-authored-by: Vishal Sharma --- frontend/src/AppRoutes/index.tsx | 45 ++-- frontend/src/api/errors/getAll.ts | 17 +- frontend/src/api/errors/getErrorCounts.ts | 13 +- frontend/src/api/trace/getSpans.ts | 2 +- frontend/src/api/trace/getSpansAggregate.ts | 2 +- frontend/src/container/AllError/index.tsx | 9 +- .../ResourceAttributesFilter/QueryChip.tsx | 32 --- .../ResourceAttributesFilter.Machine.ts | 61 ----- ...esourceAttributesFilter.Machine.typegen.ts | 32 --- .../ResourceAttributesFilter/index.tsx | 219 ------------------ .../ResourceAttributesFilter/types.ts | 11 - .../ResourceAttributesFilter/utils.ts | 64 ----- .../MetricsApplication/Tabs/DBCall.tsx | 21 +- .../MetricsApplication/Tabs/External.tsx | 21 +- .../MetricsApplication/Tabs/Overview.tsx | 27 +-- .../MetricsApplication/TopOperationsTable.tsx | 10 +- .../container/MetricsApplication/index.tsx | 2 +- .../MetricTagKey.machine.typegen.ts | 30 +-- .../ResourceAttributesFilter.tsx | 77 ++++++ .../components/QueryChip/QueryChip.tsx | 23 ++ .../components/QueryChip/index.ts | 3 + .../components/QueryChip/types.ts | 6 + .../ResourceAttributesFilter/index.ts | 3 + .../ResourceAttributesFilter/styles.ts | 6 +- frontend/src/container/SideNav/index.tsx | 9 +- .../useResourceAttribute/ResourceProvider.tsx | 181 +++++++++++++++ .../src/hooks/useResourceAttribute/context.ts | 7 + .../src/hooks/useResourceAttribute/index.ts | 7 + .../src/hooks/useResourceAttribute/machine.ts | 61 +++++ .../useResourceAttribute/machine.typegen.ts | 37 +++ .../src/hooks/useResourceAttribute/types.ts | 32 +++ .../useResourceAttribute.tsx | 9 + .../src/hooks/useResourceAttribute/utils.ts | 161 +++++++++++++ frontend/src/lib/resourceAttributes.ts | 76 ------ frontend/src/pages/AllErrors/index.tsx | 28 ++- .../src/pages/MetricApplication/index.tsx | 14 +- frontend/src/pages/Metrics/index.tsx | 23 +- .../metrics/setResourceAttributeQueries.ts | 55 ----- frontend/src/store/reducers/metric.ts | 16 -- frontend/src/types/actions/metrics.ts | 17 +- frontend/src/types/api/errors/getAll.ts | 2 + .../src/types/api/errors/getErrorCounts.ts | 2 + frontend/src/types/reducer/metrics.ts | 3 - 43 files changed, 754 insertions(+), 722 deletions(-) delete mode 100644 frontend/src/container/MetricsApplication/ResourceAttributesFilter/QueryChip.tsx delete mode 100644 frontend/src/container/MetricsApplication/ResourceAttributesFilter/ResourceAttributesFilter.Machine.ts delete mode 100644 frontend/src/container/MetricsApplication/ResourceAttributesFilter/ResourceAttributesFilter.Machine.typegen.ts delete mode 100644 frontend/src/container/MetricsApplication/ResourceAttributesFilter/index.tsx delete mode 100644 frontend/src/container/MetricsApplication/ResourceAttributesFilter/types.ts delete mode 100644 frontend/src/container/MetricsApplication/ResourceAttributesFilter/utils.ts create mode 100644 frontend/src/container/ResourceAttributesFilter/ResourceAttributesFilter.tsx create mode 100644 frontend/src/container/ResourceAttributesFilter/components/QueryChip/QueryChip.tsx create mode 100644 frontend/src/container/ResourceAttributesFilter/components/QueryChip/index.ts create mode 100644 frontend/src/container/ResourceAttributesFilter/components/QueryChip/types.ts create mode 100644 frontend/src/container/ResourceAttributesFilter/index.ts rename frontend/src/container/{MetricsApplication => }/ResourceAttributesFilter/styles.ts (77%) create mode 100644 frontend/src/hooks/useResourceAttribute/ResourceProvider.tsx create mode 100644 frontend/src/hooks/useResourceAttribute/context.ts create mode 100644 frontend/src/hooks/useResourceAttribute/index.ts create mode 100644 frontend/src/hooks/useResourceAttribute/machine.ts create mode 100644 frontend/src/hooks/useResourceAttribute/machine.typegen.ts create mode 100644 frontend/src/hooks/useResourceAttribute/types.ts create mode 100644 frontend/src/hooks/useResourceAttribute/useResourceAttribute.tsx create mode 100644 frontend/src/hooks/useResourceAttribute/utils.ts delete mode 100644 frontend/src/lib/resourceAttributes.ts delete mode 100644 frontend/src/store/actions/metrics/setResourceAttributeQueries.ts diff --git a/frontend/src/AppRoutes/index.tsx b/frontend/src/AppRoutes/index.tsx index 832e557e49..edfe843882 100644 --- a/frontend/src/AppRoutes/index.tsx +++ b/frontend/src/AppRoutes/index.tsx @@ -4,6 +4,7 @@ import Spinner from 'components/Spinner'; import AppLayout from 'container/AppLayout'; import { useThemeConfig } from 'hooks/useDarkMode'; import { NotificationProvider } from 'hooks/useNotifications'; +import { ResourceProvider } from 'hooks/useResourceAttribute'; import history from 'lib/history'; import { QueryBuilderProvider } from 'providers/QueryBuilder'; import React, { Suspense } from 'react'; @@ -17,30 +18,32 @@ function App(): JSX.Element { return ( - - + + - - - }> - - {routes.map(({ path, component, exact }) => ( - - ))} + + + + }> + + {routes.map(({ path, component, exact }) => ( + + ))} - - - - - + + + + + + - - + + ); } diff --git a/frontend/src/api/errors/getAll.ts b/frontend/src/api/errors/getAll.ts index 7014e52a56..8d6793ee87 100644 --- a/frontend/src/api/errors/getAll.ts +++ b/frontend/src/api/errors/getAll.ts @@ -1,7 +1,6 @@ import axios from 'api'; import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; import { AxiosError } from 'axios'; -import createQueryParams from 'lib/createQueryParams'; import { ErrorResponse, SuccessResponse } from 'types/api'; import { PayloadProps, Props } from 'types/api/errors/getAll'; @@ -9,11 +8,17 @@ const getAll = async ( props: Props, ): Promise | ErrorResponse> => { try { - const response = await axios.get( - `/listErrors?${createQueryParams({ - ...props, - })}`, - ); + const response = await axios.post(`/listErrors`, { + start: `${props.start}`, + end: `${props.end}`, + order: props.order, + orderParam: props.orderParam, + limit: props.limit, + offset: props.offset, + exceptionType: props.exceptionType, + serviceName: props.serviceName, + tags: props.tags, + }); return { statusCode: 200, diff --git a/frontend/src/api/errors/getErrorCounts.ts b/frontend/src/api/errors/getErrorCounts.ts index 4992a6d391..977eeb226f 100644 --- a/frontend/src/api/errors/getErrorCounts.ts +++ b/frontend/src/api/errors/getErrorCounts.ts @@ -1,7 +1,6 @@ import axios from 'api'; import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; import { AxiosError } from 'axios'; -import createQueryParams from 'lib/createQueryParams'; import { ErrorResponse, SuccessResponse } from 'types/api'; import { PayloadProps, Props } from 'types/api/errors/getErrorCounts'; @@ -9,11 +8,13 @@ const getErrorCounts = async ( props: Props, ): Promise | ErrorResponse> => { try { - const response = await axios.get( - `/countErrors?${createQueryParams({ - ...props, - })}`, - ); + const response = await axios.post(`/countErrors`, { + start: `${props.start}`, + end: `${props.end}`, + exceptionType: props.exceptionType, + serviceName: props.serviceName, + tags: props.tags, + }); return { statusCode: 200, diff --git a/frontend/src/api/trace/getSpans.ts b/frontend/src/api/trace/getSpans.ts index 8b56caa46d..261b2652c6 100644 --- a/frontend/src/api/trace/getSpans.ts +++ b/frontend/src/api/trace/getSpans.ts @@ -10,7 +10,7 @@ const getSpans = async ( ): Promise | ErrorResponse> => { try { const updatedSelectedTags = props.selectedTags.map((e) => ({ - Key: e.Key, + Key: `${e.Key}.(string)`, Operator: e.Operator, StringValues: e.StringValues, NumberValues: e.NumberValues, diff --git a/frontend/src/api/trace/getSpansAggregate.ts b/frontend/src/api/trace/getSpansAggregate.ts index cfa1f7e31f..7f245605fc 100644 --- a/frontend/src/api/trace/getSpansAggregate.ts +++ b/frontend/src/api/trace/getSpansAggregate.ts @@ -28,7 +28,7 @@ const getSpanAggregate = async ( }); const updatedSelectedTags = props.selectedTags.map((e) => ({ - Key: e.Key, + Key: `${e.Key}.(string)`, Operator: e.Operator, StringValues: e.StringValues, NumberValues: e.NumberValues, diff --git a/frontend/src/container/AllError/index.tsx b/frontend/src/container/AllError/index.tsx index 64d83e70ec..c3b0580f44 100644 --- a/frontend/src/container/AllError/index.tsx +++ b/frontend/src/container/AllError/index.tsx @@ -18,6 +18,8 @@ import { ResizeTable } from 'components/ResizeTable'; import ROUTES from 'constants/routes'; import dayjs from 'dayjs'; import { useNotifications } from 'hooks/useNotifications'; +import useResourceAttribute from 'hooks/useResourceAttribute'; +import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils'; import useUrlQuery from 'hooks/useUrlQuery'; import createQueryParams from 'lib/createQueryParams'; import history from 'lib/history'; @@ -93,9 +95,11 @@ function AllErrors(): JSX.Element { ], ); + const { queries } = useResourceAttribute(); + const [{ isLoading, data }, errorCountResponse] = useQueries([ { - queryKey: ['getAllErrors', updatedPath, maxTime, minTime], + queryKey: ['getAllErrors', updatedPath, maxTime, minTime, queries], queryFn: (): Promise | ErrorResponse> => getAll({ end: maxTime, @@ -106,6 +110,7 @@ function AllErrors(): JSX.Element { orderParam: getUpdatedParams, exceptionType: getUpdatedExceptionType, serviceName: getUpdatedServiceName, + tags: convertRawQueriesToTraceSelectedTags(queries), }), enabled: !loading, }, @@ -116,6 +121,7 @@ function AllErrors(): JSX.Element { minTime, getUpdatedExceptionType, getUpdatedServiceName, + queries, ], queryFn: (): Promise> => getErrorCounts({ @@ -123,6 +129,7 @@ function AllErrors(): JSX.Element { start: minTime, exceptionType: getUpdatedExceptionType, serviceName: getUpdatedServiceName, + tags: convertRawQueriesToTraceSelectedTags(queries), }), enabled: !loading, }, diff --git a/frontend/src/container/MetricsApplication/ResourceAttributesFilter/QueryChip.tsx b/frontend/src/container/MetricsApplication/ResourceAttributesFilter/QueryChip.tsx deleted file mode 100644 index 09c5d27471..0000000000 --- a/frontend/src/container/MetricsApplication/ResourceAttributesFilter/QueryChip.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { convertMetricKeyToTrace } from 'lib/resourceAttributes'; -import React from 'react'; - -import { QueryChipContainer, QueryChipItem } from './styles'; -import { IResourceAttributeQuery } from './types'; - -interface IQueryChipProps { - queryData: IResourceAttributeQuery; - onClose: (id: string) => void; - disabled: boolean; -} - -export default function QueryChip({ - queryData, - onClose, - disabled, -}: IQueryChipProps): JSX.Element { - return ( - - {convertMetricKeyToTrace(queryData.tagKey)} - {queryData.operator} - { - if (!disabled) onClose(queryData.id); - }} - > - {queryData.tagValue.join(', ')} - - - ); -} diff --git a/frontend/src/container/MetricsApplication/ResourceAttributesFilter/ResourceAttributesFilter.Machine.ts b/frontend/src/container/MetricsApplication/ResourceAttributesFilter/ResourceAttributesFilter.Machine.ts deleted file mode 100644 index 3b9078f76b..0000000000 --- a/frontend/src/container/MetricsApplication/ResourceAttributesFilter/ResourceAttributesFilter.Machine.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { createMachine } from 'xstate'; - -export const ResourceAttributesFilterMachine = - /** @xstate-layout N4IgpgJg5mDOIC5QBECGsAWAjA9qgThAAQDKYBAxhkQIIB2xAYgJYA2ALmPgHQAqqUANJgAngGIAcgFEAGr0SgADjljN2zHHQUgAHogAcAFgAM3AOz6ATAEYAzJdsA2Y4cOWAnABoQIxAFpDR2tuQ319AFYTcKdbFycAX3jvNExcAmIySmp6JjZOHn4hUTFNACFWAFd8bWVVdU1tPQQzY1MXY2tDdzNHM3dHd0NvXwR7biMTa313S0i+63DE5PRsPEJScnwqWgYiFg4uPgFhcQAlKRIpeSQQWrUNLRumx3Czbg8TR0sbS31jfUcw38fW47gBHmm4XCVms3SWIBSq3SGyyO1yBx4AHlFFxUOwcPhJLJrkoVPcGk9ENYFuF3i5YR0wtEHECEAEgiEmV8zH1DLYzHZ4Yi0utMltsrt9vluNjcfjCWVKtUbnd6o9QE1rMYBtxbGFvsZ3NrZj1WdYOfotUZLX0XEFHEKViKMpttjk9nlDrL8HiCWJzpcSbcyWrGoh3NCQj0zK53P1ph1WeFLLqnJZ2s5vmZLA6kginWsXaj3VLDoUAGqoSpgEp0cpVGohh5hhDWDy0sz8zruakzamWVm-Qyg362V5-AZOayO1KFlHitEejFHKCV6v+i5XRt1ZuU1s52zjNOOaZfdOWIY+RDZ0Hc6ZmKEXqyLPPCudit2Sz08ACSEFYNbSHI27kuquiIOEjiONwjJgrM3RWJYZisgEIJgnYPTmuEdi2OaiR5nQOAQHA2hvsiH4Sui0qFCcIGhnuLSmP0YJuJ2xjJsmKELG8XZTK0tjdHG06vgW5GupRS7St6vrKqSO4UhqVL8TBWp8o4eqdl0A5Xmy3G6gK56-B4uERDOSKiuJi6lgUAhrhUYB0buimtrEKZBDYrxaS0OZca8+ltheybOI4hivGZzrzp+VGHH+AGOQp4EIHy+ghNYnawtG4TsbYvk8QKfHGAJfQ9uF76WSW37xWBTSGJ0qXpd0vRZdEKGPqC2YeO2-zfO4+HxEAA */ - createMachine({ - tsTypes: {} as import('./ResourceAttributesFilter.Machine.typegen').Typegen0, - initial: 'Idle', - states: { - TagKey: { - on: { - NEXT: { - actions: 'onSelectOperator', - target: 'Operator', - }, - onBlur: { - actions: 'onBlurPurge', - target: 'Idle', - }, - RESET: { - target: 'Idle', - }, - }, - }, - Operator: { - on: { - NEXT: { - actions: 'onSelectTagValue', - target: 'TagValue', - }, - onBlur: { - actions: 'onBlurPurge', - target: 'Idle', - }, - RESET: { - target: 'Idle', - }, - }, - }, - TagValue: { - on: { - onBlur: { - actions: ['onValidateQuery', 'onBlurPurge'], - target: 'Idle', - }, - RESET: { - target: 'Idle', - }, - }, - }, - Idle: { - on: { - NEXT: { - actions: 'onSelectTagKey', - description: 'Select Category', - target: 'TagKey', - }, - }, - }, - }, - id: 'Dashboard Search And Filter', - }); diff --git a/frontend/src/container/MetricsApplication/ResourceAttributesFilter/ResourceAttributesFilter.Machine.typegen.ts b/frontend/src/container/MetricsApplication/ResourceAttributesFilter/ResourceAttributesFilter.Machine.typegen.ts deleted file mode 100644 index e7f7ee3de7..0000000000 --- a/frontend/src/container/MetricsApplication/ResourceAttributesFilter/ResourceAttributesFilter.Machine.typegen.ts +++ /dev/null @@ -1,32 +0,0 @@ -// This file was automatically generated. Edits will be overwritten - -export interface Typegen0 { - '@@xstate/typegen': true; - eventsCausingActions: { - onSelectOperator: 'NEXT'; - onBlurPurge: 'onBlur'; - onSelectTagValue: 'NEXT'; - onValidateQuery: 'onBlur'; - onSelectTagKey: 'NEXT'; - }; - internalEvents: { - 'xstate.init': { type: 'xstate.init' }; - }; - invokeSrcNameMap: {}; - missingImplementations: { - actions: - | 'onSelectOperator' - | 'onBlurPurge' - | 'onSelectTagValue' - | 'onValidateQuery' - | 'onSelectTagKey'; - services: never; - guards: never; - delays: never; - }; - eventsCausingServices: {}; - eventsCausingGuards: {}; - eventsCausingDelays: {}; - matchesStates: 'TagKey' | 'Operator' | 'TagValue' | 'Idle'; - tags: never; -} diff --git a/frontend/src/container/MetricsApplication/ResourceAttributesFilter/index.tsx b/frontend/src/container/MetricsApplication/ResourceAttributesFilter/index.tsx deleted file mode 100644 index b8bed255f7..0000000000 --- a/frontend/src/container/MetricsApplication/ResourceAttributesFilter/index.tsx +++ /dev/null @@ -1,219 +0,0 @@ -import { CloseCircleFilled } from '@ant-design/icons'; -import { useMachine } from '@xstate/react'; -import { Button, Select, Spin } from 'antd'; -import ROUTES from 'constants/routes'; -import history from 'lib/history'; -import { convertMetricKeyToTrace } from 'lib/resourceAttributes'; -import { map } from 'lodash-es'; -import React, { useEffect, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { ResetInitialData } from 'store/actions/metrics/resetInitialData'; -import { SetResourceAttributeQueries } from 'store/actions/metrics/setResourceAttributeQueries'; -import { AppState } from 'store/reducers'; -import MetricReducer from 'types/reducer/metrics'; -import { v4 as uuid } from 'uuid'; - -import QueryChip from './QueryChip'; -import { ResourceAttributesFilterMachine } from './ResourceAttributesFilter.Machine'; -import { QueryChipItem, SearchContainer } from './styles'; -import { IOption, IResourceAttributeQuery } from './types'; -import { createQuery, GetTagKeys, GetTagValues, OperatorSchema } from './utils'; - -function ResourceAttributesFilter(): JSX.Element | null { - const dispatch = useDispatch(); - const [disabled, setDisabled] = useState( - !(history.location.pathname === ROUTES.APPLICATION), - ); - - useEffect(() => { - const unListen = history.listen(({ pathname }) => { - setDisabled(!(pathname === ROUTES.APPLICATION)); - }); - return (): void => { - if (!history.location.pathname.startsWith(`${ROUTES.APPLICATION}/`)) { - dispatch(ResetInitialData()); - } - unListen(); - }; - }, [dispatch]); - - const { resourceAttributeQueries } = useSelector( - (state) => state.metrics, - ); - const [loading, setLoading] = useState(true); - const [selectedValues, setSelectedValues] = useState([]); - const [staging, setStaging] = useState([]); - const [queries, setQueries] = useState([]); - const [optionsData, setOptionsData] = useState<{ - mode: undefined | 'tags' | 'multiple'; - options: IOption[]; - }>({ - mode: undefined, - options: [], - }); - - const dispatchQueries = (updatedQueries: IResourceAttributeQuery[]): void => { - dispatch(SetResourceAttributeQueries(updatedQueries)); - }; - const handleLoading = (isLoading: boolean): void => { - setLoading(isLoading); - if (isLoading) { - setOptionsData({ mode: undefined, options: [] }); - } - }; - const [state, send] = useMachine(ResourceAttributesFilterMachine, { - actions: { - onSelectTagKey: () => { - handleLoading(true); - GetTagKeys() - .then((tagKeys) => setOptionsData({ options: tagKeys, mode: undefined })) - .finally(() => { - handleLoading(false); - }); - }, - onSelectOperator: () => { - setOptionsData({ options: OperatorSchema, mode: undefined }); - }, - onSelectTagValue: () => { - handleLoading(true); - - GetTagValues(staging[0]) - .then((tagValuesOptions) => - setOptionsData({ options: tagValuesOptions, mode: 'multiple' }), - ) - .finally(() => { - handleLoading(false); - }); - }, - onBlurPurge: () => { - setSelectedValues([]); - setStaging([]); - }, - onValidateQuery: (): void => { - if (staging.length < 2 || selectedValues.length === 0) { - return; - } - - const generatedQuery = createQuery([...staging, selectedValues]); - if (generatedQuery) { - dispatchQueries([...queries, generatedQuery]); - } - }, - }, - }); - - useEffect(() => { - setQueries(resourceAttributeQueries); - }, [resourceAttributeQueries]); - - const handleFocus = (): void => { - if (state.value === 'Idle') { - send('NEXT'); - } - }; - - const handleBlur = (): void => { - send('onBlur'); - }; - const handleChange = (value: never): void => { - if (!optionsData.mode) { - setStaging((prevStaging) => [...prevStaging, value]); - setSelectedValues([]); - send('NEXT'); - return; - } - - setSelectedValues([...value]); - }; - - const handleClose = (id: string): void => { - dispatchQueries(queries.filter((queryData) => queryData.id !== id)); - }; - - const handleClearAll = (): void => { - send('RESET'); - dispatchQueries([]); - setStaging([]); - setSelectedValues([]); - }; - const disabledAndEmpty = !!( - !queries.length && - !staging.length && - !selectedValues.length && - disabled - ); - const disabledOrEmpty = !!( - queries.length || - staging.length || - selectedValues.length || - disabled - ); - - if (disabledAndEmpty) { - return null; - } - - return ( - -
- {map( - queries, - (query): JSX.Element => ( - - ), - )} - {map(staging, (item, idx) => ( - - {idx === 0 ? convertMetricKeyToTrace(item) : item} - - ))} -
- {!disabled && ( - + Loading...{' '} + + ) : ( + + No resource attributes available to filter. Please refer docs to send + attributes. + + ) + } + /> + + {queries.length || staging.length || selectedQuery.length ? ( + + + + + + + + ); +}); diff --git a/frontend/src/container/QueryBuilder/components/AdditionalFiltersToggler/AdditionalFiltersToggler.interfaces.ts b/frontend/src/container/QueryBuilder/components/AdditionalFiltersToggler/AdditionalFiltersToggler.interfaces.ts new file mode 100644 index 0000000000..8f92ec9eab --- /dev/null +++ b/frontend/src/container/QueryBuilder/components/AdditionalFiltersToggler/AdditionalFiltersToggler.interfaces.ts @@ -0,0 +1,5 @@ +import { PropsWithChildren } from 'react'; + +export type AdditionalFiltersProps = PropsWithChildren & { + listOfAdditionalFilter: string[]; +}; diff --git a/frontend/src/container/QueryBuilder/components/AdditionalFiltersToggler/AdditionalFiltersToggler.styled.ts b/frontend/src/container/QueryBuilder/components/AdditionalFiltersToggler/AdditionalFiltersToggler.styled.ts new file mode 100644 index 0000000000..afa457b417 --- /dev/null +++ b/frontend/src/container/QueryBuilder/components/AdditionalFiltersToggler/AdditionalFiltersToggler.styled.ts @@ -0,0 +1,40 @@ +import { MinusSquareOutlined, PlusSquareOutlined } from '@ant-design/icons'; +import { Typography } from 'antd'; +import styled, { css } from 'styled-components'; + +const IconCss = css` + margin-right: 0.6875rem; + transition: all 0.2s ease; +`; + +export const StyledIconOpen = styled(PlusSquareOutlined)` + ${IconCss} +`; + +export const StyledIconClose = styled(MinusSquareOutlined)` + ${IconCss} +`; + +export const StyledWrapper = styled.div` + display: flex; + flex-direction: column; + width: fit-content; +`; + +export const StyledInner = styled.div` + width: 100%; + display: flex; + align-items: center; + margin-bottom: 0.875rem; + min-height: 1.375rem; + cursor: pointer; + &:hover { + ${StyledIconOpen}, ${StyledIconClose} { + opacity: 0.7; + } + } +`; + +export const StyledLink = styled(Typography.Link)` + pointer-events: none; +`; diff --git a/frontend/src/container/QueryBuilder/components/AdditionalFiltersToggler/AdditionalFiltersToggler.tsx b/frontend/src/container/QueryBuilder/components/AdditionalFiltersToggler/AdditionalFiltersToggler.tsx new file mode 100644 index 0000000000..8e3f0e1889 --- /dev/null +++ b/frontend/src/container/QueryBuilder/components/AdditionalFiltersToggler/AdditionalFiltersToggler.tsx @@ -0,0 +1,52 @@ +import { Row } from 'antd'; +import React, { Fragment, memo, ReactNode, useState } from 'react'; + +// ** Types +import { AdditionalFiltersProps } from './AdditionalFiltersToggler.interfaces'; +// ** Styles +import { + StyledIconClose, + StyledIconOpen, + StyledInner, + StyledLink, + StyledWrapper, +} from './AdditionalFiltersToggler.styled'; + +export const AdditionalFiltersToggler = memo(function AdditionalFiltersToggler({ + children, + listOfAdditionalFilter, +}: AdditionalFiltersProps): JSX.Element { + const [isOpenedFilters, setIsOpenedFilters] = useState(false); + + const handleToggleOpenFilters = (): void => { + setIsOpenedFilters((prevState) => !prevState); + }; + + const filtersTexts: ReactNode = listOfAdditionalFilter.map((str, index) => { + const isNextLast = index + 1 === listOfAdditionalFilter.length - 1; + if (index === listOfAdditionalFilter.length - 1) { + return ( + + and {str.toUpperCase()} + + ); + } + + return ( + + {str.toUpperCase()} + {isNextLast ? ' ' : ', '} + + ); + }); + + return ( + + + {isOpenedFilters ? : } + {!isOpenedFilters && Add conditions for {filtersTexts}} + + {isOpenedFilters && {children}} + + ); +}); diff --git a/frontend/src/container/QueryBuilder/components/AdditionalFiltersToggler/index.ts b/frontend/src/container/QueryBuilder/components/AdditionalFiltersToggler/index.ts new file mode 100644 index 0000000000..d08059abdf --- /dev/null +++ b/frontend/src/container/QueryBuilder/components/AdditionalFiltersToggler/index.ts @@ -0,0 +1 @@ +export { AdditionalFiltersToggler } from './AdditionalFiltersToggler'; diff --git a/frontend/src/container/QueryBuilder/components/DataSourceDropdown/DataSourceDropdown.tsx b/frontend/src/container/QueryBuilder/components/DataSourceDropdown/DataSourceDropdown.tsx index 4efe754f1a..2f5387398c 100644 --- a/frontend/src/container/QueryBuilder/components/DataSourceDropdown/DataSourceDropdown.tsx +++ b/frontend/src/container/QueryBuilder/components/DataSourceDropdown/DataSourceDropdown.tsx @@ -1,5 +1,5 @@ import { Select } from 'antd'; -import React from 'react'; +import React, { memo } from 'react'; import { DataSource } from 'types/common/queryBuilder'; import { SelectOption } from 'types/common/select'; // ** Helpers @@ -10,7 +10,9 @@ import { QueryLabelProps } from './DataSourceDropdown.interfaces'; const dataSourceMap = [DataSource.LOGS, DataSource.METRICS, DataSource.TRACES]; -export function DataSourceDropdown(props: QueryLabelProps): JSX.Element { +export const DataSourceDropdown = memo(function DataSourceDropdown( + props: QueryLabelProps, +): JSX.Element { const { onChange, value, style } = props; const dataSourceOptions: SelectOption< @@ -30,4 +32,4 @@ export function DataSourceDropdown(props: QueryLabelProps): JSX.Element { style={style} /> ); -} +}); diff --git a/frontend/src/container/QueryBuilder/components/FilterLabel/FilterLabel.styled.ts b/frontend/src/container/QueryBuilder/components/FilterLabel/FilterLabel.styled.ts index d379f6cbbe..fd66c62faf 100644 --- a/frontend/src/container/QueryBuilder/components/FilterLabel/FilterLabel.styled.ts +++ b/frontend/src/container/QueryBuilder/components/FilterLabel/FilterLabel.styled.ts @@ -5,6 +5,7 @@ export const StyledLabel = styled.div` width: fit-content; min-height: 2rem; display: inline-flex; + white-space: nowrap; align-items: center; border-radius: 0.125rem; border: 0.0625rem solid #434343; diff --git a/frontend/src/container/QueryBuilder/components/FilterLabel/FilterLabel.tsx b/frontend/src/container/QueryBuilder/components/FilterLabel/FilterLabel.tsx index aa9eb85bf1..48153f4823 100644 --- a/frontend/src/container/QueryBuilder/components/FilterLabel/FilterLabel.tsx +++ b/frontend/src/container/QueryBuilder/components/FilterLabel/FilterLabel.tsx @@ -1,10 +1,12 @@ -import React from 'react'; +import React, { memo } from 'react'; // ** Types import { FilterLabelProps } from './FilterLabel.interfaces'; // ** Styles import { StyledLabel } from './FilterLabel.styled'; -export function FilterLabel({ label }: FilterLabelProps): JSX.Element { +export const FilterLabel = memo(function FilterLabel({ + label, +}: FilterLabelProps): JSX.Element { return {label}; -} +}); diff --git a/frontend/src/container/QueryBuilder/components/ListMarker/ListMarker.styled.ts b/frontend/src/container/QueryBuilder/components/ListMarker/ListMarker.styled.ts index 6344456d0b..5c18783959 100644 --- a/frontend/src/container/QueryBuilder/components/ListMarker/ListMarker.styled.ts +++ b/frontend/src/container/QueryBuilder/components/ListMarker/ListMarker.styled.ts @@ -1,12 +1,13 @@ import { Button } from 'antd'; import styled from 'styled-components'; -export const StyledButton = styled(Button)<{ isAvailableToDisable: boolean }>` +export const StyledButton = styled(Button)<{ $isAvailableToDisable: boolean }>` min-width: 2rem; height: 2.25rem; - padding: 0.125rem; + padding: ${(props): string => + props.$isAvailableToDisable ? '0.43rem' : '0.43rem 0.68rem'}; border-radius: 0.375rem; margin-right: 0.1rem; pointer-events: ${(props): string => - props.isAvailableToDisable ? 'default' : 'none'}; + props.$isAvailableToDisable ? 'default' : 'none'}; `; diff --git a/frontend/src/container/QueryBuilder/components/ListMarker/ListMarker.tsx b/frontend/src/container/QueryBuilder/components/ListMarker/ListMarker.tsx index 8573579be7..a246047305 100644 --- a/frontend/src/container/QueryBuilder/components/ListMarker/ListMarker.tsx +++ b/frontend/src/container/QueryBuilder/components/ListMarker/ListMarker.tsx @@ -1,13 +1,13 @@ import { EyeFilled, EyeInvisibleFilled } from '@ant-design/icons'; import { ButtonProps } from 'antd'; -import React from 'react'; +import React, { memo } from 'react'; // ** Types import { ListMarkerProps } from './ListMarker.interfaces'; // ** Styles import { StyledButton } from './ListMarker.styled'; -export function ListMarker({ +export const ListMarker = memo(function ListMarker({ isDisabled, labelName, index, @@ -30,10 +30,10 @@ export function ListMarker({ icon={buttonProps.icon} onClick={buttonProps.onClick} className={className} - isAvailableToDisable={isAvailableToDisable} + $isAvailableToDisable={isAvailableToDisable} style={style} > {labelName} ); -} +}); diff --git a/frontend/src/container/QueryBuilder/components/Query/Query.interfaces.ts b/frontend/src/container/QueryBuilder/components/Query/Query.interfaces.ts index baf1bbac98..5420be12ea 100644 --- a/frontend/src/container/QueryBuilder/components/Query/Query.interfaces.ts +++ b/frontend/src/container/QueryBuilder/components/Query/Query.interfaces.ts @@ -1,3 +1,4 @@ +import { ITEMS } from 'container/NewDashboard/ComponentsSlider/menuItems'; import { IBuilderQueryForm } from 'types/api/queryBuilder/queryBuilderData'; export type QueryProps = { @@ -5,4 +6,5 @@ export type QueryProps = { isAvailableToDisable: boolean; query: IBuilderQueryForm; queryVariant: 'static' | 'dropdown'; + panelType?: ITEMS; }; diff --git a/frontend/src/container/QueryBuilder/components/Query/Query.styled.ts b/frontend/src/container/QueryBuilder/components/Query/Query.styled.ts new file mode 100644 index 0000000000..cd271b2187 --- /dev/null +++ b/frontend/src/container/QueryBuilder/components/Query/Query.styled.ts @@ -0,0 +1,22 @@ +import { CloseCircleOutlined } from '@ant-design/icons'; +import { Row } from 'antd'; +import styled from 'styled-components'; + +export const StyledDeleteEntity = styled(CloseCircleOutlined)` + position: absolute; + top: 0.9375rem; + right: 0.9375rem; + z-index: 1; + cursor: pointer; + opacity: 0.45; + width: 1.3125rem; + height: 1.3125rem; + svg { + width: 100%; + height: 100%; + } +`; + +export const StyledRow = styled(Row)` + padding-right: 3rem; +`; diff --git a/frontend/src/container/QueryBuilder/components/Query/Query.tsx b/frontend/src/container/QueryBuilder/components/Query/Query.tsx index a41eb51e5d..4af3906699 100644 --- a/frontend/src/container/QueryBuilder/components/Query/Query.tsx +++ b/frontend/src/container/QueryBuilder/components/Query/Query.tsx @@ -1,7 +1,14 @@ -/* eslint-disable react/jsx-props-no-spreading */ -import { Col, Row } from 'antd'; +import { Col, Input, Row } from 'antd'; +// ** Constants +import { + initialAggregateAttribute, + mapOfFilters, + mapOfOperators, +} from 'constants/queryBuilder'; +import { initialQueryBuilderFormValues } from 'constants/queryBuilder'; // ** Components import { + AdditionalFiltersToggler, DataSourceDropdown, FilterLabel, ListMarker, @@ -10,64 +17,178 @@ import { AggregatorFilter, GroupByFilter, OperatorsSelect, + ReduceToFilter, } from 'container/QueryBuilder/filters'; // Context import { useQueryBuilder } from 'hooks/useQueryBuilder'; +import { findDataTypeOfOperator } from 'lib/query/findDataTypeOfOperator'; // ** Hooks -import React from 'react'; +import React, { memo, useCallback, useMemo } from 'react'; import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; +import { IBuilderQueryForm } from 'types/api/queryBuilder/queryBuilderData'; 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'; +// ** Styles +import { StyledDeleteEntity, StyledRow } from './Query.styled'; -const mapOfOperators: Record = { - metrics: Object.values(MetricAggregateOperator), - logs: Object.values(LogsAggregatorOperator), - traces: Object.values(TracesAggregatorOperator), -}; - -export function Query({ +export const Query = memo(function Query({ index, isAvailableToDisable, queryVariant, query, + panelType, }: QueryProps): JSX.Element { - const { handleSetQueryData } = useQueryBuilder(); + const { + handleSetQueryData, + removeEntityByIndex, + initialDataSource, + } = useQueryBuilder(); - const currentListOfOperators = mapOfOperators[query.dataSource]; + const currentListOfOperators = useMemo( + () => mapOfOperators[query.dataSource], + [query], + ); + const listOfAdditionalFilters = useMemo(() => mapOfFilters[query.dataSource], [ + query, + ]); - const handleChangeOperator = (value: string): void => { - handleSetQueryData(index, { aggregateOperator: value }); - }; + const handleChangeOperator = useCallback( + (value: string): void => { + const aggregateDataType: BaseAutocompleteData['dataType'] = + query.aggregateAttribute.dataType; - const handleChangeDataSource = (nextSource: DataSource): void => { - handleSetQueryData(index, { dataSource: nextSource }); - }; + const newQuery: IBuilderQueryForm = { + ...query, + aggregateOperator: value, + having: [], + }; - const handleToggleDisableQuery = (): void => { - handleSetQueryData(index, { disabled: !query.disabled }); - }; + if (!aggregateDataType || query.dataSource === DataSource.METRICS) { + handleSetQueryData(index, newQuery); + return; + } - const handleChangeAggregatorAttribute = ( - value: BaseAutocompleteData, - ): void => { - handleSetQueryData(index, { aggregateAttribute: value }); - }; + switch (aggregateDataType) { + case 'string': + case 'bool': { + const typeOfValue = findDataTypeOfOperator(value); - const handleChangeGroupByKeys = (values: BaseAutocompleteData[]): void => { - handleSetQueryData(index, { groupBy: values }); - }; + handleSetQueryData(index, { + ...newQuery, + ...(typeOfValue === 'number' + ? { aggregateAttribute: initialAggregateAttribute } + : {}), + }); + + break; + } + case 'float64': + case 'int64': { + handleSetQueryData(index, newQuery); + + break; + } + + default: { + handleSetQueryData(index, newQuery); + break; + } + } + }, + [index, query, handleSetQueryData], + ); + + const handleChangeAggregatorAttribute = useCallback( + (value: BaseAutocompleteData): void => { + const newQuery: IBuilderQueryForm = { + ...query, + aggregateAttribute: value, + }; + + handleSetQueryData(index, newQuery); + }, + [index, query, handleSetQueryData], + ); + + const handleChangeDataSource = useCallback( + (nextSource: DataSource): void => { + let newQuery: IBuilderQueryForm = { + ...query, + dataSource: nextSource, + }; + + if (nextSource !== query.dataSource) { + const initCopy = { + ...(initialQueryBuilderFormValues as Partial), + }; + delete initCopy.queryName; + + newQuery = { + ...newQuery, + ...initCopy, + dataSource: initialDataSource || nextSource, + aggregateOperator: mapOfOperators[nextSource][0], + }; + } + + handleSetQueryData(index, newQuery); + }, + [index, query, initialDataSource, handleSetQueryData], + ); + + const handleToggleDisableQuery = useCallback((): void => { + const newQuery: IBuilderQueryForm = { + ...query, + disabled: !query.disabled, + }; + + handleSetQueryData(index, newQuery); + }, [index, query, handleSetQueryData]); + + const handleChangeGroupByKeys = useCallback( + (values: BaseAutocompleteData[]): void => { + const newQuery: IBuilderQueryForm = { + ...query, + groupBy: values, + }; + + handleSetQueryData(index, newQuery); + }, + [index, query, handleSetQueryData], + ); + + const handleChangeQueryLegend = useCallback( + (e: React.ChangeEvent): void => { + const newQuery: IBuilderQueryForm = { + ...query, + legend: e.target.value, + }; + handleSetQueryData(index, newQuery); + }, + [index, query, handleSetQueryData], + ); + + const handleChangeReduceTo = useCallback( + (value: string): void => { + const newQuery: IBuilderQueryForm = { + ...query, + reduceTo: value, + }; + handleSetQueryData(index, newQuery); + }, + [index, query, handleSetQueryData], + ); + + const handleDeleteQuery = useCallback(() => { + removeEntityByIndex('queryData', index); + }, [removeEntityByIndex, index]); return ( - + + @@ -92,14 +213,14 @@ export function Query({ - + - + - - + + - - + + {panelType === 'VALUE' ? ( + + ) : ( + + )} - + + + {/* TODO: Render filter by Col component */} + test additional filter + + + + + + ); -} +}); diff --git a/frontend/src/container/QueryBuilder/components/index.ts b/frontend/src/container/QueryBuilder/components/index.ts index f21c33d5c9..c009562b22 100644 --- a/frontend/src/container/QueryBuilder/components/index.ts +++ b/frontend/src/container/QueryBuilder/components/index.ts @@ -1,3 +1,4 @@ +export { AdditionalFiltersToggler } from './AdditionalFiltersToggler'; export { DataSourceDropdown } from './DataSourceDropdown'; export { FilterLabel } from './FilterLabel'; export { Formula } from './Formula'; diff --git a/frontend/src/container/QueryBuilder/filters/AggregatorFilter/AggregatorFilter.tsx b/frontend/src/container/QueryBuilder/filters/AggregatorFilter/AggregatorFilter.tsx index 6eebe2f955..174967b8f7 100644 --- a/frontend/src/container/QueryBuilder/filters/AggregatorFilter/AggregatorFilter.tsx +++ b/frontend/src/container/QueryBuilder/filters/AggregatorFilter/AggregatorFilter.tsx @@ -2,8 +2,9 @@ import { AutoComplete, Spin } from 'antd'; // ** Api import { getAggregateAttribute } from 'api/queryBuilder/getAggregateAttribute'; +import { initialAggregateAttribute } from 'constants/queryBuilder'; import { transformStringWithPrefix } from 'lib/query/transformStringWithPrefix'; -import React, { useMemo, useState } from 'react'; +import React, { memo, useMemo, useState } from 'react'; import { useQuery } from 'react-query'; import { SelectOption } from 'types/common/select'; import { transformToUpperCase } from 'utils/transformToUpperCase'; @@ -11,7 +12,7 @@ import { transformToUpperCase } from 'utils/transformToUpperCase'; // ** Types import { AgregatorFilterProps } from './AggregatorFilter.intefaces'; -export function AggregatorFilter({ +export const AggregatorFilter = memo(function AggregatorFilter({ onChange, query, }: AgregatorFilterProps): JSX.Element { @@ -50,7 +51,7 @@ export function AggregatorFilter({ const handleChangeAttribute = (value: string): void => { const currentAttributeObj = data?.payload?.attributeKeys?.find( (item) => item.key === value, - ) || { key: value, type: null, dataType: null, isColumn: null }; + ) || { ...initialAggregateAttribute, key: value }; onChange(currentAttributeObj); }; @@ -79,4 +80,4 @@ export function AggregatorFilter({ onChange={handleChangeAttribute} /> ); -} +}); diff --git a/frontend/src/container/QueryBuilder/filters/GroupByFilter/GroupByFilter.tsx b/frontend/src/container/QueryBuilder/filters/GroupByFilter/GroupByFilter.tsx index 3cda80d704..ff37393d8f 100644 --- a/frontend/src/container/QueryBuilder/filters/GroupByFilter/GroupByFilter.tsx +++ b/frontend/src/container/QueryBuilder/filters/GroupByFilter/GroupByFilter.tsx @@ -2,11 +2,11 @@ import { Select, Spin } from 'antd'; // ** Api import { getAggregateKeys } from 'api/queryBuilder/getAttributeKeys'; // ** Constants -import { QueryBuilderKeys } from 'constants/useQueryKeys'; +import { QueryBuilderKeys } from 'constants/queryBuilder'; // ** Components // ** Helpers import { transformStringWithPrefix } from 'lib/query/transformStringWithPrefix'; -import React, { useState } from 'react'; +import React, { memo, useState } from 'react'; import { useQuery } from 'react-query'; import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; import { SelectOption } from 'types/common/select'; @@ -17,7 +17,7 @@ import { GroupByFilterValue, } from './GroupByFilter.interfaces'; -export function GroupByFilter({ +export const GroupByFilter = memo(function GroupByFilter({ query, onChange, }: GroupByFilterProps): JSX.Element { @@ -97,4 +97,4 @@ export function GroupByFilter({ onChange={handleChange} /> ); -} +}); diff --git a/frontend/src/container/QueryBuilder/filters/OperatorsSelect/OperatorsSelect.tsx b/frontend/src/container/QueryBuilder/filters/OperatorsSelect/OperatorsSelect.tsx index e0bf89e6d2..9ff0a4f425 100644 --- a/frontend/src/container/QueryBuilder/filters/OperatorsSelect/OperatorsSelect.tsx +++ b/frontend/src/container/QueryBuilder/filters/OperatorsSelect/OperatorsSelect.tsx @@ -1,5 +1,5 @@ import { Select } from 'antd'; -import React from 'react'; +import React, { memo } from 'react'; // ** Types import { SelectOption } from 'types/common/select'; // ** Helpers @@ -7,7 +7,7 @@ import { transformToUpperCase } from 'utils/transformToUpperCase'; import { OperatorsSelectProps } from './OperatorsSelect.interfaces'; -export function OperatorsSelect({ +export const OperatorsSelect = memo(function OperatorsSelect({ operators, value, onChange, @@ -30,4 +30,4 @@ export function OperatorsSelect({ {...props} /> ); -} +}); diff --git a/frontend/src/container/QueryBuilder/filters/ReduceToFilter/ReduceToFilter.interfaces.ts b/frontend/src/container/QueryBuilder/filters/ReduceToFilter/ReduceToFilter.interfaces.ts new file mode 100644 index 0000000000..8f9ee7b05c --- /dev/null +++ b/frontend/src/container/QueryBuilder/filters/ReduceToFilter/ReduceToFilter.interfaces.ts @@ -0,0 +1,7 @@ +import { SelectProps } from 'antd'; +import { IBuilderQueryForm } from 'types/api/queryBuilder/queryBuilderData'; + +export type ReduceToFilterProps = Omit & { + query: IBuilderQueryForm; + onChange: (value: string) => void; +}; diff --git a/frontend/src/container/QueryBuilder/filters/ReduceToFilter/ReduceToFilter.tsx b/frontend/src/container/QueryBuilder/filters/ReduceToFilter/ReduceToFilter.tsx new file mode 100644 index 0000000000..1e99bd24c0 --- /dev/null +++ b/frontend/src/container/QueryBuilder/filters/ReduceToFilter/ReduceToFilter.tsx @@ -0,0 +1,26 @@ +import { Select } from 'antd'; +import React, { memo } from 'react'; +// ** Types +import { EReduceOperator } from 'types/common/queryBuilder'; +import { SelectOption } from 'types/common/select'; + +import { ReduceToFilterProps } from './ReduceToFilter.interfaces'; + +export const ReduceToFilter = memo(function ReduceToFilter({ + query, + onChange, +}: ReduceToFilterProps): JSX.Element { + const options: SelectOption[] = Object.values( + EReduceOperator, + ).map((str) => ({ label: str, value: str })); + + return ( + : null} + > + {options?.map((option) => ( + + {option.value} + {option.selected && } + + ))} + + ); +} + +interface QueryBuilderSearchProps { + query: IBuilderQueryForm; +} + +export interface CustomTagProps { + label: React.ReactNode; + value: string; + disabled: boolean; + onClose: (event?: React.MouseEvent) => void; + closable: boolean; +} + +export default QueryBuilderSearch; diff --git a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/style.ts b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/style.ts new file mode 100644 index 0000000000..064392b773 --- /dev/null +++ b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/style.ts @@ -0,0 +1,11 @@ +import { CheckOutlined } from '@ant-design/icons'; +import { Typography } from 'antd'; +import styled from 'styled-components'; + +export const TypographyText = styled(Typography.Text)<{ isInNin: boolean }>` + width: ${({ isInNin }): string => (isInNin ? '10rem' : 'auto')}; +`; + +export const StyledCheckOutlined = styled(CheckOutlined)` + float: right; +`; diff --git a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/utils.ts b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/utils.ts new file mode 100644 index 0000000000..7ff23055eb --- /dev/null +++ b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/utils.ts @@ -0,0 +1,9 @@ +import { OPERATORS } from 'constants/queryBuilder'; + +export function isInNotInOperator(value: string): boolean { + return value?.includes(OPERATORS.IN || OPERATORS.NIN); +} + +export function isExistsNotExistsOperator(value: string): boolean { + return value?.includes(OPERATORS.EXISTS || OPERATORS.NOT_EXISTS); +} diff --git a/frontend/src/container/QueryBuilder/type.ts b/frontend/src/container/QueryBuilder/type.ts new file mode 100644 index 0000000000..85cfe42e84 --- /dev/null +++ b/frontend/src/container/QueryBuilder/type.ts @@ -0,0 +1,16 @@ +import { IQueryBuilderState } from 'constants/queryBuilder'; + +export interface InitialStateI { + search: string; +} + +export interface ContextValueI { + values: InitialStateI; + onChangeHandler: (type: IQueryBuilderState) => (value: string) => void; + onSubmitHandler: VoidFunction; +} + +export type Option = { + value: string; + selected?: boolean; +}; diff --git a/frontend/src/hooks/queryBuilder/useAutoComplete.ts b/frontend/src/hooks/queryBuilder/useAutoComplete.ts new file mode 100644 index 0000000000..49ce84e109 --- /dev/null +++ b/frontend/src/hooks/queryBuilder/useAutoComplete.ts @@ -0,0 +1,131 @@ +import { isExistsNotExistsOperator } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils'; +import { Option } from 'container/QueryBuilder/type'; +import { useCallback, useState } from 'react'; +import { IBuilderQueryForm } from 'types/api/queryBuilder/queryBuilderData'; +import { checkStringEndsWithSpace } from 'utils/checkStringEndsWithSpace'; + +import { useFetchKeysAndValues } from './useFetchKeysAndValues'; +import { useOptions } from './useOptions'; +import { useSetCurrentKeyAndOperator } from './useSetCurrentKeyAndOperator'; +import { useTag } from './useTag'; +import { useTagValidation } from './useTagValidation'; + +interface IAutoComplete { + handleSearch: (value: string) => void; + handleClearTag: (value: string) => void; + handleSelect: (value: string) => void; + handleKeyDown: (event: React.KeyboardEvent) => void; + options: Option[]; + tags: string[]; + searchValue: string; + isMulti: boolean; + isFetching: boolean; +} + +export const useAutoComplete = (query: IBuilderQueryForm): IAutoComplete => { + const [searchValue, setSearchValue] = useState(''); + + const handleSearch = (value: string): void => setSearchValue(value); + + const { keys, results, isFetching } = useFetchKeysAndValues( + searchValue, + query, + ); + + const [key, operator, result] = useSetCurrentKeyAndOperator(searchValue, keys); + + const { + isValidTag, + isExist, + isValidOperator, + isMulti, + isFreeText, + } = useTagValidation(searchValue, operator, result); + + const { handleAddTag, handleClearTag, tags } = useTag( + key, + isValidTag, + isFreeText, + handleSearch, + ); + + const handleSelect = useCallback( + (value: string): void => { + if (isMulti) { + setSearchValue((prev: string) => { + if (prev.includes(value)) { + return prev.replace(` ${value}`, ''); + } + return checkStringEndsWithSpace(prev) + ? `${prev} ${value}` + : `${prev}, ${value}`; + }); + } + if (!isMulti && isValidTag && !isExistsNotExistsOperator(value)) { + handleAddTag(value); + } + if (!isMulti && isExistsNotExistsOperator(value)) { + handleAddTag(value); + } + }, + [handleAddTag, isMulti, isValidTag], + ); + + const handleKeyDown = useCallback( + (event: React.KeyboardEvent): void => { + if ( + event.key === ' ' && + (searchValue.endsWith(' ') || searchValue.length === 0) + ) { + event.preventDefault(); + } + + if (event.key === 'Enter' && searchValue && isValidTag) { + if (isMulti || isFreeText) { + event.stopPropagation(); + } + event.preventDefault(); + handleAddTag(searchValue); + } + + if (event.key === 'Backspace' && !searchValue) { + event.stopPropagation(); + const last = tags[tags.length - 1]; + handleClearTag(last); + } + }, + [ + handleAddTag, + handleClearTag, + isFreeText, + isMulti, + isValidTag, + searchValue, + tags, + ], + ); + + const options = useOptions( + key, + keys, + operator, + searchValue, + isMulti, + isValidOperator, + isExist, + results, + result, + ); + + return { + handleSearch, + handleClearTag, + handleSelect, + handleKeyDown, + options, + tags, + searchValue, + isMulti, + isFetching, + }; +}; diff --git a/frontend/src/hooks/queryBuilder/useFetchKeysAndValues.ts b/frontend/src/hooks/queryBuilder/useFetchKeysAndValues.ts new file mode 100644 index 0000000000..3fd04181dc --- /dev/null +++ b/frontend/src/hooks/queryBuilder/useFetchKeysAndValues.ts @@ -0,0 +1,106 @@ +import { + AttributeKeyOptions, + getAttributesKeys, + getAttributesValues, +} from 'api/queryBuilder/getAttributesKeysValues'; +import { useEffect, useRef, useState } from 'react'; +import { useQuery } from 'react-query'; +import { useDebounce } from 'react-use'; +import { IBuilderQueryForm } from 'types/api/queryBuilder/queryBuilderData'; +import { separateSearchValue } from 'utils/separateSearchValue'; + +type UseFetchKeysAndValuesReturnValues = { + keys: AttributeKeyOptions[]; + results: string[]; + isFetching: boolean; +}; + +/** + * Custom hook to fetch attribute keys and values from an API + * @param searchValue - the search query value + * @param query - an object containing data for the query + * @returns an object containing the fetched attribute keys, results, and the status of the fetch + */ + +export const useFetchKeysAndValues = ( + searchValue: string, + query: IBuilderQueryForm, +): UseFetchKeysAndValuesReturnValues => { + const [keys, setKeys] = useState([]); + const [results, setResults] = useState([]); + const { data, isFetching, status } = useQuery( + [ + 'GET_ATTRIBUTE_KEY', + searchValue, + query.dataSource, + query.aggregateOperator, + query.aggregateAttribute.key, + ], + async () => + getAttributesKeys({ + searchText: searchValue, + dataSource: query.dataSource, + aggregateOperator: query.aggregateOperator, + aggregateAttribute: query.aggregateAttribute.key, + }), + { enabled: !!query.aggregateOperator && !!query.dataSource }, + ); + + /** + * Fetches the options to be displayed based on the selected value + * @param value - the selected value + * @param query - an object containing data for the query + */ + const handleFetchOption = async ( + value: string, + query: IBuilderQueryForm, + ): Promise => { + if (value) { + // separate the search value into the attribute key and the operator + const [tKey, operator] = separateSearchValue(value); + setResults([]); + if (tKey && operator) { + const { payload } = await getAttributesValues({ + searchText: searchValue, + dataSource: query.dataSource, + aggregateOperator: query.aggregateOperator, + aggregateAttribute: query.aggregateAttribute.key, + attributeKey: tKey, + }); + if (payload) { + const values = Object.values(payload).find((el) => !!el); + if (values) { + setResults(values); + } else { + setResults([]); + } + } + } + } + }; + + // creates a ref to the fetch function so that it doesn't change on every render + const clearFetcher = useRef(handleFetchOption).current; + + // debounces the fetch function to avoid excessive API calls + useDebounce(() => clearFetcher(searchValue, query), 500, [ + clearFetcher, + searchValue, + query, + ]); + + // update the fetched keys when the fetch status changes + useEffect(() => { + if (status === 'success' && data?.payload) { + setKeys(data?.payload); + } else { + setKeys([]); + } + }, [data?.payload, status]); + + return { + keys, + results, + isFetching, + }; +}; diff --git a/frontend/src/hooks/queryBuilder/useIsValidTag.ts b/frontend/src/hooks/queryBuilder/useIsValidTag.ts new file mode 100644 index 0000000000..216971b0ac --- /dev/null +++ b/frontend/src/hooks/queryBuilder/useIsValidTag.ts @@ -0,0 +1,22 @@ +import { useMemo } from 'react'; + +import { OperatorType } from './useOperatorType'; + +const validationMapper: Record< + OperatorType, + (resultLength: number) => boolean +> = { + SINGLE_VALUE: (resultLength: number) => resultLength === 1, + MULTIPLY_VALUE: (resultLength: number) => resultLength >= 1, + NON_VALUE: (resultLength: number) => resultLength === 0, + NOT_VALID: () => false, +}; + +export const useIsValidTag = ( + operatorType: OperatorType, + resultLength: number, +): boolean => + useMemo(() => validationMapper[operatorType]?.(resultLength), [ + operatorType, + resultLength, + ]); diff --git a/frontend/src/hooks/queryBuilder/useOperatorType.ts b/frontend/src/hooks/queryBuilder/useOperatorType.ts new file mode 100644 index 0000000000..a387708301 --- /dev/null +++ b/frontend/src/hooks/queryBuilder/useOperatorType.ts @@ -0,0 +1,27 @@ +import { OPERATORS } from 'constants/queryBuilder'; + +export type OperatorType = + | 'SINGLE_VALUE' + | 'MULTIPLY_VALUE' + | 'NON_VALUE' + | 'NOT_VALID'; + +const operatorTypeMapper: Record = { + [OPERATORS.IN]: 'MULTIPLY_VALUE', + [OPERATORS.NIN]: 'MULTIPLY_VALUE', + [OPERATORS.EXISTS]: 'NON_VALUE', + [OPERATORS.NOT_EXISTS]: 'NON_VALUE', + [OPERATORS.LTE]: 'SINGLE_VALUE', + [OPERATORS.LT]: 'SINGLE_VALUE', + [OPERATORS.GTE]: 'SINGLE_VALUE', + [OPERATORS.GT]: 'SINGLE_VALUE', + [OPERATORS.LIKE]: 'SINGLE_VALUE', + [OPERATORS.NLIKE]: 'SINGLE_VALUE', + [OPERATORS.CONTAINS]: 'SINGLE_VALUE', + [OPERATORS.NOT_CONTAINS]: 'SINGLE_VALUE', + [OPERATORS.EQUALS]: 'SINGLE_VALUE', + [OPERATORS.NOT_EQUALS]: 'SINGLE_VALUE', +}; + +export const useOperatorType = (operator: string): OperatorType => + operatorTypeMapper[operator] || 'NOT_VALID'; diff --git a/frontend/src/hooks/queryBuilder/useOperators.ts b/frontend/src/hooks/queryBuilder/useOperators.ts new file mode 100644 index 0000000000..b56fed1dee --- /dev/null +++ b/frontend/src/hooks/queryBuilder/useOperators.ts @@ -0,0 +1,20 @@ +import { AttributeKeyOptions } from 'api/queryBuilder/getAttributesKeysValues'; +import { QUERY_BUILDER_OPERATORS_BY_TYPES } from 'constants/queryBuilder'; +import { useMemo } from 'react'; + +type IOperators = + | typeof QUERY_BUILDER_OPERATORS_BY_TYPES.universal + | typeof QUERY_BUILDER_OPERATORS_BY_TYPES.string + | typeof QUERY_BUILDER_OPERATORS_BY_TYPES.boolean + | typeof QUERY_BUILDER_OPERATORS_BY_TYPES.number; + +export const useOperators = ( + key: string, + keys: AttributeKeyOptions[], +): IOperators => + useMemo(() => { + const currentKey = keys?.find((el) => el.key === key); + return currentKey + ? QUERY_BUILDER_OPERATORS_BY_TYPES[currentKey.dataType] + : QUERY_BUILDER_OPERATORS_BY_TYPES.universal; + }, [keys, key]); diff --git a/frontend/src/hooks/queryBuilder/useOptions.ts b/frontend/src/hooks/queryBuilder/useOptions.ts new file mode 100644 index 0000000000..3b378f0684 --- /dev/null +++ b/frontend/src/hooks/queryBuilder/useOptions.ts @@ -0,0 +1,78 @@ +import { AttributeKeyOptions } from 'api/queryBuilder/getAttributesKeysValues'; +import { Option } from 'container/QueryBuilder/type'; +import { useCallback, useEffect, useMemo, useState } from 'react'; + +import { useOperators } from './useOperators'; + +export const useOptions = ( + key: string, + keys: AttributeKeyOptions[], + operator: string, + searchValue: string, + isMulti: boolean, + isValidOperator: boolean, + isExist: boolean, + results: string[], + result: string[], +): Option[] => { + const [options, setOptions] = useState([]); + const operators = useOperators(key, keys); + + const updateOptions = useCallback(() => { + if (!key) { + setOptions( + searchValue + ? [{ value: searchValue }, ...keys.map((k) => ({ value: k.key }))] + : keys?.map((k) => ({ value: k.key })), + ); + } else if (key && !operator) { + setOptions( + operators.map((o) => ({ + value: `${key} ${o}`, + label: `${key} ${o.replace('_', ' ')}`, + })), + ); + } else if (key && operator) { + if (isMulti) { + setOptions(results.map((r) => ({ value: `${r}` }))); + } else if (isExist) { + setOptions([]); + } else if (isValidOperator) { + const hasAllResults = result.every((val) => results.includes(val)); + const values = results.map((r) => ({ + value: `${key} ${operator} ${r}`, + })); + const options = hasAllResults + ? values + : [{ value: searchValue }, ...values]; + setOptions(options); + } + } + }, [ + isExist, + isMulti, + isValidOperator, + key, + keys, + operator, + operators, + result, + results, + searchValue, + ]); + + useEffect(() => { + updateOptions(); + }, [updateOptions]); + + return useMemo( + () => + options?.map((option) => { + if (isMulti) { + return { ...option, selected: searchValue.includes(option.value) }; + } + return option; + }), + [isMulti, options, searchValue], + ); +}; diff --git a/frontend/src/hooks/queryBuilder/useSetCurrentKeyAndOperator.ts b/frontend/src/hooks/queryBuilder/useSetCurrentKeyAndOperator.ts new file mode 100644 index 0000000000..cd1e7cec2f --- /dev/null +++ b/frontend/src/hooks/queryBuilder/useSetCurrentKeyAndOperator.ts @@ -0,0 +1,32 @@ +import { AttributeKeyOptions } from 'api/queryBuilder/getAttributesKeysValues'; +import { useMemo } from 'react'; +import { getCountOfSpace } from 'utils/getCountOfSpace'; +import { separateSearchValue } from 'utils/separateSearchValue'; + +type ICurrentKeyAndOperator = [string, string, string[]]; + +export const useSetCurrentKeyAndOperator = ( + value: string, + keys: AttributeKeyOptions[], +): ICurrentKeyAndOperator => { + const [key, operator, result] = useMemo(() => { + let key = ''; + let operator = ''; + let result: string[] = []; + + if (value) { + const [tKey, tOperator, tResult] = separateSearchValue(value); + const isSuggestKey = keys?.some((el) => el.key === tKey); + + if (getCountOfSpace(value) >= 1 || isSuggestKey) { + key = tKey || ''; + operator = tOperator || ''; + result = tResult.filter((el) => el); + } + } + + return [key, operator, result]; + }, [value, keys]); + + return [key, operator, result]; +}; diff --git a/frontend/src/hooks/queryBuilder/useTag.ts b/frontend/src/hooks/queryBuilder/useTag.ts new file mode 100644 index 0000000000..38aef6fde8 --- /dev/null +++ b/frontend/src/hooks/queryBuilder/useTag.ts @@ -0,0 +1,53 @@ +import { isExistsNotExistsOperator } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils'; +import { useCallback, useState } from 'react'; + +type IUseTag = { + handleAddTag: (value: string) => void; + handleClearTag: (value: string) => void; + tags: string[]; +}; + +/** + * A custom React hook for handling tags. + * @param {string} key - A string value to identify tags. + * @param {boolean} isValidTag - A boolean value to indicate whether the tag is valid. + * @param {boolean} isFreeText - A boolean value to indicate whether free text is allowed. + * @param {function} handleSearch - A callback function to handle search. + * @returns {IUseTag} The return object containing handlers and tags. + */ +export const useTag = ( + key: string, + isValidTag: boolean, + isFreeText: boolean, + handleSearch: (value: string) => void, +): IUseTag => { + const [tags, setTags] = useState([]); + + /** + * Adds a new tag to the tag list. + * @param {string} value - The tag value to be added. + */ + const handleAddTag = useCallback( + (value: string): void => { + if ( + (value && key && isValidTag) || + isFreeText || + isExistsNotExistsOperator(value) + ) { + setTags((prevTags) => [...prevTags, value]); + handleSearch(''); + } + }, + [key, isValidTag, isFreeText, handleSearch], + ); + + /** + * Removes a tag from the tag list. + * @param {string} value - The tag value to be removed. + */ + const handleClearTag = useCallback((value: string): void => { + setTags((prevTags) => prevTags.filter((v) => v !== value)); + }, []); + + return { handleAddTag, handleClearTag, tags }; +}; diff --git a/frontend/src/hooks/queryBuilder/useTagValidation.ts b/frontend/src/hooks/queryBuilder/useTagValidation.ts new file mode 100644 index 0000000000..fd3932b93a --- /dev/null +++ b/frontend/src/hooks/queryBuilder/useTagValidation.ts @@ -0,0 +1,35 @@ +import { QUERY_BUILDER_SEARCH_VALUES } from 'constants/queryBuilder'; +import { useMemo } from 'react'; +import { checkStringEndsWithSpace } from 'utils/checkStringEndsWithSpace'; + +import { useIsValidTag } from './useIsValidTag'; +import { useOperatorType } from './useOperatorType'; + +type ITagValidation = { + isValidTag: boolean; + isExist: boolean; + isValidOperator: boolean; + isMulti: boolean; + isFreeText: boolean; +}; + +export const useTagValidation = ( + value: string, + operator: string, + result: string[], +): ITagValidation => { + const operatorType = useOperatorType(operator); + const isValidTag = useIsValidTag(operatorType, result.length); + + const { isExist, isValidOperator, isMulti, isFreeText } = useMemo(() => { + const isExist = operatorType === QUERY_BUILDER_SEARCH_VALUES.NON; + const isValidOperator = + operatorType !== QUERY_BUILDER_SEARCH_VALUES.NOT_VALID; + const isMulti = operatorType === QUERY_BUILDER_SEARCH_VALUES.MULTIPLY; + const isFreeText = operator === '' && !checkStringEndsWithSpace(value); + + return { isExist, isValidOperator, isMulti, isFreeText }; + }, [operator, operatorType, value]); + + return { isValidTag, isExist, isValidOperator, isMulti, isFreeText }; +}; diff --git a/frontend/src/providers/QueryBuilder.tsx b/frontend/src/providers/QueryBuilder.tsx index 905aa85886..90e72bdff4 100644 --- a/frontend/src/providers/QueryBuilder.tsx +++ b/frontend/src/providers/QueryBuilder.tsx @@ -1,7 +1,9 @@ // ** Helpers // ** Constants -import { initialQueryBuilderFormValues } from 'constants/queryBuilder'; -import { mapOfOperators } from 'constants/queryBuilder'; +import { + initialQueryBuilderFormValues, + mapOfOperators, +} from 'constants/queryBuilder'; import { createNewQueryName, MAX_QUERIES, diff --git a/frontend/src/utils/checkStringEndsWithSpace.ts b/frontend/src/utils/checkStringEndsWithSpace.ts new file mode 100644 index 0000000000..99769b151b --- /dev/null +++ b/frontend/src/utils/checkStringEndsWithSpace.ts @@ -0,0 +1,4 @@ +export const checkStringEndsWithSpace = (str: string): boolean => { + const endSpace = / $/; + return endSpace.test(str); +}; diff --git a/frontend/src/utils/getCountOfSpace.ts b/frontend/src/utils/getCountOfSpace.ts new file mode 100644 index 0000000000..168520afd2 --- /dev/null +++ b/frontend/src/utils/getCountOfSpace.ts @@ -0,0 +1 @@ +export const getCountOfSpace = (s: string): number => s.split(' ').length - 1; diff --git a/frontend/src/utils/getSearchParams.ts b/frontend/src/utils/getSearchParams.ts new file mode 100644 index 0000000000..7de4457f75 --- /dev/null +++ b/frontend/src/utils/getSearchParams.ts @@ -0,0 +1,9 @@ +export const getSearchParams = (newParams: { + [key: string]: string; +}): URLSearchParams => { + const params = new URLSearchParams(); + Object.entries(newParams).forEach(([key, value]) => { + params.set(key, value); + }); + return params; +}; diff --git a/frontend/src/utils/separateSearchValue.ts b/frontend/src/utils/separateSearchValue.ts new file mode 100644 index 0000000000..4499c478a8 --- /dev/null +++ b/frontend/src/utils/separateSearchValue.ts @@ -0,0 +1,12 @@ +import { OPERATORS } from 'constants/queryBuilder'; + +export const separateSearchValue = ( + value: string, +): [string, string, string[]] => { + const separatedString = value.split(' '); + const [key, operator, ...result] = separatedString; + if (operator === OPERATORS.IN || operator === OPERATORS.NIN) { + return [key, operator, result]; + } + return [key, operator, Array(result.join(' '))]; +}; From 0bc44c6fd986ecde0513a153dd37a5baf4a7b339 Mon Sep 17 00:00:00 2001 From: Chintan Sudani <46838508+techchintan@users.noreply.github.com> Date: Sat, 15 Apr 2023 18:37:51 +0530 Subject: [PATCH 31/44] feat: Limt filter for QB (#2561) --- .../AdditionalFiltersToggler.tsx | 5 +-- .../QueryBuilder/components/Query/Query.tsx | 25 ++++++++++++- .../filters/LimitFilter/LimitFilter.tsx | 37 +++++++++++++++++++ .../api/queryBuilder/queryBuilderData.ts | 2 +- 4 files changed, 63 insertions(+), 6 deletions(-) create mode 100644 frontend/src/container/QueryBuilder/filters/LimitFilter/LimitFilter.tsx diff --git a/frontend/src/container/QueryBuilder/components/AdditionalFiltersToggler/AdditionalFiltersToggler.tsx b/frontend/src/container/QueryBuilder/components/AdditionalFiltersToggler/AdditionalFiltersToggler.tsx index 8e3f0e1889..bb7cb47765 100644 --- a/frontend/src/container/QueryBuilder/components/AdditionalFiltersToggler/AdditionalFiltersToggler.tsx +++ b/frontend/src/container/QueryBuilder/components/AdditionalFiltersToggler/AdditionalFiltersToggler.tsx @@ -9,7 +9,6 @@ import { StyledIconOpen, StyledInner, StyledLink, - StyledWrapper, } from './AdditionalFiltersToggler.styled'; export const AdditionalFiltersToggler = memo(function AdditionalFiltersToggler({ @@ -41,12 +40,12 @@ export const AdditionalFiltersToggler = memo(function AdditionalFiltersToggler({ }); return ( - +
{isOpenedFilters ? : } {!isOpenedFilters && Add conditions for {filtersTexts}} {isOpenedFilters && {children}} - +
); }); diff --git a/frontend/src/container/QueryBuilder/components/Query/Query.tsx b/frontend/src/container/QueryBuilder/components/Query/Query.tsx index a5c27c6a6d..d6b1837081 100644 --- a/frontend/src/container/QueryBuilder/components/Query/Query.tsx +++ b/frontend/src/container/QueryBuilder/components/Query/Query.tsx @@ -19,6 +19,7 @@ import { OperatorsSelect, ReduceToFilter, } from 'container/QueryBuilder/filters'; +import LimitFilter from 'container/QueryBuilder/filters/LimitFilter/LimitFilter'; import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch'; import { useQueryBuilder } from 'hooks/useQueryBuilder'; import { findDataTypeOfOperator } from 'lib/query/findDataTypeOfOperator'; @@ -64,6 +65,7 @@ export const Query = memo(function Query({ ...query, aggregateOperator: value, having: [], + limit: null, }; if (!aggregateDataType || query.dataSource === DataSource.METRICS) { @@ -191,6 +193,17 @@ export const Query = memo(function Query({ [query.dataSource], ); + const handleChangeLimit = useCallback( + (value: number | null): void => { + const newQuery: IBuilderQueryForm = { + ...query, + limit: value, + }; + handleSetQueryData(index, newQuery); + }, + [index, query, handleSetQueryData], + ); + return ( @@ -253,8 +266,16 @@ export const Query = memo(function Query({ - {/* TODO: Render filter by Col component */} - test additional filter + {!isMatricsDataSource && ( + + + + + + + + + )} diff --git a/frontend/src/container/QueryBuilder/filters/LimitFilter/LimitFilter.tsx b/frontend/src/container/QueryBuilder/filters/LimitFilter/LimitFilter.tsx new file mode 100644 index 0000000000..e9f136691f --- /dev/null +++ b/frontend/src/container/QueryBuilder/filters/LimitFilter/LimitFilter.tsx @@ -0,0 +1,37 @@ +import { InputNumber } from 'antd'; +import React, { useState } from 'react'; +import { IBuilderQueryForm } from 'types/api/queryBuilder/queryBuilderData'; + +import { selectStyle } from '../QueryBuilderSearch/config'; + +function LimitFilter({ onChange, query }: LimitFilterProps): JSX.Element { + const [isData, setIsData] = useState(null); + const onChangeHandler = (value: number | null): void => { + setIsData(value); + }; + + const handleEnter = (e: { key: string }): void => { + if (e.key === 'Enter') { + onChange(isData); + } + }; + + return ( + + ); +} + +interface LimitFilterProps { + onChange: (values: number | null) => void; + query: IBuilderQueryForm; +} + +export default LimitFilter; diff --git a/frontend/src/types/api/queryBuilder/queryBuilderData.ts b/frontend/src/types/api/queryBuilder/queryBuilderData.ts index c70a1f4aeb..693d2910e5 100644 --- a/frontend/src/types/api/queryBuilder/queryBuilderData.ts +++ b/frontend/src/types/api/queryBuilder/queryBuilderData.ts @@ -40,7 +40,7 @@ export type IBuilderQuery = { expression: string; disabled: boolean; having: Having[]; - limit: number; + limit: number | null; stepInterval: number; orderBy: string[]; reduceTo: string; From 9aff047da4746e7109de58f875115b0f70ec1b8b Mon Sep 17 00:00:00 2001 From: Prashant Shahi Date: Mon, 17 Apr 2023 19:56:14 +0530 Subject: [PATCH 32/44] =?UTF-8?q?chore:=20=F0=9F=94=A7=20=20remove=20resou?= =?UTF-8?q?rce=20requests/limits=20from=20sample=20apps=20(#2288)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Prashant Shahi Co-authored-by: Vishal Sharma Co-authored-by: palashgdev --- sample-apps/hotrod/hotrod-template.yaml | 21 --------------------- sample-apps/hotrod/hotrod.yaml | 21 --------------------- 2 files changed, 42 deletions(-) diff --git a/sample-apps/hotrod/hotrod-template.yaml b/sample-apps/hotrod/hotrod-template.yaml index 6fdd6dd9ae..f2d432ca4d 100644 --- a/sample-apps/hotrod/hotrod-template.yaml +++ b/sample-apps/hotrod/hotrod-template.yaml @@ -56,13 +56,6 @@ spec: name: hotrod ports: - containerPort: 8080 - resources: - requests: - cpu: 100m - memory: 100Mi - limits: - cpu: 200m - memory: 200Mi restartPolicy: Always --- apiVersion: v1 @@ -126,13 +119,6 @@ spec: name: comm-plus-1 - containerPort: 8089 name: web-ui - resources: - requests: - cpu: 100m - memory: 100Mi - limits: - cpu: 200m - memory: 200Mi terminationMessagePath: /dev/termination-log terminationMessagePolicy: File dnsPolicy: ClusterFirst @@ -203,13 +189,6 @@ spec: volumeMounts: - mountPath: /locust name: locust-scripts - resources: - requests: - cpu: 100m - memory: 100Mi - limits: - cpu: 200m - memory: 200Mi terminationMessagePath: /dev/termination-log terminationMessagePolicy: File dnsPolicy: ClusterFirst diff --git a/sample-apps/hotrod/hotrod.yaml b/sample-apps/hotrod/hotrod.yaml index dfad0132c7..63d7fc88de 100644 --- a/sample-apps/hotrod/hotrod.yaml +++ b/sample-apps/hotrod/hotrod.yaml @@ -56,13 +56,6 @@ spec: name: hotrod ports: - containerPort: 8080 - resources: - requests: - cpu: 100m - memory: 100Mi - limits: - cpu: 200m - memory: 200Mi restartPolicy: Always --- apiVersion: v1 @@ -126,13 +119,6 @@ spec: name: comm-plus-1 - containerPort: 8089 name: web-ui - resources: - requests: - cpu: 100m - memory: 100Mi - limits: - cpu: 200m - memory: 200Mi terminationMessagePath: /dev/termination-log terminationMessagePolicy: File dnsPolicy: ClusterFirst @@ -203,13 +189,6 @@ spec: volumeMounts: - mountPath: /locust name: locust-scripts - resources: - requests: - cpu: 100m - memory: 100Mi - limits: - cpu: 200m - memory: 200Mi terminationMessagePath: /dev/termination-log terminationMessagePolicy: File dnsPolicy: ClusterFirst From 041d347d50e5e56753dea0f4a7e8c1eca8debcc5 Mon Sep 17 00:00:00 2001 From: Prashant Shahi Date: Mon, 17 Apr 2023 19:57:42 +0530 Subject: [PATCH 33/44] =?UTF-8?q?chore:=20=F0=9F=94=A7=20update=20install?= =?UTF-8?q?=20and=20troubleshooting=20guide=20url=20(#2451)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Prashant Shahi --- README.de-de.md | 4 ++-- README.md | 4 ++-- README.pt-br.md | 4 ++-- README.zh-cn.md | 4 ++-- deploy/install.sh | 6 +++--- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/README.de-de.md b/README.de-de.md index 55dd7f4c22..6587756b9b 100644 --- a/README.de-de.md +++ b/README.de-de.md @@ -85,9 +85,9 @@ Hier findest du die vollständige Liste von unterstützten Programmiersprachen - ### Bereitstellung mit Docker -Bitte folge den [hier](https://signoz.io/docs/deployment/docker/) aufgelisteten Schritten um deine Anwendung mit Docker bereitzustellen. +Bitte folge den [hier](https://signoz.io/docs/install/docker/) aufgelisteten Schritten um deine Anwendung mit Docker bereitzustellen. -Die [Anleitungen zur Fehlerbehebung](https://signoz.io/docs/deployment/troubleshooting) könnten hilfreich sein, falls du auf irgendwelche Schwierigkeiten stößt. +Die [Anleitungen zur Fehlerbehebung](https://signoz.io/docs/install/troubleshooting/) könnten hilfreich sein, falls du auf irgendwelche Schwierigkeiten stößt.

 

diff --git a/README.md b/README.md index 70779f3de5..4920ca9a26 100644 --- a/README.md +++ b/README.md @@ -130,9 +130,9 @@ You can find the complete list of languages here - https://opentelemetry.io/docs ### Deploy using Docker -Please follow the steps listed [here](https://signoz.io/docs/deployment/docker/) to install using docker +Please follow the steps listed [here](https://signoz.io/docs/install/docker/) to install using docker -The [troubleshooting instructions](https://signoz.io/docs/deployment/troubleshooting) may be helpful if you face any issues. +The [troubleshooting instructions](https://signoz.io/docs/install/troubleshooting/) may be helpful if you face any issues.

 

diff --git a/README.pt-br.md b/README.pt-br.md index ce168b4101..c817e8afb9 100644 --- a/README.pt-br.md +++ b/README.pt-br.md @@ -84,9 +84,9 @@ Você pode encontrar a lista completa de linguagens aqui - https://opentelemetry ### Implantar usando Docker -Siga as etapas listadas [aqui](https://signoz.io/docs/deployment/docker/) para instalar usando o Docker. +Siga as etapas listadas [aqui](https://signoz.io/docs/install/docker/) para instalar usando o Docker. -Esse [guia para solução de problemas](https://signoz.io/docs/deployment/troubleshooting) pode ser útil se você enfrentar quaisquer problemas. +Esse [guia para solução de problemas](https://signoz.io/docs/install/troubleshooting/) pode ser útil se você enfrentar quaisquer problemas.

 

diff --git a/README.zh-cn.md b/README.zh-cn.md index 3658eeb520..aaa89551bf 100644 --- a/README.zh-cn.md +++ b/README.zh-cn.md @@ -80,9 +80,9 @@ SigNoz帮助开发人员监控应用并排查已部署应用中的问题。SigNo ### 使用Docker部署 -请按照[这里](https://signoz.io/docs/deployment/docker/)列出的步骤使用Docker来安装 +请按照[这里](https://signoz.io/docs/install/docker/)列出的步骤使用Docker来安装 -如果你遇到任何问题,这个[排查指南](https://signoz.io/docs/deployment/troubleshooting)会对你有帮助。 +如果你遇到任何问题,这个[排查指南](https://signoz.io/docs/install/troubleshooting/)会对你有帮助。

 

diff --git a/deploy/install.sh b/deploy/install.sh index e8a14a5821..e908dd8952 100755 --- a/deploy/install.sh +++ b/deploy/install.sh @@ -125,7 +125,7 @@ check_ports_occupied() { echo "+++++++++++ ERROR ++++++++++++++++++++++" echo "SigNoz requires ports 3301 & 4317 to be open. Please shut down any other service(s) that may be running on these ports." - echo "You can run SigNoz on another port following this guide https://signoz.io/docs/deployment/docker#troubleshooting" + echo "You can run SigNoz on another port following this guide https://signoz.io/docs/install/troubleshooting/" echo "++++++++++++++++++++++++++++++++++++++++" echo "" exit 1 @@ -249,7 +249,7 @@ bye() { # Prints a friendly good bye message and exits the script. echo "" echo -e "$sudo_cmd docker-compose -f ./docker/clickhouse-setup/docker-compose.yaml ps -a" - # echo "Please read our troubleshooting guide https://signoz.io/docs/deployment/docker#troubleshooting" + echo "Please read our troubleshooting guide https://signoz.io/docs/install/troubleshooting/" echo "or reach us for support in #help channel in our Slack Community https://signoz.io/slack" echo "++++++++++++++++++++++++++++++++++++++++" @@ -500,7 +500,7 @@ if [[ $status_code -ne 200 ]]; then echo -e "$sudo_cmd docker-compose -f ./docker/clickhouse-setup/docker-compose.yaml ps -a" - echo "Please read our troubleshooting guide https://signoz.io/docs/deployment/docker/#troubleshooting-of-common-issues" + echo "Please read our troubleshooting guide https://signoz.io/docs/install/troubleshooting/" echo "or reach us on SigNoz for support https://signoz.io/slack" echo "++++++++++++++++++++++++++++++++++++++++" From 502b8b1ba8e9387ba2a1b86585370a40afd9b7ed Mon Sep 17 00:00:00 2001 From: Chintan Sudani <46838508+techchintan@users.noreply.github.com> Date: Tue, 18 Apr 2023 11:28:44 +0530 Subject: [PATCH 34/44] fix: tag filters query missing on page reload (#2580) * fix: remove frontend code owner * chore: set Cache-Control for auto complete requests (#2504) * feat(filter): add group by filter (#2538) * fix: tag filters query missing on page reload --------- Co-authored-by: Srikanth Chekuri Co-authored-by: Yevhen Shevchenko <90138953+yeshev@users.noreply.github.com> --- frontend/src/container/Trace/Search/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/container/Trace/Search/index.tsx b/frontend/src/container/Trace/Search/index.tsx index d9b4741082..099059ac80 100644 --- a/frontend/src/container/Trace/Search/index.tsx +++ b/frontend/src/container/Trace/Search/index.tsx @@ -27,7 +27,7 @@ function Search({ const dispatch = useDispatch>(); useEffect(() => { - if (traces.filterLoading) { + if (!traces.filterLoading) { const initialTags = parseTagsToQuery(traces.selectedTags); if (!initialTags.isError) { setValue(initialTags.payload); From 51f1d0fd05a057cd347b049f5d39b8b836f491a3 Mon Sep 17 00:00:00 2001 From: Chintan Sudani <46838508+techchintan@users.noreply.github.com> Date: Tue, 18 Apr 2023 11:44:28 +0530 Subject: [PATCH 35/44] feat: added aggregate every filter (#2581) * fix: remove frontend code owner * chore: set Cache-Control for auto complete requests (#2504) * feat(filter): add group by filter (#2538) * feat: added aggregate every filter --------- Co-authored-by: Srikanth Chekuri Co-authored-by: Yevhen Shevchenko <90138953+yeshev@users.noreply.github.com> --- .../QueryBuilder/components/Query/Query.tsx | 23 +++++++ .../filters/AggregateEveryFilter/index.tsx | 62 +++++++++++++++++++ .../filters/QueryBuilderSearch/index.tsx | 2 +- .../filters/QueryBuilderSearch/style.ts | 4 +- 4 files changed, 88 insertions(+), 3 deletions(-) create mode 100644 frontend/src/container/QueryBuilder/filters/AggregateEveryFilter/index.tsx diff --git a/frontend/src/container/QueryBuilder/components/Query/Query.tsx b/frontend/src/container/QueryBuilder/components/Query/Query.tsx index d6b1837081..bccae6d46b 100644 --- a/frontend/src/container/QueryBuilder/components/Query/Query.tsx +++ b/frontend/src/container/QueryBuilder/components/Query/Query.tsx @@ -19,6 +19,7 @@ import { OperatorsSelect, ReduceToFilter, } from 'container/QueryBuilder/filters'; +import AggregateEveryFilter from 'container/QueryBuilder/filters/AggregateEveryFilter'; import LimitFilter from 'container/QueryBuilder/filters/LimitFilter/LimitFilter'; import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch'; import { useQueryBuilder } from 'hooks/useQueryBuilder'; @@ -204,6 +205,17 @@ export const Query = memo(function Query({ [index, query, handleSetQueryData], ); + const handleChangeAggregateEvery = useCallback( + (value: number): void => { + const newQuery: IBuilderQueryForm = { + ...query, + stepInterval: value, + }; + handleSetQueryData(index, newQuery); + }, + [index, query, handleSetQueryData], + ); + return ( @@ -276,6 +288,17 @@ export const Query = memo(function Query({
)} + + + + + + + + diff --git a/frontend/src/container/QueryBuilder/filters/AggregateEveryFilter/index.tsx b/frontend/src/container/QueryBuilder/filters/AggregateEveryFilter/index.tsx new file mode 100644 index 0000000000..027fabeec4 --- /dev/null +++ b/frontend/src/container/QueryBuilder/filters/AggregateEveryFilter/index.tsx @@ -0,0 +1,62 @@ +import { Input } from 'antd'; +import getStep from 'lib/getStep'; +import React, { useMemo } from 'react'; +import { useSelector } from 'react-redux'; +import { AppState } from 'store/reducers'; +import { IBuilderQueryForm } from 'types/api/queryBuilder/queryBuilderData'; +import { GlobalReducer } from 'types/reducer/globalTime'; + +import { selectStyle } from '../QueryBuilderSearch/config'; + +function AggregateEveryFilter({ + onChange, + query, +}: AggregateEveryFilterProps): JSX.Element { + const { maxTime, minTime } = useSelector( + (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(); + } + }; + + return ( + onChange(Number(event.target.value))} + onKeyDown={handleKeyDown} + /> + ); +} + +interface AggregateEveryFilterProps { + onChange: (values: number) => void; + query: IBuilderQueryForm; +} + +export default AggregateEveryFilter; diff --git a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/index.tsx b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/index.tsx index d08345b47c..f31e813fc5 100644 --- a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/index.tsx +++ b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/index.tsx @@ -35,7 +35,7 @@ function QueryBuilderSearch({ query }: QueryBuilderSearchProps): JSX.Element { return ( - + {value} diff --git a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/style.ts b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/style.ts index 064392b773..5969c0f1a7 100644 --- a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/style.ts +++ b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/style.ts @@ -2,8 +2,8 @@ import { CheckOutlined } from '@ant-design/icons'; import { Typography } from 'antd'; import styled from 'styled-components'; -export const TypographyText = styled(Typography.Text)<{ isInNin: boolean }>` - width: ${({ isInNin }): string => (isInNin ? '10rem' : 'auto')}; +export const TypographyText = styled(Typography.Text)<{ $isInNin: boolean }>` + width: ${({ $isInNin }): string => ($isInNin ? '10rem' : 'auto')}; `; export const StyledCheckOutlined = styled(CheckOutlined)` From 60b78e94d84b8e0c837002448b1ca031869ddeaf Mon Sep 17 00:00:00 2001 From: Vishal Sharma Date: Tue, 18 Apr 2023 16:38:52 +0530 Subject: [PATCH 36/44] fix: add support for count aggregate attribute (#2584) --- pkg/query-service/app/clickhouseReader/reader.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pkg/query-service/app/clickhouseReader/reader.go b/pkg/query-service/app/clickhouseReader/reader.go index 1b0b771139..0dd0e1e77a 100644 --- a/pkg/query-service/app/clickhouseReader/reader.go +++ b/pkg/query-service/app/clickhouseReader/reader.go @@ -3806,7 +3806,9 @@ func (r *ClickHouseReader) GetLogAggregateAttributes(ctx context.Context, req *v where := "" switch req.Operator { - case v3.AggregateOperatorCountDistinct: + case + v3.AggregateOperatorCountDistinct, + v3.AggregateOpeatorCount: where = "tagKey ILIKE $1" case v3.AggregateOperatorRateSum, @@ -3829,7 +3831,6 @@ func (r *ClickHouseReader) GetLogAggregateAttributes(ctx context.Context, req *v v3.AggregateOperatorMax: where = "tagKey ILIKE $1 AND (tagDataType='int64' or tagDataType='float64')" case - v3.AggregateOpeatorCount, v3.AggregateOperatorNoOp: return &v3.AggregateAttributeResponse{}, nil default: @@ -4228,7 +4229,9 @@ func (r *ClickHouseReader) GetTraceAggregateAttributes(ctx context.Context, req var response v3.AggregateAttributeResponse where := "" switch req.Operator { - case v3.AggregateOperatorCountDistinct: + case + v3.AggregateOperatorCountDistinct, + v3.AggregateOpeatorCount: where = "tagKey ILIKE $1" case v3.AggregateOperatorRateSum, @@ -4251,7 +4254,6 @@ func (r *ClickHouseReader) GetTraceAggregateAttributes(ctx context.Context, req v3.AggregateOperatorMax: where = "tagKey ILIKE $1 AND dataType='float64'" case - v3.AggregateOpeatorCount, v3.AggregateOperatorNoOp: return &v3.AggregateAttributeResponse{}, nil default: From 63570c847a5fdbea5792c3eaa94c5e3cf93cc9bb Mon Sep 17 00:00:00 2001 From: Chintan Sudani <46838508+techchintan@users.noreply.github.com> Date: Tue, 18 Apr 2023 17:17:06 +0530 Subject: [PATCH 37/44] feat: added Order By filter (#2551) * fix: remove frontend code owner * chore: set Cache-Control for auto complete requests (#2504) * feat(filter): add group by filter (#2538) * feat: poc of search bar * feat: poc of search bar * feat: attribute keys auto complete api * chore: conflict resolve * chore: conflict resolve * fix: menu was not open on click * feat: re-used antoney's hooks code * fix: linting & type issue * fix: unwanted file changes * fix: conflic changes * feat: added orderby filter * chore: rebased changes * feat: poc of search bar * feat: poc of search bar * feat: attribute keys auto complete api * chore: conflict resolve * fix: menu was not open on click * feat: re-used antoney's hooks code * fix: linting & type issue * fix: uncomment qb component * fix: unwanted file changes * fix: conflic changes * fix: suggested changes * fix: reused label component * fix: unwanted changes * fix: unwanted changes * fix: recovered old changes * fix: orderby reset behaviour * chore: rebased changes * fix: resolved unwanted changes * fix: ui of filter row * fix: resolved order by filter issue on label * fix: resolved reset behaviour --------- Co-authored-by: Srikanth Chekuri Co-authored-by: Yevhen Shevchenko <90138953+yeshev@users.noreply.github.com> Co-authored-by: Palash Gupta --- frontend/src/constants/queryBuilder.ts | 2 +- .../AdditionalFiltersToggler.tsx | 3 +- .../components/Query/Query.styled.ts | 4 + .../QueryBuilder/components/Query/Query.tsx | 55 +++++-- .../filters/GroupByFilter/GroupByFilter.tsx | 17 ++- .../filters/LimitFilter/LimitFilter.tsx | 13 +- .../OperatorsSelect/OperatorsSelect.tsx | 3 +- .../OrderByFilter/OrderByFilter.interfaces.ts | 15 ++ .../filters/OrderByFilter/OrderByFilter.tsx | 141 ++++++++++++++++++ .../filters/OrderByFilter/index.ts | 1 + .../filters/OrderByFilter/utils.ts | 32 ++++ .../filters/QueryBuilderSearch/index.tsx | 36 ++++- .../src/hooks/queryBuilder/useAutoComplete.ts | 2 +- frontend/src/hooks/queryBuilder/useOptions.ts | 2 +- .../api/queryBuilder/queryBuilderData.ts | 7 +- 15 files changed, 294 insertions(+), 39 deletions(-) create mode 100644 frontend/src/container/QueryBuilder/filters/OrderByFilter/OrderByFilter.interfaces.ts create mode 100644 frontend/src/container/QueryBuilder/filters/OrderByFilter/OrderByFilter.tsx create mode 100644 frontend/src/container/QueryBuilder/filters/OrderByFilter/index.ts create mode 100644 frontend/src/container/QueryBuilder/filters/OrderByFilter/utils.ts diff --git a/frontend/src/constants/queryBuilder.ts b/frontend/src/constants/queryBuilder.ts index ba174d8137..51fe841a4c 100644 --- a/frontend/src/constants/queryBuilder.ts +++ b/frontend/src/constants/queryBuilder.ts @@ -44,7 +44,7 @@ export const initialQueryBuilderFormValues: IBuilderQueryForm = { queryName: createNewQueryName([]), aggregateOperator: Object.values(MetricAggregateOperator)[0], aggregateAttribute: initialAggregateAttribute, - tagFilters: [], + tagFilters: { items: [], op: 'AND' }, expression: '', disabled: false, having: [], diff --git a/frontend/src/container/QueryBuilder/components/AdditionalFiltersToggler/AdditionalFiltersToggler.tsx b/frontend/src/container/QueryBuilder/components/AdditionalFiltersToggler/AdditionalFiltersToggler.tsx index bb7cb47765..60f44a6007 100644 --- a/frontend/src/container/QueryBuilder/components/AdditionalFiltersToggler/AdditionalFiltersToggler.tsx +++ b/frontend/src/container/QueryBuilder/components/AdditionalFiltersToggler/AdditionalFiltersToggler.tsx @@ -1,4 +1,3 @@ -import { Row } from 'antd'; import React, { Fragment, memo, ReactNode, useState } from 'react'; // ** Types @@ -45,7 +44,7 @@ export const AdditionalFiltersToggler = memo(function AdditionalFiltersToggler({ {isOpenedFilters ? : } {!isOpenedFilters && Add conditions for {filtersTexts}} - {isOpenedFilters && {children}} + {isOpenedFilters && children} ); }); diff --git a/frontend/src/container/QueryBuilder/components/Query/Query.styled.ts b/frontend/src/container/QueryBuilder/components/Query/Query.styled.ts index cd271b2187..cab8afbc93 100644 --- a/frontend/src/container/QueryBuilder/components/Query/Query.styled.ts +++ b/frontend/src/container/QueryBuilder/components/Query/Query.styled.ts @@ -20,3 +20,7 @@ export const StyledDeleteEntity = styled(CloseCircleOutlined)` export const StyledRow = styled(Row)` padding-right: 3rem; `; + +export const StyledFilterRow = styled(Row)` + margin-bottom: 0.875rem; +`; diff --git a/frontend/src/container/QueryBuilder/components/Query/Query.tsx b/frontend/src/container/QueryBuilder/components/Query/Query.tsx index bccae6d46b..0d46d71a6a 100644 --- a/frontend/src/container/QueryBuilder/components/Query/Query.tsx +++ b/frontend/src/container/QueryBuilder/components/Query/Query.tsx @@ -21,20 +21,24 @@ import { } from 'container/QueryBuilder/filters'; import AggregateEveryFilter from 'container/QueryBuilder/filters/AggregateEveryFilter'; import LimitFilter from 'container/QueryBuilder/filters/LimitFilter/LimitFilter'; +import { OrderByFilter } from 'container/QueryBuilder/filters/OrderByFilter'; import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch'; import { useQueryBuilder } from 'hooks/useQueryBuilder'; import { findDataTypeOfOperator } from 'lib/query/findDataTypeOfOperator'; // ** Hooks import React, { memo, useCallback, useMemo } from 'react'; import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; -import { IBuilderQueryForm } from 'types/api/queryBuilder/queryBuilderData'; +import { + IBuilderQueryForm, + TagFilter, +} from 'types/api/queryBuilder/queryBuilderData'; import { DataSource } from 'types/common/queryBuilder'; import { transformToUpperCase } from 'utils/transformToUpperCase'; // ** Types import { QueryProps } from './Query.interfaces'; // ** Styles -import { StyledDeleteEntity, StyledRow } from './Query.styled'; +import { StyledDeleteEntity, StyledFilterRow, StyledRow } from './Query.styled'; export const Query = memo(function Query({ index, @@ -66,10 +70,13 @@ export const Query = memo(function Query({ ...query, aggregateOperator: value, having: [], + groupBy: [], + orderBy: [], limit: null, + tagFilters: { items: [], op: 'AND' }, }; - if (!aggregateDataType || query.dataSource === DataSource.METRICS) { + if (!aggregateDataType) { handleSetQueryData(index, newQuery); return; } @@ -194,6 +201,17 @@ export const Query = memo(function Query({ [query.dataSource], ); + const handleChangeOrderByKeys = useCallback( + (values: BaseAutocompleteData[]): void => { + const newQuery: IBuilderQueryForm = { + ...query, + orderBy: values, + }; + handleSetQueryData(index, newQuery); + }, + [handleSetQueryData, index, query], + ); + const handleChangeLimit = useCallback( (value: number | null): void => { const newQuery: IBuilderQueryForm = { @@ -216,6 +234,17 @@ export const Query = memo(function Query({ [index, query, handleSetQueryData], ); + const handleChangeTagFilters = useCallback( + (value: TagFilter): void => { + const newQuery: IBuilderQueryForm = { + ...query, + tagFilters: value, + }; + handleSetQueryData(index, newQuery); + }, + [index, query, handleSetQueryData], + ); + return ( @@ -241,7 +270,7 @@ export const Query = memo(function Query({ {isMatricsDataSource && } - + @@ -279,20 +308,26 @@ export const Query = memo(function Query({ {!isMatricsDataSource && ( - - + + + + + + + + - + - + )} - + - + + !query.aggregateAttribute.key || + query.aggregateOperator === MetricAggregateOperator.NOOP, + [query.aggregateAttribute.key, query.aggregateOperator], + ); + return ( : null} + onChange={handleChange} + /> + ); +} diff --git a/frontend/src/container/QueryBuilder/filters/OrderByFilter/index.ts b/frontend/src/container/QueryBuilder/filters/OrderByFilter/index.ts new file mode 100644 index 0000000000..ece36ae2f6 --- /dev/null +++ b/frontend/src/container/QueryBuilder/filters/OrderByFilter/index.ts @@ -0,0 +1 @@ +export { OrderByFilter } from './OrderByFilter'; diff --git a/frontend/src/container/QueryBuilder/filters/OrderByFilter/utils.ts b/frontend/src/container/QueryBuilder/filters/OrderByFilter/utils.ts new file mode 100644 index 0000000000..e5b31df7bd --- /dev/null +++ b/frontend/src/container/QueryBuilder/filters/OrderByFilter/utils.ts @@ -0,0 +1,32 @@ +import { IOption } from 'hooks/useResourceAttribute/types'; +import { transformStringWithPrefix } from 'lib/query/transformStringWithPrefix'; +import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; + +import { OrderByFilterValue } from './OrderByFilter.interfaces'; + +export function mapLabelValuePairs( + arr: BaseAutocompleteData[], +): Array[] { + return arr.map((item) => { + const label = transformStringWithPrefix({ + str: item.key, + prefix: item.type || '', + condition: !item.isColumn, + }); + const value = item.key; + return [ + { + label: `${label} asc`, + value: `${value} asc`, + }, + { + label: `${label} desc`, + value: `${value} desc`, + }, + ]; + }); +} + +export function getLabelFromValue(arr: OrderByFilterValue[]): string[] { + return arr.map((value) => value.label.split(' ')[0]); +} diff --git a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/index.tsx b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/index.tsx index f31e813fc5..61c2b31930 100644 --- a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/index.tsx +++ b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/index.tsx @@ -1,13 +1,20 @@ import { Select, Spin, Tag, Tooltip } from 'antd'; import { useAutoComplete } from 'hooks/queryBuilder/useAutoComplete'; -import React from 'react'; -import { IBuilderQueryForm } from 'types/api/queryBuilder/queryBuilderData'; +import React, { useEffect, useMemo } from 'react'; +import { + IBuilderQueryForm, + TagFilter, +} from 'types/api/queryBuilder/queryBuilderData'; +import { v4 as uuid } from 'uuid'; import { selectStyle } from './config'; import { StyledCheckOutlined, TypographyText } from './style'; import { isInNotInOperator } from './utils'; -function QueryBuilderSearch({ query }: QueryBuilderSearchProps): JSX.Element { +function QueryBuilderSearch({ + query, + onChange, +}: QueryBuilderSearchProps): JSX.Element { const { handleClearTag, handleKeyDown, @@ -51,6 +58,26 @@ function QueryBuilderSearch({ query }: QueryBuilderSearchProps): JSX.Element { if (isMulti || event.key === 'Backspace') handleKeyDown(event); }; + const queryTags = useMemo(() => { + if (!query.aggregateAttribute.key) return []; + return tags; + }, [query.aggregateAttribute.key, tags]); + + useEffect(() => { + const initialTagFilters: TagFilter = { items: [], op: 'AND' }; + initialTagFilters.items = tags.map((tag) => { + const [tagKey, tagOperator, ...tagValue] = tag.split(' '); + return { + id: uuid().slice(0, 8), + key: tagKey, + op: tagOperator, + value: tagValue.map((i) => i.replace(',', '')), + }; + }); + onChange(initialTagFilters); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [tags]); + return (