diff --git a/README.md b/README.md index 4f6dff2261..ab7f9862aa 100644 --- a/README.md +++ b/README.md @@ -199,10 +199,13 @@ Not sure how to get started? Just ping us on `#contributing` in our [slack commu #### Frontend - [Palash Gupta](https://github.com/palashgdev) +- [Yunus M](https://github.com/YounixM) +- [Rajat Dabade](https://github.com/Rajat-Dabade) #### DevOps - [Prashant Shahi](https://github.com/prashant-shahi) +- [Dhawal Sanghvi](https://github.com/dhawal1248)

diff --git a/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml b/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml index 30e5deecc1..0c860928f6 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: @@ -146,7 +146,7 @@ services: condition: on-failure query-service: - image: signoz/query-service:0.35.1 + image: signoz/query-service:0.36.0 command: [ "-config=/root/config/prometheus.yml", @@ -186,7 +186,7 @@ services: <<: *db-depend frontend: - image: signoz/frontend:0.35.1 + image: signoz/frontend:0.36.0 deploy: restart_policy: condition: on-failure @@ -199,7 +199,7 @@ services: - ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf otel-collector: - image: signoz/signoz-otel-collector:0.88.3 + image: signoz/signoz-otel-collector:0.88.4 command: [ "--config=/etc/otel-collector-config.yaml", @@ -237,7 +237,7 @@ services: - query-service otel-collector-migrator: - image: signoz/signoz-schema-migrator:0.88.3 + image: signoz/signoz-schema-migrator:0.88.4 deploy: restart_policy: condition: on-failure @@ -250,7 +250,7 @@ services: # - clickhouse-3 otel-collector-metrics: - image: signoz/signoz-otel-collector:0.88.3 + image: signoz/signoz-otel-collector:0.88.4 command: [ "--config=/etc/otel-collector-metrics-config.yaml", diff --git a/deploy/docker/clickhouse-setup/docker-compose-core.yaml b/deploy/docker/clickhouse-setup/docker-compose-core.yaml index 4e3f5d8bbe..5b67209405 100644 --- a/deploy/docker/clickhouse-setup/docker-compose-core.yaml +++ b/deploy/docker/clickhouse-setup/docker-compose-core.yaml @@ -66,7 +66,7 @@ services: - --storage.path=/data otel-collector-migrator: - image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.3} + image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.4} container_name: otel-migrator command: - "--dsn=tcp://clickhouse:9000" @@ -81,7 +81,7 @@ services: # Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md` otel-collector: container_name: signoz-otel-collector - image: signoz/signoz-otel-collector:0.88.3 + image: signoz/signoz-otel-collector:0.88.4 command: [ "--config=/etc/otel-collector-config.yaml", @@ -118,7 +118,7 @@ services: otel-collector-metrics: container_name: signoz-otel-collector-metrics - image: signoz/signoz-otel-collector:0.88.3 + image: signoz/signoz-otel-collector:0.88.4 command: [ "--config=/etc/otel-collector-metrics-config.yaml", diff --git a/deploy/docker/clickhouse-setup/docker-compose.yaml b/deploy/docker/clickhouse-setup/docker-compose.yaml index 2e54477bcf..c44cf42850 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 @@ -164,7 +164,7 @@ services: # Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md` query-service: - image: signoz/query-service:${DOCKER_TAG:-0.35.1} + image: signoz/query-service:${DOCKER_TAG:-0.36.0} container_name: signoz-query-service command: [ @@ -203,7 +203,7 @@ services: <<: *db-depend frontend: - image: signoz/frontend:${DOCKER_TAG:-0.35.1} + image: signoz/frontend:${DOCKER_TAG:-0.36.0} container_name: signoz-frontend restart: on-failure depends_on: @@ -215,7 +215,7 @@ services: - ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf otel-collector-migrator: - image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.3} + image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.4} container_name: otel-migrator command: - "--dsn=tcp://clickhouse:9000" @@ -229,7 +229,7 @@ services: otel-collector: - image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.88.3} + image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.88.4} container_name: signoz-otel-collector command: [ @@ -269,7 +269,7 @@ services: condition: service_healthy otel-collector-metrics: - image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.88.3} + image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.88.4} container_name: signoz-otel-collector-metrics command: [ diff --git a/ee/query-service/Dockerfile b/ee/query-service/Dockerfile index d7a6786377..769f18756f 100644 --- a/ee/query-service/Dockerfile +++ b/ee/query-service/Dockerfile @@ -18,6 +18,7 @@ COPY ee/query-service/bin/query-service-${TARGETOS}-${TARGETARCH} /root/query-se # copy prometheus YAML config COPY pkg/query-service/config/prometheus.yml /root/config/prometheus.yml +COPY pkg/query-service/templates /root/templates # Make query-service executable for non-root users RUN chmod 755 /root /root/query-service 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/package.json b/frontend/package.json index 64ac911fc4..f0edc5c959 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -29,6 +29,9 @@ "dependencies": { "@ant-design/colors": "6.0.0", "@ant-design/icons": "4.8.0", + "@dnd-kit/core": "6.1.0", + "@dnd-kit/modifiers": "7.0.0", + "@dnd-kit/sortable": "8.0.0", "@grafana/data": "^9.5.2", "@mdx-js/loader": "2.3.0", "@mdx-js/react": "2.3.0", diff --git a/frontend/public/Logos/cloudwatch.png b/frontend/public/Logos/cloudwatch.png new file mode 100644 index 0000000000..57cd671f42 Binary files /dev/null and b/frontend/public/Logos/cloudwatch.png differ diff --git a/frontend/public/Logos/dotnet.png b/frontend/public/Logos/dotnet.png new file mode 100644 index 0000000000..53c3da3c7a Binary files /dev/null and b/frontend/public/Logos/dotnet.png differ diff --git a/frontend/public/Logos/heroku.png b/frontend/public/Logos/heroku.png new file mode 100644 index 0000000000..0328d37b3a Binary files /dev/null and b/frontend/public/Logos/heroku.png differ diff --git a/frontend/public/Logos/http.png b/frontend/public/Logos/http.png new file mode 100644 index 0000000000..83ccf1086f Binary files /dev/null and b/frontend/public/Logos/http.png differ diff --git a/frontend/public/Logos/vercel.png b/frontend/public/Logos/vercel.png new file mode 100644 index 0000000000..0ac66ce6ad Binary files /dev/null and b/frontend/public/Logos/vercel.png differ 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-GB/organizationsettings.json b/frontend/public/locales/en-GB/organizationsettings.json index 7daaf5c781..deae9666ee 100644 --- a/frontend/public/locales/en-GB/organizationsettings.json +++ b/frontend/public/locales/en-GB/organizationsettings.json @@ -14,5 +14,6 @@ "delete_domain_message": "Are you sure you want to delete this domain?", "delete_domain": "Delete Domain", "add_domain": "Add Domains", - "saml_settings":"Your SAML settings have been saved, please login from incognito window to confirm that it has been set up correctly" + "saml_settings": "Your SAML settings have been saved, please login from incognito window to confirm that it has been set up correctly", + "invite_link_share_manually": "After inviting members, please copy the invite link and send them the link manually" } diff --git a/frontend/public/locales/en-GB/services.json b/frontend/public/locales/en-GB/services.json new file mode 100644 index 0000000000..4c49847031 --- /dev/null +++ b/frontend/public/locales/en-GB/services.json @@ -0,0 +1,3 @@ +{ + "rps_over_100": "You are sending data at more than 100 RPS, your ingestion may be rate limited. Please reach out to us via Intercom support." +} 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/public/locales/en/organizationsettings.json b/frontend/public/locales/en/organizationsettings.json index 7daaf5c781..deae9666ee 100644 --- a/frontend/public/locales/en/organizationsettings.json +++ b/frontend/public/locales/en/organizationsettings.json @@ -14,5 +14,6 @@ "delete_domain_message": "Are you sure you want to delete this domain?", "delete_domain": "Delete Domain", "add_domain": "Add Domains", - "saml_settings":"Your SAML settings have been saved, please login from incognito window to confirm that it has been set up correctly" + "saml_settings": "Your SAML settings have been saved, please login from incognito window to confirm that it has been set up correctly", + "invite_link_share_manually": "After inviting members, please copy the invite link and send them the link manually" } diff --git a/frontend/public/locales/en/services.json b/frontend/public/locales/en/services.json new file mode 100644 index 0000000000..4c49847031 --- /dev/null +++ b/frontend/public/locales/en/services.json @@ -0,0 +1,3 @@ +{ + "rps_over_100": "You are sending data at more than 100 RPS, your ingestion may be rate limited. Please reach out to us via Intercom support." +} diff --git a/frontend/src/api/dashboard/queryRangeFormat.ts b/frontend/src/api/dashboard/queryRangeFormat.ts new file mode 100644 index 0000000000..02e020bfb5 --- /dev/null +++ b/frontend/src/api/dashboard/queryRangeFormat.ts @@ -0,0 +1,15 @@ +import { ApiV3Instance as axios } from 'api'; +import { ApiResponse } from 'types/api'; +import { ICompositeMetricQuery } from 'types/api/alerts/compositeQuery'; +import { QueryRangePayload } from 'types/api/metrics/getQueryRange'; + +interface IQueryRangeFormat { + compositeQuery: ICompositeMetricQuery; +} + +export const getQueryRangeFormat = ( + props?: Partial, +): Promise => + axios + .post>('/query_range/format', props) + .then((res) => res.data.data); 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/components/ResizeTable/DynamicColumnTable.tsx b/frontend/src/components/ResizeTable/DynamicColumnTable.tsx index 385734f11d..55af931d5c 100644 --- a/frontend/src/components/ResizeTable/DynamicColumnTable.tsx +++ b/frontend/src/components/ResizeTable/DynamicColumnTable.tsx @@ -27,6 +27,7 @@ function DynamicColumnTable({ ); useEffect(() => { + setColumnsData(columns); const visibleColumns = getVisibleColumns({ tablesource, columnsData: columns, @@ -42,7 +43,7 @@ function DynamicColumnTable({ : undefined, ); // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [columns]); const onToggleHandler = (index: number) => ( checked: boolean, 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/constants/global.ts b/frontend/src/constants/global.ts new file mode 100644 index 0000000000..d2a455ea57 --- /dev/null +++ b/frontend/src/constants/global.ts @@ -0,0 +1,2 @@ +const MAX_RPS_LIMIT = 100; +export { MAX_RPS_LIMIT }; 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/GantChart/GantChart.styles.scss b/frontend/src/container/GantChart/GantChart.styles.scss new file mode 100644 index 0000000000..cc1e828842 --- /dev/null +++ b/frontend/src/container/GantChart/GantChart.styles.scss @@ -0,0 +1,16 @@ +.span-container { + .spanDetails { + position: absolute; + height: 50px; + padding: 8px; + min-width: 150px; + background: lightcyan; + color: black; + bottom: 24px; + left: 0; + + display: flex; + justify-content: center; + align-items: center; + } +} diff --git a/frontend/src/container/GantChart/Span/index.tsx b/frontend/src/container/GantChart/Span/index.tsx new file mode 100644 index 0000000000..23fc000ac3 --- /dev/null +++ b/frontend/src/container/GantChart/Span/index.tsx @@ -0,0 +1,96 @@ +import '../GantChart.styles.scss'; + +import { Popover, Typography } from 'antd'; +import { convertTimeToRelevantUnit } from 'container/TraceDetail/utils'; +import dayjs from 'dayjs'; +import { useIsDarkMode } from 'hooks/useDarkMode'; +import { useEffect } from 'react'; +import { toFixed } from 'utils/toFixed'; + +import { SpanBorder, SpanLine, SpanText, SpanWrapper } from './styles'; + +interface SpanLengthProps { + globalStart: number; + startTime: number; + name: string; + width: string; + leftOffset: string; + bgColor: string; + inMsCount: number; +} + +function Span(props: SpanLengthProps): JSX.Element { + const { + width, + leftOffset, + bgColor, + inMsCount, + startTime, + name, + globalStart, + } = props; + const isDarkMode = useIsDarkMode(); + const { time, timeUnitName } = convertTimeToRelevantUnit(inMsCount); + + useEffect(() => { + document.documentElement.scrollTop = document.documentElement.clientHeight; + document.documentElement.scrollLeft = document.documentElement.clientWidth; + }, []); + + const getContent = (): JSX.Element => { + const timeStamp = dayjs(startTime).format('h:mm:ss:SSS A'); + const startTimeInMs = startTime - globalStart; + return ( +
+ + {' '} + Duration : {inMsCount} + +
+ + Start Time: {startTimeInMs}ms [{timeStamp}]{' '} + +
+ ); + }; + + return ( + + + +
+ + + +
+ + {`${toFixed( + time, + 2, + )} ${timeUnitName}`} +
+ ); +} + +export default Span; diff --git a/frontend/src/container/GantChart/SpanLength/styles.ts b/frontend/src/container/GantChart/Span/styles.ts similarity index 100% rename from frontend/src/container/GantChart/SpanLength/styles.ts rename to frontend/src/container/GantChart/Span/styles.ts diff --git a/frontend/src/container/GantChart/SpanLength/index.tsx b/frontend/src/container/GantChart/SpanLength/index.tsx deleted file mode 100644 index 9e3611bb01..0000000000 --- a/frontend/src/container/GantChart/SpanLength/index.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { convertTimeToRelevantUnit } from 'container/TraceDetail/utils'; -import { useIsDarkMode } from 'hooks/useDarkMode'; -import { toFixed } from 'utils/toFixed'; - -import { SpanBorder, SpanLine, SpanText, SpanWrapper } from './styles'; - -interface SpanLengthProps { - width: string; - leftOffset: string; - bgColor: string; - inMsCount: number; -} - -function SpanLength(props: SpanLengthProps): JSX.Element { - const { width, leftOffset, bgColor, inMsCount } = props; - const isDarkMode = useIsDarkMode(); - const { time, timeUnitName } = convertTimeToRelevantUnit(inMsCount); - return ( - - - - {`${toFixed( - time, - 2, - )} ${timeUnitName}`} - - ); -} - -export default SpanLength; diff --git a/frontend/src/container/GantChart/Trace/index.tsx b/frontend/src/container/GantChart/Trace/index.tsx index d38e3594ae..05a19a8268 100644 --- a/frontend/src/container/GantChart/Trace/index.tsx +++ b/frontend/src/container/GantChart/Trace/index.tsx @@ -16,7 +16,7 @@ import { import { ITraceTree } from 'types/api/trace/getTraceItem'; import { ITraceMetaData } from '..'; -import SpanLength from '../SpanLength'; +import Span from '../Span'; import SpanName from '../SpanName'; import { getMetaDataFromSpanTree, getTopLeftFromBody } from '../utils'; import { @@ -169,7 +169,10 @@ function Trace(props: TraceProps): JSX.Element { - - {isExpandAll ? : } + {isExpandAll ? ( + + ) : ( + + )} - {getAbbreviatedLabel(label)} + + {getAbbreviatedLabel(label)} + ); } 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..820c3b8a07 100644 --- a/frontend/src/container/GridCardLayout/WidgetHeader/index.tsx +++ b/frontend/src/container/GridCardLayout/WidgetHeader/index.tsx @@ -10,11 +10,11 @@ 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'; -import ROUTES from 'constants/routes'; +import useCreateAlerts from 'hooks/queryBuilder/useCreateAlerts'; import useComponentPermission from 'hooks/useComponentPermission'; import history from 'lib/history'; import { ReactNode, useCallback, useMemo } from 'react'; @@ -71,13 +71,7 @@ function WidgetHeader({ ); }, [widget.id, widget.panelTypes, widget.query]); - const onCreateAlertsHandler = useCallback(() => { - history.push( - `${ROUTES.ALERTS_NEW}?${QueryParams.compositeQuery}=${encodeURIComponent( - JSON.stringify(widget.query), - )}`, - ); - }, [widget]); + const onCreateAlertsHandler = useCreateAlerts(widget); const keyMethodMapping = useMemo( () => ({ @@ -199,9 +193,7 @@ function WidgetHeader({ )} - - )} - + {addNewAlert && ( + + )} + + ); diff --git a/frontend/src/container/ListAlertRules/styles.ts b/frontend/src/container/ListAlertRules/styles.ts index f2d0937950..c0deac35de 100644 --- a/frontend/src/container/ListAlertRules/styles.ts +++ b/frontend/src/container/ListAlertRules/styles.ts @@ -1,11 +1,17 @@ import { Button as ButtonComponent } from 'antd'; import styled from 'styled-components'; +export const SearchContainer = styled.div` + &&& { + display: flex; + margin-bottom: 2rem; + align-items: center; + gap: 2rem; + } +`; export const ButtonContainer = styled.div` &&& { display: flex; - justify-content: flex-end; - margin-bottom: 2rem; align-items: center; } `; diff --git a/frontend/src/container/ListAlertRules/utils.ts b/frontend/src/container/ListAlertRules/utils.ts new file mode 100644 index 0000000000..a1cef5add7 --- /dev/null +++ b/frontend/src/container/ListAlertRules/utils.ts @@ -0,0 +1,25 @@ +import { GettableAlert } from 'types/api/alerts/get'; + +export const filterAlerts = ( + allAlertRules: GettableAlert[], + filter: string, +): GettableAlert[] => { + const value = filter.toLowerCase(); + return allAlertRules.filter((alert) => { + const alertName = alert.alert.toLowerCase(); + const severity = alert.labels?.severity.toLowerCase(); + const labels = Object.keys(alert.labels || {}) + .filter((e) => e !== 'severity') + .join(' ') + .toLowerCase(); + + const labelValue = Object.values(alert.labels || {}); + + return ( + alertName.includes(value) || + severity?.includes(value) || + labels.includes(value) || + labelValue.includes(value) + ); + }); +}; diff --git a/frontend/src/container/LogDetailedView/TableView.tsx b/frontend/src/container/LogDetailedView/TableView.tsx index 9237136349..929e827dc3 100644 --- a/frontend/src/container/LogDetailedView/TableView.tsx +++ b/frontend/src/container/LogDetailedView/TableView.tsx @@ -22,6 +22,7 @@ import { ILog } from 'types/api/logs/log'; import ActionItem, { ActionItemProps } from './ActionItem'; import FieldRenderer from './FieldRenderer'; import { + filterKeyForField, flattenObject, jsonToDataNodes, recursiveParseJSON, @@ -98,11 +99,12 @@ function TableView({ title: 'Action', width: 11, render: (fieldData: Record): JSX.Element | null => { - const fieldKey = fieldData.field.split('.').slice(-1); - if (!RESTRICTED_FIELDS.includes(fieldKey[0])) { + const fieldFilterKey = filterKeyForField(fieldData.field); + + if (!RESTRICTED_FIELDS.includes(fieldFilterKey)) { return ( @@ -119,7 +121,6 @@ function TableView({ align: 'left', ellipsis: true, render: (field: string, record): JSX.Element => { - const fieldKey = field.split('.').slice(-1); const renderedField = ; if (record.field === 'trace_id') { @@ -148,10 +149,11 @@ function TableView({ ); } - if (!RESTRICTED_FIELDS.includes(fieldKey[0])) { + const fieldFilterKey = filterKeyForField(field); + if (!RESTRICTED_FIELDS.includes(fieldFilterKey)) { return ( diff --git a/frontend/src/container/LogDetailedView/utils.tsx b/frontend/src/container/LogDetailedView/utils.tsx index f31534ace8..cb1b1a2e53 100644 --- a/frontend/src/container/LogDetailedView/utils.tsx +++ b/frontend/src/container/LogDetailedView/utils.tsx @@ -132,6 +132,16 @@ export const generateFieldKeyForArray = ( export const removeObjectFromString = (str: string): string => str.replace(/\[object Object\]./g, ''); +// Split `str` on the first occurrence of `delimiter` +// For example, will return `['a', 'b.c']` when splitting `'a.b.c'` at dots +const splitOnce = (str: string, delimiter: string): string[] => { + const parts = str.split(delimiter); + if (parts.length < 2) { + return parts; + } + return [parts[0], parts.slice(1).join(delimiter)]; +}; + export const getFieldAttributes = (field: string): IFieldAttributes => { let dataType; let newField; @@ -140,18 +150,30 @@ export const getFieldAttributes = (field: string): IFieldAttributes => { if (field.startsWith('attributes_')) { logType = MetricsType.Tag; const stringWithoutPrefix = field.slice('attributes_'.length); - const parts = stringWithoutPrefix.split('.'); + const parts = splitOnce(stringWithoutPrefix, '.'); [dataType, newField] = parts; } else if (field.startsWith('resources_')) { logType = MetricsType.Resource; const stringWithoutPrefix = field.slice('resources_'.length); - const parts = stringWithoutPrefix.split('.'); + const parts = splitOnce(stringWithoutPrefix, '.'); [dataType, newField] = parts; } return { dataType, newField, logType }; }; +// Returns key to be used when filtering for `field` via +// the query builder. This is useful for powering filtering +// by field values from log details view. +export const filterKeyForField = (field: string): string => { + // Must work for all 3 of the following types of cases + // timestamp -> timestamp + // attributes_string.log.file -> log.file + // resources_string.k8s.pod.name -> k8s.pod.name + const fieldAttribs = getFieldAttributes(field); + return fieldAttribs?.newField || field; +}; + export const aggregateAttributesResourcesToString = (logData: ILog): string => { const outputJson: ILogAggregateAttributesResources = { body: logData.body, diff --git a/frontend/src/container/LogsExplorerViews/index.tsx b/frontend/src/container/LogsExplorerViews/index.tsx index eeac5513b9..bcc67ddd6a 100644 --- a/frontend/src/container/LogsExplorerViews/index.tsx +++ b/frontend/src/container/LogsExplorerViews/index.tsx @@ -147,13 +147,13 @@ function LogsExplorerViews(): JSX.Element { [currentQuery, updateAllQueriesOperators], ); - const listChartData = useGetExplorerQueryRange( - listChartQuery, - PANEL_TYPES.TIME_SERIES, - { - enabled: !!listChartQuery && panelType === PANEL_TYPES.LIST, - }, - ); + const { + data: listChartData, + isFetching: isFetchingListChartData, + isLoading: isLoadingListChartData, + } = useGetExplorerQueryRange(listChartQuery, PANEL_TYPES.TIME_SERIES, { + enabled: !!listChartQuery && panelType === PANEL_TYPES.LIST, + }); const { data, isFetching, isError } = useGetExplorerQueryRange( requestData, @@ -445,12 +445,8 @@ function LogsExplorerViews(): JSX.Element { if (!stagedQuery) return []; if (panelType === PANEL_TYPES.LIST) { - if ( - listChartData && - listChartData.data && - listChartData.data.payload.data.result.length > 0 - ) { - return listChartData.data.payload.data.result; + if (listChartData && listChartData.payload.data.result.length > 0) { + return listChartData.payload.data.result; } return []; } @@ -472,7 +468,10 @@ function LogsExplorerViews(): JSX.Element { return ( <> - + {stagedQuery && ( diff --git a/frontend/src/container/NewDashboard/DashboardSettings/DashboardSettings.styles.scss b/frontend/src/container/NewDashboard/DashboardSettings/DashboardSettings.styles.scss new file mode 100644 index 0000000000..d53e8c4070 --- /dev/null +++ b/frontend/src/container/NewDashboard/DashboardSettings/DashboardSettings.styles.scss @@ -0,0 +1,5 @@ +.delete-variable-name { + font-weight: 700; + color: rgb(207, 19, 34); + font-style: italic; +} diff --git a/frontend/src/container/NewDashboard/DashboardSettings/Variables/VariableItem/VariableItem.tsx b/frontend/src/container/NewDashboard/DashboardSettings/Variables/VariableItem/VariableItem.tsx index 76f0464bd2..a9b0aea09f 100644 --- a/frontend/src/container/NewDashboard/DashboardSettings/Variables/VariableItem/VariableItem.tsx +++ b/frontend/src/container/NewDashboard/DashboardSettings/Variables/VariableItem/VariableItem.tsx @@ -18,10 +18,10 @@ import { VariableQueryTypeArr, VariableSortTypeArr, } from 'types/api/dashboard/getAll'; -import { v4 } from 'uuid'; +import { v4 as generateUUID } from 'uuid'; import { variablePropsToPayloadVariables } from '../../../utils'; -import { TVariableViewMode } from '../types'; +import { TVariableMode } from '../types'; import { LabelContainer, VariableItemRow } from './styles'; const { Option } = Select; @@ -30,9 +30,9 @@ interface VariableItemProps { variableData: IDashboardVariable; existingVariables: Record; onCancel: () => void; - onSave: (name: string, arg0: IDashboardVariable, arg1: string) => void; + onSave: (mode: TVariableMode, variableData: IDashboardVariable) => void; validateName: (arg0: string) => boolean; - variableViewMode: TVariableViewMode; + mode: TVariableMode; } function VariableItem({ variableData, @@ -40,7 +40,7 @@ function VariableItem({ onCancel, onSave, validateName, - variableViewMode, + mode, }: VariableItemProps): JSX.Element { const [variableName, setVariableName] = useState( variableData.name || '', @@ -97,7 +97,7 @@ function VariableItem({ ]); const handleSave = (): void => { - const newVariableData: IDashboardVariable = { + const variable: IDashboardVariable = { name: variableName, description: variableDescription, type: queryType, @@ -111,16 +111,12 @@ function VariableItem({ selectedValue: (variableData.selectedValue || variableTextboxValue) as never, }), - modificationUUID: v4(), + modificationUUID: generateUUID(), + id: variableData.id || generateUUID(), + order: variableData.order, }; - onSave( - variableName, - newVariableData, - (variableViewMode === 'EDIT' && variableName !== variableData.name - ? variableData.name - : '') as string, - ); - onCancel(); + + onSave(mode, variable); }; // Fetches the preview values for the SQL variable query @@ -175,7 +171,6 @@ function VariableItem({ return (
- {/* Add Variable */} Name diff --git a/frontend/src/container/NewDashboard/DashboardSettings/Variables/index.tsx b/frontend/src/container/NewDashboard/DashboardSettings/Variables/index.tsx index de23e64068..4aa1bf9b22 100644 --- a/frontend/src/container/NewDashboard/DashboardSettings/Variables/index.tsx +++ b/frontend/src/container/NewDashboard/DashboardSettings/Variables/index.tsx @@ -1,20 +1,78 @@ +import '../DashboardSettings.styles.scss'; + import { blue, red } from '@ant-design/colors'; -import { PlusOutlined } from '@ant-design/icons'; -import { Button, Modal, Row, Space, Tag } from 'antd'; -import { ResizeTable } from 'components/ResizeTable'; +import { MenuOutlined, PlusOutlined } from '@ant-design/icons'; +import type { DragEndEvent, UniqueIdentifier } from '@dnd-kit/core'; +import { + DndContext, + PointerSensor, + useSensor, + useSensors, +} from '@dnd-kit/core'; +import { restrictToVerticalAxis } from '@dnd-kit/modifiers'; +import { arrayMove, SortableContext, useSortable } from '@dnd-kit/sortable'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { CSS } from '@dnd-kit/utilities'; +import { Button, Modal, Row, Space, Table, Typography } from 'antd'; +import { RowProps } from 'antd/lib'; +import { convertVariablesToDbFormat } from 'container/NewDashboard/DashboardVariablesSelection/util'; import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard'; import { useNotifications } from 'hooks/useNotifications'; import { PencilIcon, TrashIcon } from 'lucide-react'; import { useDashboard } from 'providers/Dashboard/Dashboard'; -import { useRef, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll'; -import { TVariableViewMode } from './types'; +import { TVariableMode } from './types'; import VariableItem from './VariableItem/VariableItem'; +function TableRow({ children, ...props }: RowProps): JSX.Element { + const { + attributes, + listeners, + setNodeRef, + setActivatorNodeRef, + transform, + transition, + isDragging, + } = useSortable({ + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + id: props['data-row-key'], + }); + + const style: React.CSSProperties = { + ...props.style, + transform: CSS.Transform.toString(transform && { ...transform, scaleY: 1 }), + transition, + ...(isDragging ? { position: 'relative', zIndex: 9999 } : {}), + }; + + return ( + // eslint-disable-next-line react/jsx-props-no-spreading + + {React.Children.map(children, (child) => { + if ((child as React.ReactElement).key === 'sort') { + return React.cloneElement(child as React.ReactElement, { + children: ( + + ), + }); + } + return child; + })} + + ); +} + function VariablesSetting(): JSX.Element { - const variableToDelete = useRef(null); + const variableToDelete = useRef(null); const [deleteVariableModal, setDeleteVariableModal] = useState(false); const { t } = useTranslation(['dashboard']); @@ -25,16 +83,15 @@ function VariablesSetting(): JSX.Element { const { variables = {} } = selectedDashboard?.data || {}; - const variablesTableData = Object.keys(variables).map((variableName) => ({ - key: variableName, - name: variableName, - ...variables[variableName], - })); + const [variablesTableData, setVariablesTableData] = useState([]); + const [variblesOrderArr, setVariablesOrderArr] = useState([]); + const [existingVariableNamesMap, setExistingVariableNamesMap] = useState< + Record + >({}); - const [ - variableViewMode, - setVariableViewMode, - ] = useState(null); + const [variableViewMode, setVariableViewMode] = useState( + null, + ); const [ variableEditData, @@ -47,7 +104,7 @@ function VariablesSetting(): JSX.Element { }; const onVariableViewModeEnter = ( - viewType: TVariableViewMode, + viewType: TVariableMode, varData: IDashboardVariable, ): void => { setVariableEditData(varData); @@ -56,6 +113,41 @@ function VariablesSetting(): JSX.Element { const updateMutation = useUpdateDashboard(); + useEffect(() => { + const tableRowData = []; + const variableOrderArr = []; + const variableNamesMap = {}; + + // eslint-disable-next-line no-restricted-syntax + for (const [key, value] of Object.entries(variables)) { + const { order, id, name } = value; + + tableRowData.push({ + key, + name: key, + ...variables[key], + id, + }); + + if (name) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + variableNamesMap[name] = name; + } + + if (order) { + variableOrderArr.push(order); + } + } + + tableRowData.sort((a, b) => a.order - b.order); + variableOrderArr.sort((a, b) => a - b); + + setVariablesTableData(tableRowData); + setVariablesOrderArr(variableOrderArr); + setExistingVariableNamesMap(variableNamesMap); + }, [variables]); + const updateVariables = ( updatedVariablesData: Dashboard['data']['variables'], ): void => { @@ -89,34 +181,58 @@ function VariablesSetting(): JSX.Element { ); }; + const getVariableOrder = (): number => { + if (variblesOrderArr && variblesOrderArr.length > 0) { + return variblesOrderArr[variblesOrderArr.length - 1] + 1; + } + + return 0; + }; + const onVariableSaveHandler = ( - name: string, + mode: TVariableMode, variableData: IDashboardVariable, - oldName: string, ): void => { - if (!variableData.name) { - return; + const updatedVariableData = { + ...variableData, + order: variableData?.order >= 0 ? variableData.order : getVariableOrder(), + }; + + const newVariablesArr = variablesTableData.map( + (variable: IDashboardVariable) => { + if (variable.id === updatedVariableData.id) { + return updatedVariableData; + } + + return variable; + }, + ); + + if (mode === 'ADD') { + newVariablesArr.push(updatedVariableData); } - const newVariables = { ...variables }; - newVariables[name] = variableData; + const variables = convertVariablesToDbFormat(newVariablesArr); - if (oldName) { - delete newVariables[oldName]; - } - updateVariables(newVariables); + setVariablesTableData(newVariablesArr); + updateVariables(variables); onDoneVariableViewMode(); }; - const onVariableDeleteHandler = (variableName: string): void => { - variableToDelete.current = variableName; + const onVariableDeleteHandler = (variable: IDashboardVariable): void => { + variableToDelete.current = variable; setDeleteVariableModal(true); }; const handleDeleteConfirm = (): void => { - const newVariables = { ...variables }; - if (variableToDelete?.current) delete newVariables[variableToDelete?.current]; - updateVariables(newVariables); + const newVariablesArr = variablesTableData.filter( + (variable: IDashboardVariable) => + variable.id !== variableToDelete?.current?.id, + ); + + const updatedVariables = convertVariablesToDbFormat(newVariablesArr); + + updateVariables(updatedVariables); variableToDelete.current = null; setDeleteVariableModal(false); }; @@ -125,31 +241,36 @@ function VariablesSetting(): JSX.Element { setDeleteVariableModal(false); }; - const validateVariableName = (name: string): boolean => !variables[name]; + const validateVariableName = (name: string): boolean => + !existingVariableNamesMap[name]; const columns = [ + { + key: 'sort', + width: '10%', + }, { title: 'Variable', dataIndex: 'name', - width: 100, + width: '40%', key: 'name', }, { title: 'Description', dataIndex: 'description', - width: 100, + width: '35%', key: 'description', }, { title: 'Actions', - width: 50, + width: '15%', key: 'action', - render: (_: IDashboardVariable): JSX.Element => ( + render: (variable: IDashboardVariable): JSX.Element => ( @@ -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} /> ) : ( <> - +