From ec500831ef93f2c7168d6f82490dc3545a4a8c3f Mon Sep 17 00:00:00 2001 From: Nityananda Gohain Date: Thu, 14 Dec 2023 11:22:20 +0530 Subject: [PATCH 01/32] feat: upgrade clickhouse to 23.11.1 (#4225) --- deploy/docker-swarm/clickhouse-setup/docker-compose.yaml | 2 +- deploy/docker/clickhouse-setup/docker-compose.yaml | 2 +- pkg/query-service/tests/test-deploy/docker-compose.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml b/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml index 30e5deecc1..34be947397 100644 --- a/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml +++ b/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml @@ -1,7 +1,7 @@ version: "3.9" x-clickhouse-defaults: &clickhouse-defaults - image: clickhouse/clickhouse-server:23.7.3-alpine + image: clickhouse/clickhouse-server:23.11.1-alpine tty: true deploy: restart_policy: diff --git a/deploy/docker/clickhouse-setup/docker-compose.yaml b/deploy/docker/clickhouse-setup/docker-compose.yaml index 2e54477bcf..79e4ee2519 100644 --- a/deploy/docker/clickhouse-setup/docker-compose.yaml +++ b/deploy/docker/clickhouse-setup/docker-compose.yaml @@ -3,7 +3,7 @@ version: "2.4" x-clickhouse-defaults: &clickhouse-defaults restart: on-failure # addding non LTS version due to this fix https://github.com/ClickHouse/ClickHouse/commit/32caf8716352f45c1b617274c7508c86b7d1afab - image: clickhouse/clickhouse-server:23.7.3-alpine + image: clickhouse/clickhouse-server:23.11.1-alpine tty: true depends_on: - zookeeper-1 diff --git a/pkg/query-service/tests/test-deploy/docker-compose.yaml b/pkg/query-service/tests/test-deploy/docker-compose.yaml index caed48440b..dfb36a8dbd 100644 --- a/pkg/query-service/tests/test-deploy/docker-compose.yaml +++ b/pkg/query-service/tests/test-deploy/docker-compose.yaml @@ -2,7 +2,7 @@ version: "2.4" x-clickhouse-defaults: &clickhouse-defaults restart: on-failure - image: clickhouse/clickhouse-server:23.7.3-alpine + image: clickhouse/clickhouse-server:23.11.1-alpine tty: true depends_on: - zookeeper-1 From 9c1ea0cde9b8d20899c22381dc2f7bd6f0045b63 Mon Sep 17 00:00:00 2001 From: Rajat Dabade Date: Thu, 14 Dec 2023 11:43:02 +0530 Subject: [PATCH 02/32] refactor: pop for unsaved changes (#4188) --- frontend/public/locales/en-GB/dashboard.json | 6 +- frontend/public/locales/en/dashboard.json | 6 +- frontend/src/components/ExplorerCard/utils.ts | 78 ++++++++++--------- frontend/src/container/NewWidget/index.tsx | 75 +++++++++++++++--- frontend/src/container/NewWidget/utils.ts | 15 ++++ 5 files changed, 132 insertions(+), 48 deletions(-) create mode 100644 frontend/src/container/NewWidget/utils.ts diff --git a/frontend/public/locales/en-GB/dashboard.json b/frontend/public/locales/en-GB/dashboard.json index 35e9b47b45..6179004aff 100644 --- a/frontend/public/locales/en-GB/dashboard.json +++ b/frontend/public/locales/en-GB/dashboard.json @@ -21,5 +21,9 @@ "error_while_updating_variable": "Error while updating variable", "dashboard_has_been_updated": "Dashboard has been updated", "do_you_want_to_refresh_the_dashboard": "Do you want to refresh the dashboard?", - "delete_dashboard_success": "{{name}} dashboard deleted successfully" + "delete_dashboard_success": "{{name}} dashboard deleted successfully", + "dashboard_unsave_changes": "There are unsaved changes in the Query builder, please stage and run the query or the changes will be lost. Press OK to discard.", + "dashboard_save_changes": "Your graph built with {{queryTag}} query will be saved. Press OK to confirm.", + "your_graph_build_with": "Your graph built with", + "dashboar_ok_confirm": "query will be saved. Press OK to confirm." } diff --git a/frontend/public/locales/en/dashboard.json b/frontend/public/locales/en/dashboard.json index aa2454c3a3..a74f23d228 100644 --- a/frontend/public/locales/en/dashboard.json +++ b/frontend/public/locales/en/dashboard.json @@ -24,5 +24,9 @@ "do_you_want_to_refresh_the_dashboard": "Do you want to refresh the dashboard?", "locked_dashboard_delete_tooltip_admin_author": "Dashboard is locked. Please unlock the dashboard to enable delete.", "locked_dashboard_delete_tooltip_editor": "Dashboard is locked. Please contact admin to delete the dashboard.", - "delete_dashboard_success": "{{name}} dashboard deleted successfully" + "delete_dashboard_success": "{{name}} dashboard deleted successfully", + "dashboard_unsave_changes": "There are unsaved changes in the Query builder, please stage and run the query or the changes will be lost. Press OK to discard.", + "dashboard_save_changes": "Your graph built with {{queryTag}} query will be saved. Press OK to confirm.", + "your_graph_build_with": "Your graph built with", + "dashboar_ok_confirm": "query will be saved. Press OK to confirm." } diff --git a/frontend/src/components/ExplorerCard/utils.ts b/frontend/src/components/ExplorerCard/utils.ts index 7385fbcc7f..3a2eeac95f 100644 --- a/frontend/src/components/ExplorerCard/utils.ts +++ b/frontend/src/components/ExplorerCard/utils.ts @@ -5,6 +5,7 @@ import { QueryParams } from 'constants/query'; import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder'; import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi'; import isEqual from 'lodash-es/isEqual'; +import { Query } from 'types/api/queryBuilder/queryBuilderData'; import { DeleteViewHandlerProps, @@ -35,6 +36,45 @@ export const getViewDetailsUsingViewKey: GetViewDetailsUsingViewKey = ( return undefined; }; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const omitIdFromQuery = (query: Query | null): any => ({ + ...query, + builder: { + ...query?.builder, + queryData: query?.builder.queryData.map((queryData) => { + const { id, ...rest } = queryData.aggregateAttribute; + const newAggregateAttribute = rest; + const newGroupByAttributes = queryData.groupBy.map((groupByAttribute) => { + const { id, ...rest } = groupByAttribute; + return rest; + }); + const newItems = queryData.filters.items.map((item) => { + const { id, ...newItem } = item; + if (item.key) { + const { id, ...rest } = item.key; + return { + ...newItem, + key: rest, + }; + } + return newItem; + }); + return { + ...queryData, + aggregateAttribute: newAggregateAttribute, + groupBy: newGroupByAttributes, + filters: { + ...queryData.filters, + items: newItems, + }, + limit: queryData.limit ? queryData.limit : 0, + offset: queryData.offset ? queryData.offset : 0, + pageSize: queryData.pageSize ? queryData.pageSize : 0, + }; + }), + }, +}); + export const isQueryUpdatedInView = ({ viewKey, data, @@ -48,43 +88,7 @@ export const isQueryUpdatedInView = ({ const { query, panelType } = currentViewDetails; // Omitting id from aggregateAttribute and groupBy - const updatedCurrentQuery = { - ...stagedQuery, - builder: { - ...stagedQuery?.builder, - queryData: stagedQuery?.builder.queryData.map((queryData) => { - const { id, ...rest } = queryData.aggregateAttribute; - const newAggregateAttribute = rest; - const newGroupByAttributes = queryData.groupBy.map((groupByAttribute) => { - const { id, ...rest } = groupByAttribute; - return rest; - }); - const newItems = queryData.filters.items.map((item) => { - const { id, ...newItem } = item; - if (item.key) { - const { id, ...rest } = item.key; - return { - ...newItem, - key: rest, - }; - } - return newItem; - }); - return { - ...queryData, - aggregateAttribute: newAggregateAttribute, - groupBy: newGroupByAttributes, - filters: { - ...queryData.filters, - items: newItems, - }, - limit: queryData.limit ? queryData.limit : 0, - offset: queryData.offset ? queryData.offset : 0, - pageSize: queryData.pageSize ? queryData.pageSize : 0, - }; - }), - }, - }; + const updatedCurrentQuery = omitIdFromQuery(stagedQuery); return ( panelType !== currentPanelType || diff --git a/frontend/src/container/NewWidget/index.tsx b/frontend/src/container/NewWidget/index.tsx index 18f05ecec2..e4f40ee2ac 100644 --- a/frontend/src/container/NewWidget/index.tsx +++ b/frontend/src/container/NewWidget/index.tsx @@ -1,5 +1,6 @@ -import { LockFilled } from '@ant-design/icons'; -import { Button, Modal, Tooltip, Typography } from 'antd'; +/* eslint-disable sonarjs/cognitive-complexity */ +import { LockFilled, WarningOutlined } from '@ant-design/icons'; +import { Button, Modal, Space, Tooltip, Typography } from 'antd'; import { SOMETHING_WENT_WRONG } from 'constants/api'; import { FeatureKeys } from 'constants/features'; import { PANEL_TYPES } from 'constants/queryBuilder'; @@ -18,6 +19,7 @@ import { getSelectedWidgetIndex, } from 'providers/Dashboard/util'; import { useCallback, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; import { generatePath, useLocation, useParams } from 'react-router-dom'; import { AppState } from 'store/reducers'; @@ -39,6 +41,7 @@ import { RightContainerWrapper, } from './styles'; import { NewWidgetProps } from './types'; +import { getIsQueryModified } from './utils'; function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element { const { @@ -47,7 +50,14 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element { setToScrollWidgetId, } = useDashboard(); - const { currentQuery } = useQueryBuilder(); + const { t } = useTranslation(['dashboard']); + + const { currentQuery, stagedQuery } = useQueryBuilder(); + + const isQueryModified = useMemo( + () => getIsQueryModified(currentQuery, stagedQuery), + [currentQuery, stagedQuery], + ); const { featureResponse } = useSelector( (state) => state.app, @@ -92,6 +102,12 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element { selectedWidget?.fillSpans || false, ); const [saveModal, setSaveModal] = useState(false); + const [discardModal, setDiscardModal] = useState(false); + + const closeModal = (): void => { + setSaveModal(false); + setDiscardModal(false); + }; const [graphType, setGraphType] = useState(selectedGraph); @@ -206,6 +222,14 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element { ]); const onClickDiscardHandler = useCallback(() => { + if (isQueryModified) { + setDiscardModal(true); + return; + } + history.push(generatePath(ROUTES.DASHBOARD, { dashboardId })); + }, [dashboardId, isQueryModified]); + + const discardChanges = useCallback(() => { history.push(generatePath(ROUTES.DASHBOARD, { dashboardId })); }, [dashboardId]); @@ -321,21 +345,54 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element { + + Unsaved Changes + + ) : ( + 'Save Widget' + ) + } focusTriggerAfterClose forceRender destroyOnClose closable - onCancel={(): void => setSaveModal(false)} + onCancel={closeModal} onOk={onClickSaveHandler} centered open={saveModal} width={600} > - - Your graph built with {' '} - query will be saved. Press OK to confirm. - + {!isQueryModified ? ( + + {t('your_graph_build_with')}{' '} + + {t('dashboar_ok_confirm')} + + ) : ( + {t('dashboard_unsave_changes')} + )} + + + + Unsaved Changes + + } + focusTriggerAfterClose + forceRender + destroyOnClose + closable + onCancel={closeModal} + onOk={discardChanges} + centered + open={discardModal} + width={600} + > + {t('dashboard_unsave_changes')} ); diff --git a/frontend/src/container/NewWidget/utils.ts b/frontend/src/container/NewWidget/utils.ts new file mode 100644 index 0000000000..15fcd278d7 --- /dev/null +++ b/frontend/src/container/NewWidget/utils.ts @@ -0,0 +1,15 @@ +import { omitIdFromQuery } from 'components/ExplorerCard/utils'; +import { isEqual } from 'lodash-es'; +import { Query } from 'types/api/queryBuilder/queryBuilderData'; + +export const getIsQueryModified = ( + currentQuery: Query, + stagedQuery: Query | null, +): boolean => { + if (!stagedQuery) { + return false; + } + const omitIdFromStageQuery = omitIdFromQuery(stagedQuery); + const omitIdFromCurrentQuery = omitIdFromQuery(currentQuery); + return !isEqual(omitIdFromStageQuery, omitIdFromCurrentQuery); +}; From a16fca63763d15b8ac78973b1e26592c2c63ca04 Mon Sep 17 00:00:00 2001 From: Nityananda Gohain Date: Thu, 14 Dec 2023 16:52:02 +0530 Subject: [PATCH 03/32] fix: remove timestamp roundup for logs list api call (#4229) * fix: remove timestamp roundup for logs list api call * fix: test updated --- pkg/query-service/app/logs/v3/query_builder.go | 6 ++++-- pkg/query-service/app/logs/v3/query_builder_test.go | 8 ++++---- pkg/query-service/app/parser.go | 7 ------- 3 files changed, 8 insertions(+), 13 deletions(-) diff --git a/pkg/query-service/app/logs/v3/query_builder.go b/pkg/query-service/app/logs/v3/query_builder.go index 32e4bb29f4..c0457358f8 100644 --- a/pkg/query-service/app/logs/v3/query_builder.go +++ b/pkg/query-service/app/logs/v3/query_builder.go @@ -483,8 +483,10 @@ func isOrderByTs(orderBy []v3.OrderBy) bool { func PrepareLogsQuery(start, end int64, queryType v3.QueryType, panelType v3.PanelType, mq *v3.BuilderQuery, options Options) (string, error) { // adjust the start and end time to the step interval - start = start - (start % (mq.StepInterval * 1000)) - end = end - (end % (mq.StepInterval * 1000)) + if panelType != v3.PanelTypeList { + start = start - (start % (mq.StepInterval * 1000)) + end = end - (end % (mq.StepInterval * 1000)) + } if options.IsLivetailQuery { query, err := buildLogsLiveTailQuery(mq) diff --git a/pkg/query-service/app/logs/v3/query_builder_test.go b/pkg/query-service/app/logs/v3/query_builder_test.go index 1db46e1374..9bbc7da729 100644 --- a/pkg/query-service/app/logs/v3/query_builder_test.go +++ b/pkg/query-service/app/logs/v3/query_builder_test.go @@ -1353,7 +1353,7 @@ var testPrepLogsQueryLimitOffsetData = []struct { PageSize: 5, }, TableName: "logs", - ExpectedQuery: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, body,CAST((attributes_string_key, attributes_string_value), 'Map(String, String)') as attributes_string,CAST((attributes_int64_key, attributes_int64_value), 'Map(String, Int64)') as attributes_int64,CAST((attributes_float64_key, attributes_float64_value), 'Map(String, Float64)') as attributes_float64,CAST((attributes_bool_key, attributes_bool_value), 'Map(String, Bool)') as attributes_bool,CAST((resources_string_key, resources_string_value), 'Map(String, String)') as resources_string from signoz_logs.distributed_logs where (timestamp >= 1680066360000000000 AND timestamp <= 1680066420000000000) order by timestamp desc LIMIT 1", + ExpectedQuery: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, body,CAST((attributes_string_key, attributes_string_value), 'Map(String, String)') as attributes_string,CAST((attributes_int64_key, attributes_int64_value), 'Map(String, Int64)') as attributes_int64,CAST((attributes_float64_key, attributes_float64_value), 'Map(String, Float64)') as attributes_float64,CAST((attributes_bool_key, attributes_bool_value), 'Map(String, Bool)') as attributes_bool,CAST((resources_string_key, resources_string_value), 'Map(String, String)') as resources_string from signoz_logs.distributed_logs where (timestamp >= 1680066360726000000 AND timestamp <= 1680066458000000000) order by timestamp desc LIMIT 1", }, { Name: "Test limit greater than pageSize - order by ts", @@ -1374,7 +1374,7 @@ var testPrepLogsQueryLimitOffsetData = []struct { PageSize: 10, }, TableName: "logs", - ExpectedQuery: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, body,CAST((attributes_string_key, attributes_string_value), 'Map(String, String)') as attributes_string,CAST((attributes_int64_key, attributes_int64_value), 'Map(String, Int64)') as attributes_int64,CAST((attributes_float64_key, attributes_float64_value), 'Map(String, Float64)') as attributes_float64,CAST((attributes_bool_key, attributes_bool_value), 'Map(String, Bool)') as attributes_bool,CAST((resources_string_key, resources_string_value), 'Map(String, String)') as resources_string from signoz_logs.distributed_logs where (timestamp >= 1680066360000000000 AND timestamp <= 1680066420000000000) AND id < '2TNh4vp2TpiWyLt3SzuadLJF2s4' order by timestamp desc LIMIT 10", + ExpectedQuery: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, body,CAST((attributes_string_key, attributes_string_value), 'Map(String, String)') as attributes_string,CAST((attributes_int64_key, attributes_int64_value), 'Map(String, Int64)') as attributes_int64,CAST((attributes_float64_key, attributes_float64_value), 'Map(String, Float64)') as attributes_float64,CAST((attributes_bool_key, attributes_bool_value), 'Map(String, Bool)') as attributes_bool,CAST((resources_string_key, resources_string_value), 'Map(String, String)') as resources_string from signoz_logs.distributed_logs where (timestamp >= 1680066360726000000 AND timestamp <= 1680066458000000000) AND id < '2TNh4vp2TpiWyLt3SzuadLJF2s4' order by timestamp desc LIMIT 10", }, { Name: "Test limit less than pageSize - order by custom", @@ -1393,7 +1393,7 @@ var testPrepLogsQueryLimitOffsetData = []struct { PageSize: 5, }, TableName: "logs", - ExpectedQuery: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, body,CAST((attributes_string_key, attributes_string_value), 'Map(String, String)') as attributes_string,CAST((attributes_int64_key, attributes_int64_value), 'Map(String, Int64)') as attributes_int64,CAST((attributes_float64_key, attributes_float64_value), 'Map(String, Float64)') as attributes_float64,CAST((attributes_bool_key, attributes_bool_value), 'Map(String, Bool)') as attributes_bool,CAST((resources_string_key, resources_string_value), 'Map(String, String)') as resources_string from signoz_logs.distributed_logs where (timestamp >= 1680066360000000000 AND timestamp <= 1680066420000000000) order by attributes_string_value[indexOf(attributes_string_key, 'method')] desc LIMIT 1 OFFSET 0", + ExpectedQuery: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, body,CAST((attributes_string_key, attributes_string_value), 'Map(String, String)') as attributes_string,CAST((attributes_int64_key, attributes_int64_value), 'Map(String, Int64)') as attributes_int64,CAST((attributes_float64_key, attributes_float64_value), 'Map(String, Float64)') as attributes_float64,CAST((attributes_bool_key, attributes_bool_value), 'Map(String, Bool)') as attributes_bool,CAST((resources_string_key, resources_string_value), 'Map(String, String)') as resources_string from signoz_logs.distributed_logs where (timestamp >= 1680066360726000000 AND timestamp <= 1680066458000000000) order by attributes_string_value[indexOf(attributes_string_key, 'method')] desc LIMIT 1 OFFSET 0", }, { Name: "Test limit greater than pageSize - order by custom", @@ -1414,7 +1414,7 @@ var testPrepLogsQueryLimitOffsetData = []struct { PageSize: 50, }, TableName: "logs", - ExpectedQuery: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, body,CAST((attributes_string_key, attributes_string_value), 'Map(String, String)') as attributes_string,CAST((attributes_int64_key, attributes_int64_value), 'Map(String, Int64)') as attributes_int64,CAST((attributes_float64_key, attributes_float64_value), 'Map(String, Float64)') as attributes_float64,CAST((attributes_bool_key, attributes_bool_value), 'Map(String, Bool)') as attributes_bool,CAST((resources_string_key, resources_string_value), 'Map(String, String)') as resources_string from signoz_logs.distributed_logs where (timestamp >= 1680066360000000000 AND timestamp <= 1680066420000000000) AND id < '2TNh4vp2TpiWyLt3SzuadLJF2s4' order by attributes_string_value[indexOf(attributes_string_key, 'method')] desc LIMIT 50 OFFSET 50", + ExpectedQuery: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, body,CAST((attributes_string_key, attributes_string_value), 'Map(String, String)') as attributes_string,CAST((attributes_int64_key, attributes_int64_value), 'Map(String, Int64)') as attributes_int64,CAST((attributes_float64_key, attributes_float64_value), 'Map(String, Float64)') as attributes_float64,CAST((attributes_bool_key, attributes_bool_value), 'Map(String, Bool)') as attributes_bool,CAST((resources_string_key, resources_string_value), 'Map(String, String)') as resources_string from signoz_logs.distributed_logs where (timestamp >= 1680066360726000000 AND timestamp <= 1680066458000000000) AND id < '2TNh4vp2TpiWyLt3SzuadLJF2s4' order by attributes_string_value[indexOf(attributes_string_key, 'method')] desc LIMIT 50 OFFSET 50", }, } diff --git a/pkg/query-service/app/parser.go b/pkg/query-service/app/parser.go index 4e6350b70e..96905dc1d5 100644 --- a/pkg/query-service/app/parser.go +++ b/pkg/query-service/app/parser.go @@ -1021,13 +1021,6 @@ func ParseQueryRangeParams(r *http.Request) (*v3.QueryRangeParamsV3, *model.ApiE queryRangeParams.Start = queryRangeParams.End } - // round up the end to neaerest multiple - if queryRangeParams.CompositeQuery.QueryType == v3.QueryTypeBuilder { - end := (queryRangeParams.End) / 1000 - step := queryRangeParams.Step - queryRangeParams.End = (end / step * step) * 1000 - } - // replace go template variables in clickhouse query if queryRangeParams.CompositeQuery.QueryType == v3.QueryTypeClickHouseSQL { for _, chQuery := range queryRangeParams.CompositeQuery.ClickHouseQueries { From 1d1154aa8cc0ed05800932f7325af0a56fdc9602 Mon Sep 17 00:00:00 2001 From: Rajat Dabade Date: Thu, 14 Dec 2023 18:10:52 +0530 Subject: [PATCH 04/32] [Refactor]: added tooltip for graph manager (#4236) --- .../GridCardLayout/GridCard/FullView/TableRender/Label.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/src/container/GridCardLayout/GridCard/FullView/TableRender/Label.tsx b/frontend/src/container/GridCardLayout/GridCard/FullView/TableRender/Label.tsx index 7f5592e592..4b9a33587b 100644 --- a/frontend/src/container/GridCardLayout/GridCard/FullView/TableRender/Label.tsx +++ b/frontend/src/container/GridCardLayout/GridCard/FullView/TableRender/Label.tsx @@ -1,3 +1,4 @@ +import { Tooltip } from 'antd'; import { useIsDarkMode } from 'hooks/useDarkMode'; import { LabelContainer } from '../styles'; @@ -23,7 +24,9 @@ function Label({ disabled={disabled} onClick={onClickHandler} > - {getAbbreviatedLabel(label)} + + {getAbbreviatedLabel(label)} + ); } From 7efe90775762737eac41518019a8f4bec0795b38 Mon Sep 17 00:00:00 2001 From: Vikrant Gupta <54737045+Vikrant2520@users.noreply.github.com> Date: Thu, 14 Dec 2023 22:14:58 +0530 Subject: [PATCH 05/32] fix: [GH-3790]: timerange not working for different users (#4192) --- .../TopNav/DateTimeSelection/index.tsx | 48 ++++++++++++------- frontend/src/hooks/logs/useCopyLogLink.ts | 20 ++++++-- 2 files changed, 46 insertions(+), 22 deletions(-) diff --git a/frontend/src/container/TopNav/DateTimeSelection/index.tsx b/frontend/src/container/TopNav/DateTimeSelection/index.tsx index 742409e455..14f454dbb1 100644 --- a/frontend/src/container/TopNav/DateTimeSelection/index.tsx +++ b/frontend/src/container/TopNav/DateTimeSelection/index.tsx @@ -3,12 +3,16 @@ import { Button, Select as DefaultSelect } from 'antd'; import getLocalStorageKey from 'api/browser/localstorage/get'; import setLocalStorageKey from 'api/browser/localstorage/set'; import { LOCALSTORAGE } from 'constants/localStorage'; +import { QueryParams } from 'constants/query'; +import ROUTES from 'constants/routes'; import dayjs, { Dayjs } from 'dayjs'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { updateStepInterval } from 'hooks/queryBuilder/useStepInterval'; +import useUrlQuery from 'hooks/useUrlQuery'; import GetMinMax from 'lib/getMinMax'; import getTimeString from 'lib/getTimeString'; -import { useCallback, useEffect, useState } from 'react'; +import history from 'lib/history'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { connect, useSelector } from 'react-redux'; import { RouteComponentProps, withRouter } from 'react-router-dom'; import { bindActionCreators, Dispatch } from 'redux'; @@ -34,9 +38,9 @@ function DateTimeSelection({ }: Props): JSX.Element { const [formSelector] = Form.useForm(); - const params = new URLSearchParams(location.search); - const searchStartTime = params.get('startTime'); - const searchEndTime = params.get('endTime'); + const urlQuery = useUrlQuery(); + const searchStartTime = urlQuery.get('startTime'); + const searchEndTime = urlQuery.get('endTime'); const localstorageStartTime = getLocalStorageKey('startTime'); const localstorageEndTime = getLocalStorageKey('endTime'); @@ -169,6 +173,11 @@ function DateTimeSelection({ return `Last refresh - ${secondsDiff} sec ago`; }, [maxTime, minTime, selectedTime]); + const isLogsExplorerPage = useMemo( + () => location.pathname === ROUTES.LOGS_EXPLORER, + [location.pathname], + ); + const onSelectHandler = (value: Time): void => { if (value !== 'custom') { updateTimeInterval(value); @@ -181,12 +190,18 @@ function DateTimeSelection({ setCustomDTPickerVisible(true); } + const { maxTime, minTime } = GetMinMax(value, getTime()); + + if (!isLogsExplorerPage) { + urlQuery.set(QueryParams.startTime, minTime.toString()); + urlQuery.set(QueryParams.endTime, maxTime.toString()); + const generatedUrl = `${location.pathname}?${urlQuery.toString()}`; + history.replace(generatedUrl); + } + if (!stagedQuery) { return; } - - const { maxTime, minTime } = GetMinMax(value, getTime()); - initQueryBuilderData(updateStepInterval(stagedQuery, maxTime, minTime)); }; @@ -207,6 +222,12 @@ function DateTimeSelection({ setLocalStorageKey('startTime', startTimeMoment.toString()); setLocalStorageKey('endTime', endTimeMoment.toString()); updateLocalStorageForRoutes('custom'); + if (!isLogsExplorerPage) { + urlQuery.set(QueryParams.startTime, startTimeMoment.toString()); + urlQuery.set(QueryParams.endTime, endTimeMoment.toString()); + const generatedUrl = `${location.pathname}?${urlQuery.toString()}`; + history.replace(generatedUrl); + } } } }; @@ -234,7 +255,6 @@ function DateTimeSelection({ if (searchEndTime !== null && searchStartTime !== null) { return 'custom'; } - if ( (localstorageEndTime === null || localstorageStartTime === null) && time === 'custom' @@ -252,16 +272,8 @@ function DateTimeSelection({ setRefreshButtonHidden(updatedTime === 'custom'); updateTimeInterval(updatedTime, [preStartTime, preEndTime]); - }, [ - location.pathname, - getTime, - localstorageEndTime, - localstorageStartTime, - searchEndTime, - searchStartTime, - updateTimeInterval, - globalTimeLoading, - ]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [location.pathname, updateTimeInterval, globalTimeLoading]); return ( <> diff --git a/frontend/src/hooks/logs/useCopyLogLink.ts b/frontend/src/hooks/logs/useCopyLogLink.ts index 8401b6433f..35b4293f51 100644 --- a/frontend/src/hooks/logs/useCopyLogLink.ts +++ b/frontend/src/hooks/logs/useCopyLogLink.ts @@ -3,6 +3,7 @@ import ROUTES from 'constants/routes'; import { useNotifications } from 'hooks/useNotifications'; import useUrlQuery from 'hooks/useUrlQuery'; import useUrlQueryData from 'hooks/useUrlQueryData'; +import history from 'lib/history'; import { MouseEventHandler, useCallback, @@ -28,16 +29,27 @@ export const useCopyLogLink = (logId?: string): UseCopyLogLink => { (state) => state.globalTime, ); - const { - queryData: timeRange, - redirectWithQuery: onTimeRangeChange, - } = useUrlQueryData(QueryParams.timeRange, null); + const { queryData: timeRange } = useUrlQueryData( + QueryParams.timeRange, + null, + ); const { queryData: activeLogId } = useUrlQueryData( QueryParams.activeLogId, null, ); + const onTimeRangeChange = useCallback( + (newTimeRange: LogTimeRange | null): void => { + urlQuery.set(QueryParams.timeRange, JSON.stringify(newTimeRange)); + urlQuery.set(QueryParams.startTime, newTimeRange?.start.toString() || ''); + urlQuery.set(QueryParams.endTime, newTimeRange?.end.toString() || ''); + const generatedUrl = `${pathname}?${urlQuery.toString()}`; + history.replace(generatedUrl); + }, + [pathname, urlQuery], + ); + const isActiveLog = useMemo(() => activeLogId === logId, [activeLogId, logId]); const [isHighlighted, setIsHighlighted] = useState(isActiveLog); From 418ab67d5018751fa599379ec5a314c744e15fcb Mon Sep 17 00:00:00 2001 From: Yunus M Date: Thu, 14 Dec 2023 22:56:25 +0530 Subject: [PATCH 06/32] Uplot time range (#4144) * feat: show range bound chart based on the selected time range * feat: handle no data * feat: show bigger point if only data point exists * feat: show bigger point if only data point exists * feat: widget ui fixes * feat: no data - full view fix * fix: show closed point on hover * feat: handle widget time preference in case of dashboard, edit view, full view and chart preview --- frontend/.eslintrc.js | 2 +- .../Uplot/{uplot.scss => Uplot.styles.scss} | 8 ++++ frontend/src/components/Uplot/Uplot.tsx | 13 +++++- .../FormAlertRules/ChartPreview/index.tsx | 24 +++++++++-- .../GridCard/FullView/index.tsx | 18 +++++++++ .../GridCard/WidgetGraphComponent.tsx | 6 ++- .../GridCardLayout/GridCard/index.tsx | 24 ++++++++--- .../GridCardLayout/GridCardLayout.styles.scss | 8 ++++ .../WidgetHeader/WidgetHeader.styles.scss | 9 ++++- .../GridCardLayout/WidgetHeader/index.tsx | 6 +-- .../src/container/GridCardLayout/styles.ts | 10 +---- .../WidgetGraph/WidgetGraphs.tsx | 26 +++++++++++- .../LeftContainer/WidgetGraph/styles.ts | 3 +- .../TimeSeriesView/TimeSeriesView.tsx | 23 ++++++++++- .../src/lib/uPlotLib/getUplotChartOptions.ts | 24 ++++++----- .../src/lib/uPlotLib/plugins/tooltipPlugin.ts | 5 ++- frontend/src/lib/uPlotLib/utils/getAxes.ts | 6 +-- .../src/lib/uPlotLib/utils/getGridColor.ts | 4 +- .../src/lib/uPlotLib/utils/getSeriesData.ts | 9 ++++- .../src/lib/uPlotLib/utils/getXAxisScale.ts | 40 +++++++++++++++++++ .../src/lib/uPlotLib/utils/getYAxisScale.ts | 7 +++- frontend/src/utils/getTimeRange.ts | 36 +++++++++++++++++ 22 files changed, 260 insertions(+), 51 deletions(-) rename frontend/src/components/Uplot/{uplot.scss => Uplot.styles.scss} (68%) create mode 100644 frontend/src/lib/uPlotLib/utils/getXAxisScale.ts create mode 100644 frontend/src/utils/getTimeRange.ts diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js index 540b1ded70..5034915c00 100644 --- a/frontend/.eslintrc.js +++ b/frontend/.eslintrc.js @@ -86,6 +86,7 @@ module.exports = { }, ], 'import/no-extraneous-dependencies': ['error', { devDependencies: true }], + 'no-plusplus': 'off', 'jsx-a11y/label-has-associated-control': [ 'error', { @@ -109,7 +110,6 @@ module.exports = { // eslint rules need to remove '@typescript-eslint/no-shadow': 'off', 'import/no-cycle': 'off', - 'prettier/prettier': [ 'error', {}, diff --git a/frontend/src/components/Uplot/uplot.scss b/frontend/src/components/Uplot/Uplot.styles.scss similarity index 68% rename from frontend/src/components/Uplot/uplot.scss rename to frontend/src/components/Uplot/Uplot.styles.scss index 55e681cb70..448ef56b18 100644 --- a/frontend/src/components/Uplot/uplot.scss +++ b/frontend/src/components/Uplot/Uplot.styles.scss @@ -13,3 +13,11 @@ height: 100%; width: 100%; } + +.uplot-no-data { + position: relative; + display: flex; + width: 100%; + flex-direction: column; + gap: 8px; +} diff --git a/frontend/src/components/Uplot/Uplot.tsx b/frontend/src/components/Uplot/Uplot.tsx index 84b3d1bb9b..05f050a87c 100644 --- a/frontend/src/components/Uplot/Uplot.tsx +++ b/frontend/src/components/Uplot/Uplot.tsx @@ -1,8 +1,9 @@ /* eslint-disable sonarjs/cognitive-complexity */ -import './uplot.scss'; +import './Uplot.styles.scss'; import { Typography } from 'antd'; import { ToggleGraphProps } from 'components/Graph/types'; +import { LineChart } from 'lucide-react'; import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback'; import { forwardRef, @@ -127,6 +128,16 @@ const Uplot = forwardRef( } }, [data, resetScales, create]); + if (data && data[0] && data[0]?.length === 0) { + return ( +
+ + + No Data +
+ ); + } + return (
diff --git a/frontend/src/container/FormAlertRules/ChartPreview/index.tsx b/frontend/src/container/FormAlertRules/ChartPreview/index.tsx index 3551c3a87a..7e22e3a11c 100644 --- a/frontend/src/container/FormAlertRules/ChartPreview/index.tsx +++ b/frontend/src/container/FormAlertRules/ChartPreview/index.tsx @@ -10,7 +10,7 @@ import { useIsDarkMode } from 'hooks/useDarkMode'; import { useResizeObserver } from 'hooks/useDimensions'; import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions'; import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData'; -import { useMemo, useRef } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; import { AppState } from 'store/reducers'; @@ -18,6 +18,7 @@ import { AlertDef } from 'types/api/alerts/def'; import { Query } from 'types/api/queryBuilder/queryBuilderData'; import { EQueryType } from 'types/common/dashboard'; import { GlobalReducer } from 'types/reducer/globalTime'; +import { getTimeRange } from 'utils/getTimeRange'; import { ChartContainer, FailedMessageContainer } from './styles'; import { getThresholdLabel } from './utils'; @@ -49,9 +50,13 @@ function ChartPreview({ }: ChartPreviewProps): JSX.Element | null { const { t } = useTranslation('alerts'); const threshold = alertDef?.condition.target || 0; - const { minTime, maxTime } = useSelector( - (state) => state.globalTime, - ); + const [minTimeScale, setMinTimeScale] = useState(); + const [maxTimeScale, setMaxTimeScale] = useState(); + + const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector< + AppState, + GlobalReducer + >((state) => state.globalTime); const canQuery = useMemo((): boolean => { if (!query || query == null) { @@ -101,6 +106,13 @@ function ChartPreview({ const graphRef = useRef(null); + useEffect((): void => { + const { startTime, endTime } = getTimeRange(queryResponse); + + setMinTimeScale(startTime); + setMaxTimeScale(endTime); + }, [maxTime, minTime, globalSelectedInterval, queryResponse]); + const chartData = getUPlotChartData(queryResponse?.data?.payload); const containerDimensions = useResizeObserver(graphRef); @@ -117,6 +129,8 @@ function ChartPreview({ yAxisUnit, apiResponse: queryResponse?.data?.payload, dimensions: containerDimensions, + minTimeScale, + maxTimeScale, isDarkMode, thresholds: [ { @@ -141,6 +155,8 @@ function ChartPreview({ yAxisUnit, queryResponse?.data?.payload, containerDimensions, + minTimeScale, + maxTimeScale, isDarkMode, threshold, t, diff --git a/frontend/src/container/GridCardLayout/GridCard/FullView/index.tsx b/frontend/src/container/GridCardLayout/GridCard/FullView/index.tsx index 3bd60c40ba..db42625f1d 100644 --- a/frontend/src/container/GridCardLayout/GridCard/FullView/index.tsx +++ b/frontend/src/container/GridCardLayout/GridCard/FullView/index.tsx @@ -23,6 +23,7 @@ import { useSelector } from 'react-redux'; import { AppState } from 'store/reducers'; import { GlobalReducer } from 'types/reducer/globalTime'; import uPlot from 'uplot'; +import { getTimeRange } from 'utils/getTimeRange'; import { PANEL_TYPES_VS_FULL_VIEW_TABLE } from './contants'; import GraphManager from './GraphManager'; @@ -92,6 +93,21 @@ function FullView({ const isDarkMode = useIsDarkMode(); + const [minTimeScale, setMinTimeScale] = useState(); + const [maxTimeScale, setMaxTimeScale] = useState(); + + const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector< + AppState, + GlobalReducer + >((state) => state.globalTime); + + useEffect((): void => { + const { startTime, endTime } = getTimeRange(response); + + setMinTimeScale(startTime); + setMaxTimeScale(endTime); + }, [maxTime, minTime, globalSelectedInterval, response]); + useEffect(() => { if (!response.isFetching && fullViewRef.current) { const width = fullViewRef.current?.clientWidth @@ -114,6 +130,8 @@ function FullView({ graphsVisibilityStates, setGraphsVisibilityStates, thresholds: widget.thresholds, + minTimeScale, + maxTimeScale, }); setChartOptions(newChartOptions); diff --git a/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.tsx b/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.tsx index 2129220427..29abaa9685 100644 --- a/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.tsx +++ b/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.tsx @@ -1,4 +1,5 @@ import { Skeleton, Typography } from 'antd'; +import cx from 'classnames'; import { ToggleGraphProps } from 'components/Graph/types'; import { SOMETHING_WENT_WRONG } from 'constants/api'; import { QueryParams } from 'constants/query'; @@ -298,7 +299,10 @@ function WidgetGraphComponent({
{queryResponse.isLoading && } {queryResponse.isSuccess && ( -
+
(); const { toScrollWidgetId, setToScrollWidgetId } = useDashboard(); + const [minTimeScale, setMinTimeScale] = useState(); + const [maxTimeScale, setMaxTimeScale] = useState(); const onDragSelect = useCallback( (start: number, end: number): void => { @@ -62,16 +65,16 @@ function GridCardGraph({ } }, [toScrollWidgetId, setToScrollWidgetId, widget.id]); - const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector< - AppState, - GlobalReducer - >((state) => state.globalTime); - const updatedQuery = useStepInterval(widget?.query); const isEmptyWidget = widget?.id === PANEL_TYPES.EMPTY_WIDGET || isEmpty(widget); + const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector< + AppState, + GlobalReducer + >((state) => state.globalTime); + const queryResponse = useGetQueryRange( { selectedTime: widget?.timePreferance, @@ -103,6 +106,13 @@ function GridCardGraph({ const containerDimensions = useResizeObserver(graphRef); + useEffect((): void => { + const { startTime, endTime } = getTimeRange(queryResponse); + + setMinTimeScale(startTime); + setMaxTimeScale(endTime); + }, [maxTime, minTime, globalSelectedInterval, queryResponse]); + const chartData = getUPlotChartData(queryResponse?.data?.payload, fillSpans); const isDarkMode = useIsDarkMode(); @@ -123,6 +133,8 @@ function GridCardGraph({ yAxisUnit: widget?.yAxisUnit, onClickHandler, thresholds: widget.thresholds, + minTimeScale, + maxTimeScale, }), [ widget?.id, @@ -133,6 +145,8 @@ function GridCardGraph({ isDarkMode, onDragSelect, onClickHandler, + minTimeScale, + maxTimeScale, ], ); diff --git a/frontend/src/container/GridCardLayout/GridCardLayout.styles.scss b/frontend/src/container/GridCardLayout/GridCardLayout.styles.scss index 08de391e4c..ab90890541 100644 --- a/frontend/src/container/GridCardLayout/GridCardLayout.styles.scss +++ b/frontend/src/container/GridCardLayout/GridCardLayout.styles.scss @@ -5,3 +5,11 @@ border: none !important; } } + +.widget-graph-container { + height: 100%; + + &.graph { + height: calc(100% - 30px); + } +} diff --git a/frontend/src/container/GridCardLayout/WidgetHeader/WidgetHeader.styles.scss b/frontend/src/container/GridCardLayout/WidgetHeader/WidgetHeader.styles.scss index e03f176570..40d138e7df 100644 --- a/frontend/src/container/GridCardLayout/WidgetHeader/WidgetHeader.styles.scss +++ b/frontend/src/container/GridCardLayout/WidgetHeader/WidgetHeader.styles.scss @@ -2,9 +2,12 @@ display: flex; justify-content: space-between; align-items: center; - height: 30px; + height: 40px; width: 100%; padding: 0.5rem; + box-sizing: border-box; + font-size: 14px; + font-weight: 600; } .widget-header-title { @@ -19,6 +22,10 @@ visibility: hidden; border: none; box-shadow: none; + cursor: pointer; + font: 14px; + font-weight: 600; + padding: 8px; } .widget-header-hover { diff --git a/frontend/src/container/GridCardLayout/WidgetHeader/index.tsx b/frontend/src/container/GridCardLayout/WidgetHeader/index.tsx index 0fb2f90ead..b70e16585c 100644 --- a/frontend/src/container/GridCardLayout/WidgetHeader/index.tsx +++ b/frontend/src/container/GridCardLayout/WidgetHeader/index.tsx @@ -10,7 +10,7 @@ import { MoreOutlined, WarningOutlined, } from '@ant-design/icons'; -import { Button, Dropdown, MenuProps, Tooltip, Typography } from 'antd'; +import { Dropdown, MenuProps, Tooltip, Typography } from 'antd'; import Spinner from 'components/Spinner'; import { QueryParams } from 'constants/query'; import { PANEL_TYPES } from 'constants/queryBuilder'; @@ -199,9 +199,7 @@ function WidgetHeader({ )} - @@ -157,7 +278,9 @@ function VariablesSetting(): JSX.Element { type="text" style={{ padding: 8, color: red[6], cursor: 'pointer' }} onClick={(): void => { - if (_.name) onVariableDeleteHandler(_.name); + if (variable) { + onVariableDeleteHandler(variable); + } }} > @@ -167,6 +290,51 @@ function VariablesSetting(): JSX.Element { }, ]; + const sensors = useSensors( + useSensor(PointerSensor, { + activationConstraint: { + // https://docs.dndkit.com/api-documentation/sensors/pointer#activation-constraints + distance: 1, + }, + }), + ); + + const onDragEnd = ({ active, over }: DragEndEvent): void => { + if (active.id !== over?.id) { + const activeIndex = variablesTableData.findIndex( + (i: { key: UniqueIdentifier }) => i.key === active.id, + ); + const overIndex = variablesTableData.findIndex( + (i: { key: UniqueIdentifier | undefined }) => i.key === over?.id, + ); + + const updatedVariables: IDashboardVariable[] = arrayMove( + variablesTableData, + activeIndex, + overIndex, + ); + + const reArrangedVariables = {}; + + for (let index = 0; index < updatedVariables.length; index += 1) { + const variableName = updatedVariables[index].name; + + if (variableName) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + reArrangedVariables[variableName] = { + ...updatedVariables[index], + order: index, + }; + } + } + + updateVariables(reArrangedVariables); + + setVariablesTableData(updatedVariables); + } + }; + return ( <> {variableViewMode ? ( @@ -176,11 +344,17 @@ function VariablesSetting(): JSX.Element { onSave={onVariableSaveHandler} onCancel={onDoneVariableViewMode} validateName={validateVariableName} - variableViewMode={variableViewMode} + mode={variableViewMode} /> ) : ( <> - +