diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index f5f5e5610a..1d8d4e7b70 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install dependencies run: cd frontend && yarn install - name: Run ESLint @@ -31,7 +31,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Create .env file run: | echo 'INTERCOM_APP_ID="${{ secrets.INTERCOM_APP_ID }}"' > frontend/.env @@ -54,12 +54,12 @@ jobs: build-query-service: runs-on: ubuntu-latest steps: + - name: Checkout code + uses: actions/checkout@v4 - name: Setup golang uses: actions/setup-go@v4 with: go-version: "1.21" - - name: Checkout code - uses: actions/checkout@v3 - name: Run tests shell: bash run: | @@ -72,12 +72,12 @@ jobs: build-ee-query-service: runs-on: ubuntu-latest steps: + - name: Checkout code + uses: actions/checkout@v4 - name: Setup golang uses: actions/setup-go@v4 with: go-version: "1.21" - - name: Checkout code - uses: actions/checkout@v3 - name: Build EE query-service image shell: bash run: | diff --git a/.github/workflows/codeql.yaml b/.github/workflows/codeql.yaml index 14a0c127aa..be02f3bb82 100644 --- a/.github/workflows/codeql.yaml +++ b/.github/workflows/codeql.yaml @@ -39,7 +39,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/commitlint.yml b/.github/workflows/commitlint.yml index b624a90b9f..3a38338cf0 100644 --- a/.github/workflows/commitlint.yml +++ b/.github/workflows/commitlint.yml @@ -7,7 +7,7 @@ jobs: lint-commits: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: wagoid/commitlint-github-action@v5 diff --git a/.github/workflows/create-issue-on-pr-merge.yml b/.github/workflows/create-issue-on-pr-merge.yml index 2b0c849ffa..2a79618d12 100644 --- a/.github/workflows/create-issue-on-pr-merge.yml +++ b/.github/workflows/create-issue-on-pr-merge.yml @@ -12,11 +12,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Codebase - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: signoz/gh-bot - name: Use Node v16 - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 16 - name: Setup Cache & Install Dependencies diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 053a8733dc..be454590f3 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - name: 'Checkout Repository' - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: 'Dependency Review' with: fail-on-severity: high diff --git a/.github/workflows/e2e-k3s.yaml b/.github/workflows/e2e-k3s.yaml index 71061bfc73..770a2f4df3 100644 --- a/.github/workflows/e2e-k3s.yaml +++ b/.github/workflows/e2e-k3s.yaml @@ -13,7 +13,7 @@ jobs: DOCKER_TAG: pull-${{ github.event.number }} steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Build query-service image env: diff --git a/.github/workflows/playwright.yaml b/.github/workflows/playwright.yaml index d6c05dfd6f..9ad3ef4313 100644 --- a/.github/workflows/playwright.yaml +++ b/.github/workflows/playwright.yaml @@ -9,8 +9,8 @@ jobs: timeout-minutes: 60 runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: "16.x" - name: Install dependencies diff --git a/.github/workflows/push.yaml b/.github/workflows/push.yaml index 98ee1e0fc4..f8eb005883 100644 --- a/.github/workflows/push.yaml +++ b/.github/workflows/push.yaml @@ -14,7 +14,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 + - name: Setup golang + uses: actions/setup-go@v4 + with: + go-version: "1.21" - name: Set up QEMU uses: docker/setup-qemu-action@v2 - name: Set up Docker Buildx @@ -42,6 +46,11 @@ jobs: else echo "DOCKER_TAG=${{ steps.branch-name.outputs.current_branch }}-oss" >> $GITHUB_ENV fi + - name: Install cross-compilation tools + run: | + set -ex + sudo apt-get update + sudo apt-get install -y gcc-aarch64-linux-gnu musl-tools - name: Build and push docker image run: make build-push-query-service @@ -49,7 +58,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 + - name: Setup golang + uses: actions/setup-go@v4 + with: + go-version: "1.21" - name: Set up QEMU uses: docker/setup-qemu-action@v2 - name: Set up Docker Buildx @@ -77,6 +90,11 @@ jobs: else echo "DOCKER_TAG=${{ steps.branch-name.outputs.current_branch }}" >> $GITHUB_ENV fi + - name: Install cross-compilation tools + run: | + set -ex + sudo apt-get update + sudo apt-get install -y gcc-aarch64-linux-gnu musl-tools - name: Build and push docker image run: make build-push-ee-query-service @@ -84,7 +102,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install dependencies working-directory: frontend run: yarn install @@ -128,7 +146,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Create .env file run: | echo 'INTERCOM_APP_ID="${{ secrets.INTERCOM_APP_ID }}"' > frontend/.env diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml index 742768525f..8c62c12d1b 100644 --- a/.github/workflows/sonar.yml +++ b/.github/workflows/sonar.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 - name: Sonar analysis diff --git a/.github/workflows/staging-deployment.yaml b/.github/workflows/staging-deployment.yaml index 6de51f4733..21ea7a3c75 100644 --- a/.github/workflows/staging-deployment.yaml +++ b/.github/workflows/staging-deployment.yaml @@ -26,6 +26,7 @@ jobs: echo "GITHUB_SHA: ${GITHUB_SHA}" export DOCKER_TAG="${GITHUB_SHA:0:7}" # needed for child process to access it export OTELCOL_TAG="main" + export PATH="/usr/local/go/bin/:$PATH" # needed for Golang to work docker system prune --force docker pull signoz/signoz-otel-collector:main cd ~/signoz diff --git a/.github/workflows/testing-deployment.yaml b/.github/workflows/testing-deployment.yaml index d65a4e8bbc..799222ee3e 100644 --- a/.github/workflows/testing-deployment.yaml +++ b/.github/workflows/testing-deployment.yaml @@ -26,6 +26,7 @@ jobs: echo "GITHUB_SHA: ${GITHUB_SHA}" export DOCKER_TAG="${GITHUB_SHA:0:7}" # needed for child process to access it export DEV_BUILD="1" + export PATH="/usr/local/go/bin/:$PATH" # needed for Golang to work docker system prune --force cd ~/signoz git status diff --git a/Makefile b/Makefile index 7d976341c1..5213c4597a 100644 --- a/Makefile +++ b/Makefile @@ -8,6 +8,7 @@ BUILD_HASH ?= $(shell git rev-parse --short HEAD) BUILD_TIME ?= $(shell date -u +"%Y-%m-%dT%H:%M:%SZ") BUILD_BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD) DEV_LICENSE_SIGNOZ_IO ?= https://staging-license.signoz.io/api/v1 +DEV_BUILD ?= "" # set to any non-empty value to enable dev build # Internal variables or constants. FRONTEND_DIRECTORY ?= frontend @@ -15,15 +16,15 @@ QUERY_SERVICE_DIRECTORY ?= pkg/query-service EE_QUERY_SERVICE_DIRECTORY ?= ee/query-service STANDALONE_DIRECTORY ?= deploy/docker/clickhouse-setup SWARM_DIRECTORY ?= deploy/docker-swarm/clickhouse-setup -LOCAL_GOOS ?= $(shell go env GOOS) -LOCAL_GOARCH ?= $(shell go env GOARCH) + +GOOS ?= $(shell go env GOOS) +GOARCH ?= $(shell go env GOARCH) +GOPATH ?= $(shell go env GOPATH) REPONAME ?= signoz DOCKER_TAG ?= $(subst v,,$(BUILD_VERSION)) - FRONTEND_DOCKER_IMAGE ?= frontend QUERY_SERVICE_DOCKER_IMAGE ?= query-service -DEV_BUILD ?= "" # Build-time Go variables PACKAGE?=go.signoz.io/signoz @@ -37,10 +38,22 @@ LD_FLAGS=-X ${buildHash}=${BUILD_HASH} -X ${buildTime}=${BUILD_TIME} -X ${buildV DEV_LD_FLAGS=-X ${licenseSignozIo}=${DEV_LICENSE_SIGNOZ_IO} all: build-push-frontend build-push-query-service + +# Steps to build static files of frontend +build-frontend-static: + @echo "------------------" + @echo "--> Building frontend static files" + @echo "------------------" + @cd $(FRONTEND_DIRECTORY) && \ + rm -rf build && \ + CI=1 yarn install && \ + yarn build && \ + ls -l build + # Steps to build and push docker image of frontend .PHONY: build-frontend-amd64 build-push-frontend # Step to build docker image of frontend in amd64 (used in build pipeline) -build-frontend-amd64: +build-frontend-amd64: build-frontend-static @echo "------------------" @echo "--> Building frontend docker image for amd64" @echo "------------------" @@ -49,7 +62,7 @@ build-frontend-amd64: --build-arg TARGETPLATFORM="linux/amd64" . # Step to build and push docker image of frontend(used in push pipeline) -build-push-frontend: +build-push-frontend: build-frontend-static @echo "------------------" @echo "--> Building and pushing frontend docker image" @echo "------------------" @@ -57,24 +70,52 @@ build-push-frontend: docker buildx build --file Dockerfile --progress plain --push --platform linux/arm64,linux/amd64 \ --tag $(REPONAME)/$(FRONTEND_DOCKER_IMAGE):$(DOCKER_TAG) . +# Steps to build static binary of query service +.PHONY: build-query-service-static +build-query-service-static: + @echo "------------------" + @echo "--> Building query-service static binary" + @echo "------------------" + @if [ $(DEV_BUILD) != "" ]; then \ + cd $(QUERY_SERVICE_DIRECTORY) && \ + CGO_ENABLED=1 go build -tags timetzdata -a -o ./bin/query-service-${GOOS}-${GOARCH} \ + -ldflags "-linkmode external -extldflags '-static' -s -w ${LD_FLAGS} ${DEV_LD_FLAGS}"; \ + else \ + cd $(QUERY_SERVICE_DIRECTORY) && \ + CGO_ENABLED=1 go build -tags timetzdata -a -o ./bin/query-service-${GOOS}-${GOARCH} \ + -ldflags "-linkmode external -extldflags '-static' -s -w ${LD_FLAGS}"; \ + fi + +.PHONY: build-query-service-static-amd64 +build-query-service-static-amd64: + make GOARCH=amd64 build-query-service-static + +.PHONY: build-query-service-static-arm64 +build-query-service-static-arm64: + make CC=aarch64-linux-gnu-gcc GOARCH=arm64 build-query-service-static + +# Steps to build static binary of query service for all platforms +.PHONY: build-query-service-static-all +build-query-service-static-all: build-query-service-static-amd64 build-query-service-static-arm64 + # Steps to build and push docker image of query service -.PHONY: build-query-service-amd64 build-push-query-service +.PHONY: build-query-service-amd64 build-push-query-service # Step to build docker image of query service in amd64 (used in build pipeline) -build-query-service-amd64: +build-query-service-amd64: build-query-service-static-amd64 @echo "------------------" @echo "--> Building query-service docker image for amd64" @echo "------------------" @docker build --file $(QUERY_SERVICE_DIRECTORY)/Dockerfile \ - -t $(REPONAME)/$(QUERY_SERVICE_DOCKER_IMAGE):$(DOCKER_TAG) \ - --build-arg TARGETPLATFORM="linux/amd64" --build-arg LD_FLAGS="$(LD_FLAGS)" . + --tag $(REPONAME)/$(QUERY_SERVICE_DOCKER_IMAGE):$(DOCKER_TAG) \ + --build-arg TARGETPLATFORM="linux/amd64" . # Step to build and push docker image of query in amd64 and arm64 (used in push pipeline) -build-push-query-service: +build-push-query-service: build-query-service-static-all @echo "------------------" @echo "--> Building and pushing query-service docker image" @echo "------------------" @docker buildx build --file $(QUERY_SERVICE_DIRECTORY)/Dockerfile --progress plain \ - --push --platform linux/arm64,linux/amd64 --build-arg LD_FLAGS="$(LD_FLAGS)" \ + --push --platform linux/arm64,linux/amd64 \ --tag $(REPONAME)/$(QUERY_SERVICE_DOCKER_IMAGE):$(DOCKER_TAG) . # Step to build EE docker image of query service in amd64 (used in build pipeline) @@ -82,24 +123,14 @@ build-ee-query-service-amd64: @echo "------------------" @echo "--> Building query-service docker image for amd64" @echo "------------------" - @if [ $(DEV_BUILD) != "" ]; then \ - docker build --file $(EE_QUERY_SERVICE_DIRECTORY)/Dockerfile \ - -t $(REPONAME)/$(QUERY_SERVICE_DOCKER_IMAGE):$(DOCKER_TAG) \ - --build-arg TARGETPLATFORM="linux/amd64" --build-arg LD_FLAGS="${LD_FLAGS} ${DEV_LD_FLAGS}" .; \ - else \ - docker build --file $(EE_QUERY_SERVICE_DIRECTORY)/Dockerfile \ - -t $(REPONAME)/$(QUERY_SERVICE_DOCKER_IMAGE):$(DOCKER_TAG) \ - --build-arg TARGETPLATFORM="linux/amd64" --build-arg LD_FLAGS="$(LD_FLAGS)" .; \ - fi + make QUERY_SERVICE_DIRECTORY=${EE_QUERY_SERVICE_DIRECTORY} build-query-service-amd64 # Step to build and push EE docker image of query in amd64 and arm64 (used in push pipeline) build-push-ee-query-service: @echo "------------------" @echo "--> Building and pushing query-service docker image" @echo "------------------" - @docker buildx build --file $(EE_QUERY_SERVICE_DIRECTORY)/Dockerfile \ - --progress plain --push --platform linux/arm64,linux/amd64 \ - --build-arg LD_FLAGS="$(LD_FLAGS)" --tag $(REPONAME)/$(QUERY_SERVICE_DOCKER_IMAGE):$(DOCKER_TAG) . + make QUERY_SERVICE_DIRECTORY=${EE_QUERY_SERVICE_DIRECTORY} build-push-query-service dev-setup: mkdir -p /var/lib/signoz @@ -110,7 +141,7 @@ dev-setup: @echo "------------------" run-local: - @LOCAL_GOOS=$(LOCAL_GOOS) LOCAL_GOARCH=$(LOCAL_GOARCH) docker-compose -f \ + @docker-compose -f \ $(STANDALONE_DIRECTORY)/docker-compose-core.yaml -f $(STANDALONE_DIRECTORY)/docker-compose-local.yaml \ up --build -d @@ -153,3 +184,4 @@ test: go test ./pkg/query-service/formatter/... go test ./pkg/query-service/tests/integration/... go test ./pkg/query-service/rules/... + go test ./pkg/query-service/collectorsimulator/... diff --git a/README.zh-cn.md b/README.zh-cn.md index aaa89551bf..32b6328fcb 100644 --- a/README.zh-cn.md +++ b/README.zh-cn.md @@ -1,170 +1,225 @@ -
-
+
-
监视你的应用,并可排查已部署应用中的问题,这是一个开源的可替代DataDog、NewRelic的方案
+监控你的应用,并且可排查已部署应用的问题,这是一个可替代 DataDog、NewRelic 的开源方案
-## + 
+### 使用 Helm 在 Kubernetes 部署 -### 使用Helm在Kubernetes上部署 - -请跟着[这里](https://signoz.io/docs/deployment/helm_chart)的步骤使用helm charts安装 +请一步步跟随 [这里](https://signoz.io/docs/deployment/helm_chart) 通过 helm 来安装 
### SigNoz vs Jaeger -Jaeger只做分布式追踪(distributed tracing),SigNoz则支持metrics,traces,logs ,即可视化的三大支柱。 +Jaeger 仅仅是一个分布式追踪系统。 但是 SigNoz 可以提供 metrics, traces 和 logs 所有的观测。 -并且SigNoz有一些Jaeger没有的高级功能: +而且, SigNoz 相较于 Jaeger 拥有更对的高级功能: -- Jaegar UI无法在traces或过滤的traces上展示metrics。 -- Jaeger不能对过滤的traces做聚合操作。例如,拥有tag为customer_type='premium'的所有请求的p99延迟。而这个功能在SigNoz这儿是很容易实现。 +- Jaegar UI 不能提供任何基于 traces 的 metrics 查询和过滤。 + +- Jaeger 不能针对过滤的 traces 做聚合。 比如, p99 延迟的请求有个标签是 customer_type='premium'。 而这些在 SigNoz 可以轻松做到。 + + 
+ +### SigNoz vs Elastic + +- SigNoz 的日志管理是基于 ClickHouse 实现的,可以使日志的聚合更加高效,因为它是基于 OLAP 的数据仓储。 + +- 与 Elastic 相比,可以节省 50% 的资源成本 + +我们已经公布了 Elastic 和 SigNoz 的性能对比。 请点击 [这里](https://signoz.io/blog/logs-performance-benchmark/?utm_source=github-readme&utm_medium=logs-benchmark) + + 
+ +### SigNoz vs Loki + +- SigNoz 支持大容量高基数的聚合,但是 loki 是不支持的。 + +- SigNoz 支持索引的高基数查询,并且对索引没有数量限制,而 Loki 会在添加部分索引后到达最大上限。 + +- 相较于 SigNoz,Loki 在搜索大量数据下既困难又缓慢。 + +我们已经发布了基准测试对比 Loki 和 SigNoz 性能。请点击 [这里](https://signoz.io/blog/logs-performance-benchmark/?utm_source=github-readme&utm_medium=logs-benchmark)@@ -40,4 +48,54 @@ function Code({ ); } -export { Code, Pre }; +function Link({ href, children }: LinkProps): JSX.Element { + return ( + + {children} + + ); +} + +const interpolateMarkdown = ( + markdownContent: any, + variables: { [s: string]: unknown } | ArrayLike, +) => { + let interpolatedContent = markdownContent; + + const variableEntries = Object.entries(variables); + + // Loop through variables and replace placeholders with values + for (const [key, value] of variableEntries) { + const placeholder = `{{${key}}}`; + const regex = new RegExp(placeholder, 'g'); + interpolatedContent = interpolatedContent.replace(regex, value); + } + + return interpolatedContent; +}; + +function MarkdownRenderer({ + markdownContent, + variables, +}: { + markdownContent: any; + variables: any; +}): JSX.Element { + const interpolatedMarkdown = interpolateMarkdown(markdownContent, variables); + + return ( + + {interpolatedMarkdown} + + ); +} + +export { Code, Link, MarkdownRenderer, Pre }; diff --git a/frontend/src/components/TextToolTip/TextToolTip.style.scss b/frontend/src/components/TextToolTip/TextToolTip.style.scss new file mode 100644 index 0000000000..192d98264c --- /dev/null +++ b/frontend/src/components/TextToolTip/TextToolTip.style.scss @@ -0,0 +1,3 @@ +.overlay--text-wrap { + white-space: pre-wrap; +} \ No newline at end of file diff --git a/frontend/src/components/TextToolTip/TextToolTip.tsx b/frontend/src/components/TextToolTip/TextToolTip.tsx new file mode 100644 index 0000000000..6c8fad783e --- /dev/null +++ b/frontend/src/components/TextToolTip/TextToolTip.tsx @@ -0,0 +1,89 @@ +import './TextToolTip.style.scss'; + +import { blue, grey } from '@ant-design/colors'; +import { + QuestionCircleFilled, + QuestionCircleOutlined, +} from '@ant-design/icons'; +import { Tooltip } from 'antd'; +import { themeColors } from 'constants/theme'; +import { useIsDarkMode } from 'hooks/useDarkMode'; +import { useMemo } from 'react'; +import { popupContainer } from 'utils/selectPopupContainer'; + +import { style } from './constant'; + +function TextToolTip({ + text, + url, + useFilledIcon = true, + urlText, +}: TextToolTipProps): JSX.Element { + const isDarkMode = useIsDarkMode(); + + const onClickHandler = ( + event: React.MouseEvent, + ): void => { + event.stopPropagation(); + }; + + const overlay = useMemo( + () => ( + + {`${text} `} + {url && ( + + {urlText || 'here'} + + )} ++ ), + [text, url, urlText], + ); + + const iconStyle = useMemo( + () => ({ + ...style, + color: isDarkMode ? themeColors.whiteCream : grey[0], + }), + [isDarkMode], + ); + + const iconOutlinedStyle = useMemo( + () => ({ + ...style, + color: isDarkMode ? themeColors.navyBlue : blue[6], + }), + [isDarkMode], + ); + + return ( ++ {useFilledIcon ? ( + + ); +} + +TextToolTip.defaultProps = { + url: '', + urlText: '', + useFilledIcon: true, +}; +interface TextToolTipProps { + url?: string; + text: string; + useFilledIcon?: boolean; + urlText?: string; +} + +export default TextToolTip; diff --git a/frontend/src/components/TextToolTip/styles.ts b/frontend/src/components/TextToolTip/constant.ts similarity index 100% rename from frontend/src/components/TextToolTip/styles.ts rename to frontend/src/components/TextToolTip/constant.ts diff --git a/frontend/src/components/TextToolTip/index.tsx b/frontend/src/components/TextToolTip/index.tsx index 72f631a872..c40a841fd0 100644 --- a/frontend/src/components/TextToolTip/index.tsx +++ b/frontend/src/components/TextToolTip/index.tsx @@ -1,87 +1,3 @@ -import { blue, grey } from '@ant-design/colors'; -import { - QuestionCircleFilled, - QuestionCircleOutlined, -} from '@ant-design/icons'; -import { Tooltip } from 'antd'; -import { themeColors } from 'constants/theme'; -import { useIsDarkMode } from 'hooks/useDarkMode'; -import { useMemo } from 'react'; -import { popupContainer } from 'utils/selectPopupContainer'; - -import { style } from './styles'; - -function TextToolTip({ - text, - url, - useFilledIcon = true, - urlText, -}: TextToolTipProps): JSX.Element { - const isDarkMode = useIsDarkMode(); - - const onClickHandler = ( - event: React.MouseEvent+ ) : ( + + )} + , - ): void => { - event.stopPropagation(); - }; - - const overlay = useMemo( - () => ( - - {`${text} `} - {url && ( - - {urlText || 'here'} - - )} -- ), - [text, url, urlText], - ); - - const iconStyle = useMemo( - () => ({ - ...style, - color: isDarkMode ? themeColors.whiteCream : grey[0], - }), - [isDarkMode], - ); - - const iconOutlinedStyle = useMemo( - () => ({ - ...style, - color: isDarkMode ? themeColors.navyBlue : blue[6], - }), - [isDarkMode], - ); - - return ( -- {useFilledIcon ? ( - - ); -} - -TextToolTip.defaultProps = { - url: '', - urlText: '', - useFilledIcon: true, -}; -interface TextToolTipProps { - url?: string; - text: string; - useFilledIcon?: boolean; - urlText?: string; -} +import TextToolTip from './TextToolTip'; export default TextToolTip; diff --git a/frontend/src/constants/reactQueryKeys.ts b/frontend/src/constants/reactQueryKeys.ts index 1f984ebd46..ec55889516 100644 --- a/frontend/src/constants/reactQueryKeys.ts +++ b/frontend/src/constants/reactQueryKeys.ts @@ -3,5 +3,8 @@ export const REACT_QUERY_KEY = { GET_QUERY_RANGE: 'GET_QUERY_RANGE', GET_ALL_DASHBOARDS: 'GET_ALL_DASHBOARDS', GET_TRIGGERED_ALERTS: 'GET_TRIGGERED_ALERTS', + DASHBOARD_BY_ID: 'DASHBOARD_BY_ID', GET_FEATURES_FLAGS: 'GET_FEATURES_FLAGS', + DELETE_DASHBOARD: 'DELETE_DASHBOARD', + LOGS_PIPELINE_PREVIEW: 'LOGS_PIPELINE_PREVIEW', }; diff --git a/frontend/src/constants/routes.ts b/frontend/src/constants/routes.ts index c910d0d25e..b156036ce4 100644 --- a/frontend/src/constants/routes.ts +++ b/frontend/src/constants/routes.ts @@ -24,6 +24,7 @@ const ROUTES = { VERSION: '/status', MY_SETTINGS: '/my-settings', ORG_SETTINGS: '/settings/org-settings', + INGESTION_SETTINGS: '/settings/ingestion-settings', SOMETHING_WENT_WRONG: '/something-went-wrong', UN_AUTHORIZED: '/un-authorized', NOT_FOUND: '/not-found', diff --git a/frontend/src/container/CreateAlertRule/defaults.ts b/frontend/src/container/CreateAlertRule/defaults.ts index 2ac2f3a7b8..8517d9b18c 100644 --- a/frontend/src/container/CreateAlertRule/defaults.ts +++ b/frontend/src/container/CreateAlertRule/defaults.ts @@ -3,6 +3,7 @@ import { initialQueryPromQLData, PANEL_TYPES, } from 'constants/queryBuilder'; +import ROUTES from 'constants/routes'; import { AlertTypes } from 'types/api/alerts/alertTypes'; import { AlertDef, @@ -77,7 +78,7 @@ export const logAlertDefaults: AlertDef = { }, labels: { severity: 'warning', - details: `${window.location.protocol}//${window.location.host}/logs`, + details: `${window.location.protocol}//${window.location.host}${ROUTES.LOGS_EXPLORER}`, }, annotations: defaultAnnotations, evalWindow: defaultEvalWindow, diff --git a/frontend/src/container/FormAlertRules/ChartPreview/index.tsx b/frontend/src/container/FormAlertRules/ChartPreview/index.tsx index f6bf35cbd7..c00b78b449 100644 --- a/frontend/src/container/FormAlertRules/ChartPreview/index.tsx +++ b/frontend/src/container/FormAlertRules/ChartPreview/index.tsx @@ -25,6 +25,7 @@ export interface ChartPreviewProps { headline?: JSX.Element; alertDef?: AlertDef; userQueryKey?: string; + allowSelectedIntervalForStepGen?: boolean; } function ChartPreview({ @@ -35,6 +36,7 @@ function ChartPreview({ selectedInterval = '5min', headline, userQueryKey, + allowSelectedIntervalForStepGen = false, alertDef, }: ChartPreviewProps): JSX.Element | null { const { t } = useTranslation('alerts'); @@ -89,6 +91,9 @@ function ChartPreview({ globalSelectedInterval: selectedInterval, graphType, selectedTime, + params: { + allowSelectedIntervalForStepGen, + }, }, { queryKey: [ @@ -127,7 +132,7 @@ function ChartPreview({- ) : ( - - )} - ); + const updatedStagedQuery = useMemo((): Query | null => { + const newQuery: Query | null = stagedQuery; + if (newQuery) { + newQuery.builder.queryData[0].stepInterval = getUpdatedStepInterval( + alertDef.evalWindow, + ); + } + return newQuery; + }, [alertDef.evalWindow, stagedQuery]); + const renderQBChartPreview = (): JSX.Element => ( } name="" - query={stagedQuery} + query={updatedStagedQuery} selectedInterval={toChartInterval(alertDef.evalWindow)} alertDef={alertDef} + allowSelectedIntervalForStepGen /> ); diff --git a/frontend/src/container/FormAlertRules/utils.test.ts b/frontend/src/container/FormAlertRules/utils.test.ts new file mode 100644 index 0000000000..49acf94bc1 --- /dev/null +++ b/frontend/src/container/FormAlertRules/utils.test.ts @@ -0,0 +1,14 @@ +// Write a test for getUpdatedStepInterval function in src/container/FormAlertRules/utils.ts + +import { getUpdatedStepInterval } from './utils'; + +describe('getUpdatedStepInterval', () => { + it('should return 60', () => { + const result = getUpdatedStepInterval('5m0s'); + expect(result).toEqual(60); + }); + it('should return 60 for 10m0s', () => { + const result = getUpdatedStepInterval('10m0s'); + expect(result).toEqual(60); + }); +}); diff --git a/frontend/src/container/FormAlertRules/utils.ts b/frontend/src/container/FormAlertRules/utils.ts index 67042569a0..3734474c29 100644 --- a/frontend/src/container/FormAlertRules/utils.ts +++ b/frontend/src/container/FormAlertRules/utils.ts @@ -1,4 +1,6 @@ import { Time } from 'container/TopNav/DateTimeSelection/config'; +import getStartEndRangeTime from 'lib/getStartEndRangeTime'; +import getStep from 'lib/getStep'; // toChartInterval converts eval window to chart selection time interval export const toChartInterval = (evalWindow: string | undefined): Time => { @@ -21,3 +23,15 @@ export const toChartInterval = (evalWindow: string | undefined): Time => { return '5min'; } }; + +export const getUpdatedStepInterval = (evalWindow?: string): number => { + const { start, end } = getStartEndRangeTime({ + type: 'GLOBAL_TIME', + interval: toChartInterval(evalWindow), + }); + return getStep({ + start, + end, + inputFormat: 'ns', + }); +}; diff --git a/frontend/src/container/GridGraphLayout/EmptyWidget/index.tsx b/frontend/src/container/GridCardLayout/EmptyWidget/index.tsx similarity index 100% rename from frontend/src/container/GridGraphLayout/EmptyWidget/index.tsx rename to frontend/src/container/GridCardLayout/EmptyWidget/index.tsx diff --git a/frontend/src/container/GridGraphLayout/EmptyWidget/styles.ts b/frontend/src/container/GridCardLayout/EmptyWidget/styles.ts similarity index 100% rename from frontend/src/container/GridGraphLayout/EmptyWidget/styles.ts rename to frontend/src/container/GridCardLayout/EmptyWidget/styles.ts diff --git a/frontend/src/container/GridCardLayout/GridCard/FullView/GraphManager.styles.scss b/frontend/src/container/GridCardLayout/GridCard/FullView/GraphManager.styles.scss new file mode 100644 index 0000000000..2d594aa8a9 --- /dev/null +++ b/frontend/src/container/GridCardLayout/GridCard/FullView/GraphManager.styles.scss @@ -0,0 +1,21 @@ +.graph-manager-container { + margin-top: 1.25rem; + display: flex; + align-items: flex-end; + overflow-x: scroll; + + .filter-table-container { + flex-basis: 80%; + } + + .save-cancel-container { + flex-basis: 20%; + display: flex; + justify-content: flex-end; + } + + .save-cancel-button { + margin: 0 0.313rem; + } + +} \ No newline at end of file diff --git a/frontend/src/container/GridCardLayout/GridCard/FullView/GraphManager.tsx b/frontend/src/container/GridCardLayout/GridCard/FullView/GraphManager.tsx new file mode 100644 index 0000000000..e6c6e26c0b --- /dev/null +++ b/frontend/src/container/GridCardLayout/GridCard/FullView/GraphManager.tsx @@ -0,0 +1,136 @@ +import './GraphManager.styles.scss'; + +import { Button, Input } from 'antd'; +import { CheckboxChangeEvent } from 'antd/es/checkbox'; +import { ResizeTable } from 'components/ResizeTable'; +import { useNotifications } from 'hooks/useNotifications'; +import { memo, useCallback, useState } from 'react'; + +import { getGraphManagerTableColumns } from './TableRender/GraphManagerColumns'; +import { ExtendedChartDataset, GraphManagerProps } from './types'; +import { + getDefaultTableDataSet, + saveLegendEntriesToLocalStorage, +} from './utils'; + +function GraphManager({ + data, + name, + yAxisUnit, + onToggleModelHandler, + setGraphsVisibilityStates, + graphsVisibilityStates = [], + lineChartRef, + parentChartRef, +}: GraphManagerProps): JSX.Element { + const [tableDataSet, setTableDataSet] = useState ( + getDefaultTableDataSet(data), + ); + + const { notifications } = useNotifications(); + + const checkBoxOnChangeHandler = useCallback( + (e: CheckboxChangeEvent, index: number): void => { + const newStates = [...graphsVisibilityStates]; + + newStates[index] = e.target.checked; + + lineChartRef?.current?.toggleGraph(index, e.target.checked); + + setGraphsVisibilityStates([...newStates]); + }, + [graphsVisibilityStates, setGraphsVisibilityStates, lineChartRef], + ); + + const labelClickedHandler = useCallback( + (labelIndex: number): void => { + const newGraphVisibilityStates = Array (data.datasets.length).fill( + false, + ); + newGraphVisibilityStates[labelIndex] = true; + + newGraphVisibilityStates.forEach((state, index) => { + lineChartRef?.current?.toggleGraph(index, state); + parentChartRef?.current?.toggleGraph(index, state); + }); + setGraphsVisibilityStates(newGraphVisibilityStates); + }, + [ + data.datasets.length, + setGraphsVisibilityStates, + lineChartRef, + parentChartRef, + ], + ); + + const columns = getGraphManagerTableColumns({ + data, + checkBoxOnChangeHandler, + graphVisibilityState: graphsVisibilityStates || [], + labelClickedHandler, + yAxisUnit, + }); + + const filterHandler = useCallback( + (event: React.ChangeEvent ): void => { + const value = event.target.value.toString().toLowerCase(); + const updatedDataSet = tableDataSet.map((item) => { + if (item.label?.toLocaleLowerCase().includes(value)) { + return { ...item, show: true }; + } + return { ...item, show: false }; + }); + setTableDataSet(updatedDataSet); + }, + [tableDataSet], + ); + + const saveHandler = useCallback((): void => { + saveLegendEntriesToLocalStorage({ + data, + graphVisibilityState: graphsVisibilityStates || [], + name, + }); + notifications.success({ + message: 'The updated graphs & legends are saved', + }); + if (onToggleModelHandler) { + onToggleModelHandler(); + } + }, [data, graphsVisibilityStates, name, notifications, onToggleModelHandler]); + + const dataSource = tableDataSet.filter((item) => item.show); + + return ( + ++ ); +} + +GraphManager.defaultProps = { + graphVisibilityStateHandler: undefined, +}; + +export default memo(GraphManager); diff --git a/frontend/src/container/GridGraphLayout/Graph/FullView/TableRender/CustomCheckBox.tsx b/frontend/src/container/GridCardLayout/GridCard/FullView/TableRender/CustomCheckBox.tsx similarity index 59% rename from frontend/src/container/GridGraphLayout/Graph/FullView/TableRender/CustomCheckBox.tsx rename to frontend/src/container/GridCardLayout/GridCard/FullView/TableRender/CustomCheckBox.tsx index 22ae630bb8..eda971c1e4 100644 --- a/frontend/src/container/GridGraphLayout/Graph/FullView/TableRender/CustomCheckBox.tsx +++ b/frontend/src/container/GridCardLayout/GridCard/FullView/TableRender/CustomCheckBox.tsx @@ -1,3 +1,4 @@ +import { grey } from '@ant-design/colors'; import { Checkbox, ConfigProvider } from 'antd'; import { CheckboxChangeEvent } from 'antd/es/checkbox'; @@ -6,7 +7,7 @@ import { CheckBoxProps } from '../types'; function CustomCheckBox({ data, index, - graphVisibilityState, + graphVisibilityState = [], checkBoxOnChangeHandler, }: CheckBoxProps): JSX.Element { const { datasets } = data; @@ -15,17 +16,21 @@ function CustomCheckBox({ checkBoxOnChangeHandler(e, index); }; + const color = datasets[index]?.borderColor?.toString() || grey[0]; + + const isChecked = graphVisibilityState[index] || false; + return (+ +++ + + + + + + ++- ); } diff --git a/frontend/src/container/GridGraphLayout/Graph/FullView/TableRender/GetLabel.tsx b/frontend/src/container/GridCardLayout/GridCard/FullView/TableRender/GetLabel.tsx similarity index 100% rename from frontend/src/container/GridGraphLayout/Graph/FullView/TableRender/GetLabel.tsx rename to frontend/src/container/GridCardLayout/GridCard/FullView/TableRender/GetLabel.tsx diff --git a/frontend/src/container/GridGraphLayout/Graph/FullView/TableRender/GraphManagerColumns.ts b/frontend/src/container/GridCardLayout/GridCard/FullView/TableRender/GraphManagerColumns.tsx similarity index 87% rename from frontend/src/container/GridGraphLayout/Graph/FullView/TableRender/GraphManagerColumns.ts rename to frontend/src/container/GridCardLayout/GridCard/FullView/TableRender/GraphManagerColumns.tsx index cc10f83f00..3702a9b3e0 100644 --- a/frontend/src/container/GridGraphLayout/Graph/FullView/TableRender/GraphManagerColumns.ts +++ b/frontend/src/container/GridCardLayout/GridCard/FullView/TableRender/GraphManagerColumns.tsx @@ -5,7 +5,7 @@ import { ChartData } from 'chart.js'; import { ColumnsKeyAndDataIndex, ColumnsTitle } from '../contants'; import { DataSetProps } from '../types'; import { getGraphManagerTableHeaderTitle } from '../utils'; -import { getCheckBox } from './GetCheckBox'; +import CustomCheckBox from './CustomCheckBox'; import { getLabel } from './GetLabel'; export const getGraphManagerTableColumns = ({ @@ -20,11 +20,14 @@ export const getGraphManagerTableColumns = ({ width: 50, dataIndex: ColumnsKeyAndDataIndex.Index, key: ColumnsKeyAndDataIndex.Index, - ...getCheckBox({ - checkBoxOnChangeHandler, - graphVisibilityState, - data, - }), + render: (_: string, __: DataSetProps, index: number): JSX.Element => ( ++ + ), }, { title: ColumnsTitle[ColumnsKeyAndDataIndex.Label], diff --git a/frontend/src/container/GridGraphLayout/Graph/FullView/TableRender/Label.tsx b/frontend/src/container/GridCardLayout/GridCard/FullView/TableRender/Label.tsx similarity index 100% rename from frontend/src/container/GridGraphLayout/Graph/FullView/TableRender/Label.tsx rename to frontend/src/container/GridCardLayout/GridCard/FullView/TableRender/Label.tsx diff --git a/frontend/src/container/GridGraphLayout/Graph/FullView/contants.ts b/frontend/src/container/GridCardLayout/GridCard/FullView/contants.ts similarity index 100% rename from frontend/src/container/GridGraphLayout/Graph/FullView/contants.ts rename to frontend/src/container/GridCardLayout/GridCard/FullView/contants.ts diff --git a/frontend/src/container/GridGraphLayout/Graph/FullView/index.tsx b/frontend/src/container/GridCardLayout/GridCard/FullView/index.tsx similarity index 80% rename from frontend/src/container/GridGraphLayout/Graph/FullView/index.tsx rename to frontend/src/container/GridCardLayout/GridCard/FullView/index.tsx index 1437a29157..02391bdc0c 100644 --- a/frontend/src/container/GridGraphLayout/Graph/FullView/index.tsx +++ b/frontend/src/container/GridCardLayout/GridCard/FullView/index.tsx @@ -12,17 +12,16 @@ import { useStepInterval } from 'hooks/queryBuilder/useStepInterval'; import { useChartMutable } from 'hooks/useChartMutable'; import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables'; import getChartData from 'lib/getChartData'; +import { useDashboard } from 'providers/Dashboard/Dashboard'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useSelector } from 'react-redux'; import { AppState } from 'store/reducers'; import { GlobalReducer } from 'types/reducer/globalTime'; -import { toggleGraphsVisibilityInChart } from '../utils'; import { PANEL_TYPES_VS_FULL_VIEW_TABLE } from './contants'; import GraphManager from './GraphManager'; import { GraphContainer, TimeContainer } from './styles'; import { FullViewProps } from './types'; -import { getIsGraphLegendToggleAvailable } from './utils'; function FullView({ widget, @@ -34,45 +33,29 @@ function FullView({ isDependedDataLoaded = false, graphsVisibilityStates, onToggleModelHandler, + setGraphsVisibilityStates, + parentChartRef, }: FullViewProps): JSX.Element { const { selectedTime: globalSelectedTime } = useSelector< AppState, GlobalReducer >((state) => state.globalTime); + const { selectedDashboard } = useDashboard(); + const getSelectedTime = useCallback( () => timeItems.find((e) => e.enum === (widget?.timePreferance || 'GLOBAL_TIME')), [widget], ); - const canModifyChart = useChartMutable({ - panelType: widget.panelTypes, - panelTypeAndGraphManagerVisibility: PANEL_TYPES_VS_FULL_VIEW_TABLE, - }); - const lineChartRef = useRef (); - useEffect(() => { - if (graphsVisibilityStates && canModifyChart && lineChartRef.current) { - toggleGraphsVisibilityInChart({ - graphsVisibilityStates, - lineChartRef, - }); - } - }, [graphsVisibilityStates, canModifyChart]); - const [selectedTime, setSelectedTime] = useState ({ name: getSelectedTime()?.name || '', enum: widget?.timePreferance || 'GLOBAL_TIME', }); - const queryKey = useMemo( - () => - `FullViewGetMetricsQueryRange-${selectedTime.enum}-${globalSelectedTime}-${widget.id}`, - [selectedTime, globalSelectedTime, widget], - ); - const updatedQuery = useStepInterval(widget?.query); const response = useGetQueryRange( @@ -81,14 +64,19 @@ function FullView({ graphType: widget.panelTypes, query: updatedQuery, globalSelectedInterval: globalSelectedTime, - variables: getDashboardVariables(), + variables: getDashboardVariables(selectedDashboard?.data.variables), }, { - queryKey, + queryKey: `FullViewGetMetricsQueryRange-${selectedTime.enum}-${globalSelectedTime}-${widget.id}`, enabled: !isDependedDataLoaded, }, ); + const canModifyChart = useChartMutable({ + panelType: widget.panelTypes, + panelTypeAndGraphManagerVisibility: PANEL_TYPES_VS_FULL_VIEW_TABLE, + }); + const chartDataSet = useMemo( () => getChartData({ @@ -101,9 +89,14 @@ function FullView({ [response], ); - const isGraphLegendToggleAvailable = getIsGraphLegendToggleAvailable( - widget.panelTypes, - ); + useEffect(() => { + if (!response.isFetching && lineChartRef.current) { + graphsVisibilityStates?.forEach((e, i) => { + lineChartRef?.current?.toggleGraph(i, e); + parentChartRef?.current?.toggleGraph(i, e); + }); + } + }, [graphsVisibilityStates, parentChartRef, response.isFetching]); if (response.isFetching) { return ; @@ -128,10 +121,10 @@ function FullView({ )} - + )} > diff --git a/frontend/src/container/GridGraphLayout/Graph/FullView/styles.ts b/frontend/src/container/GridCardLayout/GridCard/FullView/styles.ts similarity index 72% rename from frontend/src/container/GridGraphLayout/Graph/FullView/styles.ts rename to frontend/src/container/GridCardLayout/GridCard/FullView/styles.ts index 9e5bd09541..b73a2e9112 100644 --- a/frontend/src/container/GridGraphLayout/Graph/FullView/styles.ts +++ b/frontend/src/container/GridCardLayout/GridCard/FullView/styles.ts @@ -31,26 +31,6 @@ export const GraphContainer = styled.div ` isGraphLegendToggleAvailable ? '50%' : '100%'}; `; -export const FilterTableAndSaveContainer = styled.div` - margin-top: 1.875rem; - display: flex; - align-items: flex-end; -`; - -export const FilterTableContainer = styled.div` - flex-basis: 80%; -`; - -export const SaveContainer = styled.div` - flex-basis: 20%; - display: flex; - justify-content: flex-end; -`; - -export const SaveCancelButtonContainer = styled.span` - margin: 0 0.313rem; -`; - export const LabelContainer = styled.button` max-width: 18.75rem; cursor: pointer; diff --git a/frontend/src/container/GridGraphLayout/Graph/FullView/types.ts b/frontend/src/container/GridCardLayout/GridCard/FullView/types.ts similarity index 76% rename from frontend/src/container/GridGraphLayout/Graph/FullView/types.ts rename to frontend/src/container/GridCardLayout/GridCard/FullView/types.ts index 7d329e1399..ae686496e5 100644 --- a/frontend/src/container/GridGraphLayout/Graph/FullView/types.ts +++ b/frontend/src/container/GridCardLayout/GridCard/FullView/types.ts @@ -1,7 +1,8 @@ import { CheckboxChangeEvent } from 'antd/es/checkbox'; import { ChartData, ChartDataset } from 'chart.js'; -import { GraphOnClickHandler } from 'components/Graph/types'; +import { GraphOnClickHandler, ToggleGraphProps } from 'components/Graph/types'; import { PANEL_TYPES } from 'constants/queryBuilder'; +import { MutableRefObject } from 'react'; import { Widgets } from 'types/api/dashboard/getAll'; export interface DataSetProps { @@ -40,20 +41,6 @@ export interface LabelProps { label: string; } -export interface GraphManagerProps { - data: ChartData; - name: string; - yAxisUnit?: string; - onToggleModelHandler?: () => void; -} - -export interface CheckBoxProps { - data: ChartData; - index: number; - graphVisibilityState: boolean[]; - checkBoxOnChangeHandler: (e: CheckboxChangeEvent, index: number) => void; -} - export interface FullViewProps { widget: Widgets; fullViewOptions?: boolean; @@ -64,6 +51,26 @@ export interface FullViewProps { isDependedDataLoaded?: boolean; graphsVisibilityStates?: boolean[]; onToggleModelHandler?: GraphManagerProps['onToggleModelHandler']; + setGraphsVisibilityStates: (graphsVisibilityStates: boolean[]) => void; + parentChartRef: GraphManagerProps['lineChartRef']; +} + +export interface GraphManagerProps { + data: ChartData; + name: string; + yAxisUnit?: string; + onToggleModelHandler?: () => void; + setGraphsVisibilityStates: FullViewProps['setGraphsVisibilityStates']; + graphsVisibilityStates: FullViewProps['graphsVisibilityStates']; + lineChartRef?: MutableRefObject ; + parentChartRef?: MutableRefObject ; +} + +export interface CheckBoxProps { + data: ChartData; + index: number; + graphVisibilityState: boolean[]; + checkBoxOnChangeHandler: (e: CheckboxChangeEvent, index: number) => void; } export interface SaveLegendEntriesToLocalStoreProps { diff --git a/frontend/src/container/GridGraphLayout/Graph/FullView/utils.ts b/frontend/src/container/GridCardLayout/GridCard/FullView/utils.ts similarity index 94% rename from frontend/src/container/GridGraphLayout/Graph/FullView/utils.ts rename to frontend/src/container/GridCardLayout/GridCard/FullView/utils.ts index 256bc39050..b1ffb3a032 100644 --- a/frontend/src/container/GridGraphLayout/Graph/FullView/utils.ts +++ b/frontend/src/container/GridCardLayout/GridCard/FullView/utils.ts @@ -1,6 +1,5 @@ import { ChartData, ChartDataset } from 'chart.js'; import { LOCALSTORAGE } from 'constants/localStorage'; -import { PANEL_TYPES } from 'constants/queryBuilder'; import { ExtendedChartDataset, @@ -110,10 +109,6 @@ export const saveLegendEntriesToLocalStorage = ({ } }; -export const getIsGraphLegendToggleAvailable = ( - panelType: PANEL_TYPES, -): boolean => panelType === PANEL_TYPES.TIME_SERIES; - export const getGraphManagerTableHeaderTitle = ( title: string, yAxisUnit?: string, diff --git a/frontend/src/container/GridGraphLayout/Graph/Graph.test.tsx b/frontend/src/container/GridCardLayout/GridCard/Graph.test.tsx similarity index 100% rename from frontend/src/container/GridGraphLayout/Graph/Graph.test.tsx rename to frontend/src/container/GridCardLayout/GridCard/Graph.test.tsx diff --git a/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.tsx b/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.tsx new file mode 100644 index 0000000000..c011ca2471 --- /dev/null +++ b/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.tsx @@ -0,0 +1,277 @@ +import { Typography } from 'antd'; +import { ToggleGraphProps } from 'components/Graph/types'; +import { SOMETHING_WENT_WRONG } from 'constants/api'; +import GridPanelSwitch from 'container/GridPanelSwitch'; +import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard'; +import { useNotifications } from 'hooks/useNotifications'; +import createQueryParams from 'lib/createQueryParams'; +import history from 'lib/history'; +import { useDashboard } from 'providers/Dashboard/Dashboard'; +import { + Dispatch, + SetStateAction, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; +import { useSelector } from 'react-redux'; +import { useLocation } from 'react-router-dom'; +import { AppState } from 'store/reducers'; +import { Dashboard } from 'types/api/dashboard/getAll'; +import AppReducer from 'types/reducer/app'; +import { v4 } from 'uuid'; + +import WidgetHeader from '../WidgetHeader'; +import FullView from './FullView'; +import { FullViewContainer, Modal } from './styles'; +import { WidgetGraphComponentProps } from './types'; +import { getGraphVisibilityStateOnDataChange } from './utils'; + +function WidgetGraphComponent({ + data, + widget, + queryResponse, + errorMessage, + name, + onDragSelect, + onClickHandler, + threshold, + headerMenuList, + isWarning, +}: WidgetGraphComponentProps): JSX.Element { + const [deleteModal, setDeleteModal] = useState(false); + const [modal, setModal] = useState (false); + const [hovered, setHovered] = useState(false); + const { notifications } = useNotifications(); + const { pathname } = useLocation(); + + const lineChartRef = useRef (); + + const { graphVisibilityStates: localStoredVisibilityStates } = useMemo( + () => + getGraphVisibilityStateOnDataChange({ + data, + isExpandedName: true, + name, + }), + [data, name], + ); + + useEffect(() => { + if (!lineChartRef.current) return; + + localStoredVisibilityStates.forEach((state, index) => { + lineChartRef.current?.toggleGraph(index, state); + }); + }, [localStoredVisibilityStates]); + + const { setLayouts, selectedDashboard, setSelectedDashboard } = useDashboard(); + + const [graphsVisibilityStates, setGraphsVisibilityStates] = useState< + boolean[] + >(localStoredVisibilityStates); + + const { featureResponse } = useSelector ( + (state) => state.app, + ); + const onToggleModal = useCallback( + (func: Dispatch >) => { + func((value) => !value); + }, + [], + ); + + const updateDashboardMutation = useUpdateDashboard(); + + const onDeleteHandler = (): void => { + if (!selectedDashboard) return; + + const updatedWidgets = selectedDashboard?.data?.widgets?.filter( + (e) => e.id !== widget.id, + ); + + const updatedLayout = + selectedDashboard.data.layout?.filter((e) => e.i !== widget.id) || []; + + const updatedSelectedDashboard: Dashboard = { + ...selectedDashboard, + data: { + ...selectedDashboard.data, + widgets: updatedWidgets, + layout: updatedLayout, + }, + uuid: selectedDashboard.uuid, + }; + + updateDashboardMutation.mutateAsync(updatedSelectedDashboard, { + onSuccess: (updatedDashboard) => { + if (setLayouts) setLayouts(updatedDashboard.payload?.data?.layout || []); + if (setSelectedDashboard && updatedDashboard.payload) { + setSelectedDashboard(updatedDashboard.payload); + } + featureResponse.refetch(); + }, + onError: () => { + notifications.error({ + message: SOMETHING_WENT_WRONG, + }); + }, + }); + }; + + const onCloneHandler = async (): Promise => { + if (!selectedDashboard) return; + + const uuid = v4(); + + const layout = [ + ...(selectedDashboard.data.layout || []), + { + i: uuid, + w: 6, + x: 0, + h: 2, + y: 0, + }, + ]; + + updateDashboardMutation.mutateAsync( + { + ...selectedDashboard, + data: { + ...selectedDashboard.data, + layout, + widgets: [ + ...(selectedDashboard.data.widgets || []), + { + ...{ + ...widget, + id: uuid, + }, + }, + ], + }, + }, + { + onSuccess: () => { + notifications.success({ + message: 'Panel cloned successfully, redirecting to new copy.', + }); + const queryParams = { + graphType: widget?.panelTypes, + widgetId: uuid, + }; + history.push(`${pathname}/new?${createQueryParams(queryParams)}`); + }, + }, + ); + }; + + const handleOnView = (): void => { + onToggleModal(setModal); + }; + + const handleOnDelete = (): void => { + onToggleModal(setDeleteModal); + }; + + const onDeleteModelHandler = (): void => { + onToggleModal(setDeleteModal); + }; + + const onToggleModelHandler = (): void => { + onToggleModal(setModal); + }; + + return ( + { + setHovered(true); + }} + onFocus={(): void => { + setHovered(true); + }} + onMouseOut={(): void => { + setHovered(false); + }} + onBlur={(): void => { + setHovered(false); + }} + > + + + +Are you sure you want to delete this widget ++ + ++ ++ +++ + + ); +} + +WidgetGraphComponent.defaultProps = { + yAxisUnit: undefined, + setLayout: undefined, + onDragSelect: undefined, + onClickHandler: undefined, +}; + +export default WidgetGraphComponent; diff --git a/frontend/src/container/GridGraphLayout/Graph/__mock__/mockChartData.ts b/frontend/src/container/GridCardLayout/GridCard/__mock__/mockChartData.ts similarity index 100% rename from frontend/src/container/GridGraphLayout/Graph/__mock__/mockChartData.ts rename to frontend/src/container/GridCardLayout/GridCard/__mock__/mockChartData.ts diff --git a/frontend/src/container/GridGraphLayout/Graph/__mock__/mockLegendEntryData.ts b/frontend/src/container/GridCardLayout/GridCard/__mock__/mockLegendEntryData.ts similarity index 100% rename from frontend/src/container/GridGraphLayout/Graph/__mock__/mockLegendEntryData.ts rename to frontend/src/container/GridCardLayout/GridCard/__mock__/mockLegendEntryData.ts diff --git a/frontend/src/container/GridCardLayout/GridCard/index.tsx b/frontend/src/container/GridCardLayout/GridCard/index.tsx new file mode 100644 index 0000000000..598f4dd708 --- /dev/null +++ b/frontend/src/container/GridCardLayout/GridCard/index.tsx @@ -0,0 +1,134 @@ +import { Skeleton } from 'antd'; +import { PANEL_TYPES } from 'constants/queryBuilder'; +import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange'; +import { useStepInterval } from 'hooks/queryBuilder/useStepInterval'; +import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables'; +import getChartData from 'lib/getChartData'; +import isEmpty from 'lodash-es/isEmpty'; +import { useDashboard } from 'providers/Dashboard/Dashboard'; +import { memo, useMemo, useState } from 'react'; +import { useInView } from 'react-intersection-observer'; +import { useDispatch, useSelector } from 'react-redux'; +import { UpdateTimeInterval } from 'store/actions'; +import { AppState } from 'store/reducers'; +import { GlobalReducer } from 'types/reducer/globalTime'; + +import EmptyWidget from '../EmptyWidget'; +import { MenuItemKeys } from '../WidgetHeader/contants'; +import { GridCardGraphProps } from './types'; +import WidgetGraphComponent from './WidgetGraphComponent'; + +function GridCardGraph({ + widget, + name, + onClickHandler, + headerMenuList = [MenuItemKeys.View], + isQueryEnabled, + threshold, +}: GridCardGraphProps): JSX.Element { + const dispatch = useDispatch(); + const [errorMessage, setErrorMessage] = useState (); + + const onDragSelect = (start: number, end: number): void => { + const startTimestamp = Math.trunc(start); + const endTimestamp = Math.trunc(end); + + if (startTimestamp !== endTimestamp) { + dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp])); + } + }; + + const { ref: graphRef, inView: isGraphVisible } = useInView({ + threshold: 0, + triggerOnce: true, + initialInView: false, + }); + + const { selectedDashboard } = useDashboard(); + + 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 queryResponse = useGetQueryRange( + { + selectedTime: widget?.timePreferance, + graphType: widget?.panelTypes, + query: updatedQuery, + globalSelectedInterval, + variables: getDashboardVariables(selectedDashboard?.data.variables), + }, + { + queryKey: [ + maxTime, + minTime, + globalSelectedInterval, + selectedDashboard?.data?.variables, + widget?.query, + widget?.panelTypes, + widget.timePreferance, + ], + keepPreviousData: true, + enabled: isGraphVisible && !isEmptyWidget && isQueryEnabled, + refetchOnMount: false, + onError: (error) => { + setErrorMessage(error.message); + }, + }, + ); + + const chartData = useMemo( + () => + getChartData({ + queryData: [ + { + queryData: queryResponse?.data?.payload?.data?.result || [], + }, + ], + createDataset: undefined, + isWarningLimit: true, + }), + [queryResponse], + ); + + const isEmptyLayout = widget?.id === PANEL_TYPES.EMPTY_WIDGET; + + if (queryResponse.isLoading) { + return ; + } + + return ( + + + + {isEmptyLayout && } + + ); +} + +GridCardGraph.defaultProps = { + onDragSelect: undefined, + onClickHandler: undefined, + isQueryEnabled: true, + threshold: undefined, + headerMenuList: [MenuItemKeys.View], +}; + +export default memo(GridCardGraph); diff --git a/frontend/src/container/GridGraphLayout/Graph/styles.ts b/frontend/src/container/GridCardLayout/GridCard/styles.ts similarity index 100% rename from frontend/src/container/GridGraphLayout/Graph/styles.ts rename to frontend/src/container/GridCardLayout/GridCard/styles.ts diff --git a/frontend/src/container/GridGraphLayout/Graph/types.ts b/frontend/src/container/GridCardLayout/GridCard/types.ts similarity index 67% rename from frontend/src/container/GridGraphLayout/Graph/types.ts rename to frontend/src/container/GridCardLayout/GridCard/types.ts index 49b637a178..fccf488dc8 100644 --- a/frontend/src/container/GridGraphLayout/Graph/types.ts +++ b/frontend/src/container/GridCardLayout/GridCard/types.ts @@ -1,15 +1,11 @@ import { ChartData } from 'chart.js'; import { GraphOnClickHandler, ToggleGraphProps } from 'components/Graph/types'; -import { Dispatch, MutableRefObject, ReactNode, SetStateAction } from 'react'; -import { Layout } from 'react-grid-layout'; +import { MutableRefObject, ReactNode } from 'react'; import { UseQueryResult } from 'react-query'; -import { DeleteWidgetProps } from 'store/actions/dashboard/deleteWidget'; -import AppActions from 'types/actions'; import { ErrorResponse, SuccessResponse } from 'types/api'; import { Widgets } from 'types/api/dashboard/getAll'; import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange'; -import { LayoutProps } from '..'; import { MenuItemKeys } from '../WidgetHeader/contants'; import { LegendEntryProps } from './FullView/types'; @@ -18,15 +14,7 @@ export interface GraphVisibilityLegendEntryProps { legendEntry: LegendEntryProps[]; } -export interface DispatchProps { - deleteWidget: ({ - widgetId, - }: DeleteWidgetProps) => (dispatch: Dispatch ) => void; -} - -export interface WidgetGraphComponentProps extends DispatchProps { - enableModel: boolean; - enableWidgetHeader: boolean; +export interface WidgetGraphComponentProps { widget: Widgets; queryResponse: UseQueryResult< SuccessResponse | ErrorResponse @@ -34,21 +22,16 @@ export interface WidgetGraphComponentProps extends DispatchProps { errorMessage: string | undefined; data: ChartData; name: string; - yAxisUnit?: string; - layout?: Layout[]; - setLayout?: Dispatch >; onDragSelect?: (start: number, end: number) => void; onClickHandler?: GraphOnClickHandler; threshold?: ReactNode; headerMenuList: MenuItemKeys[]; + isWarning: boolean; } export interface GridCardGraphProps { widget: Widgets; name: string; - yAxisUnit: string | undefined; - layout?: Layout[]; - setLayout?: Dispatch >; onDragSelect?: (start: number, end: number) => void; onClickHandler?: GraphOnClickHandler; threshold?: ReactNode; diff --git a/frontend/src/container/GridGraphLayout/Graph/utils.ts b/frontend/src/container/GridCardLayout/GridCard/utils.ts similarity index 100% rename from frontend/src/container/GridGraphLayout/Graph/utils.ts rename to frontend/src/container/GridCardLayout/GridCard/utils.ts diff --git a/frontend/src/container/GridCardLayout/GridCardLayout.tsx b/frontend/src/container/GridCardLayout/GridCardLayout.tsx new file mode 100644 index 0000000000..b99f2396c9 --- /dev/null +++ b/frontend/src/container/GridCardLayout/GridCardLayout.tsx @@ -0,0 +1,141 @@ +import { PlusOutlined, SaveFilled } from '@ant-design/icons'; +import { SOMETHING_WENT_WRONG } from 'constants/api'; +import { PANEL_TYPES } from 'constants/queryBuilder'; +import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard'; +import useComponentPermission from 'hooks/useComponentPermission'; +import { useIsDarkMode } from 'hooks/useDarkMode'; +import { useNotifications } from 'hooks/useNotifications'; +import { useDashboard } from 'providers/Dashboard/Dashboard'; +import { useTranslation } from 'react-i18next'; +import { useSelector } from 'react-redux'; +import { AppState } from 'store/reducers'; +import { Dashboard, Widgets } from 'types/api/dashboard/getAll'; +import AppReducer from 'types/reducer/app'; + +import { headerMenuList } from './config'; +import GridCard from './GridCard'; +import { + Button, + ButtonContainer, + Card, + CardContainer, + ReactGridLayout, +} from './styles'; +import { GraphLayoutProps } from './types'; + +function GraphLayout({ + onAddPanelHandler, + widgets, +}: GraphLayoutProps): JSX.Element { + const { + selectedDashboard, + layouts, + setLayouts, + setSelectedDashboard, + } = useDashboard(); + const { t } = useTranslation(['dashboard']); + + const { featureResponse, role } = useSelector ( + (state) => state.app, + ); + + const isDarkMode = useIsDarkMode(); + + const updateDashboardMutation = useUpdateDashboard(); + + const { notifications } = useNotifications(); + + const [saveLayoutPermission, addPanelPermission] = useComponentPermission( + ['save_layout', 'add_panel'], + role, + ); + + const onSaveHandler = (): void => { + if (!selectedDashboard) return; + + const updatedDashboard: Dashboard = { + ...selectedDashboard, + data: { + ...selectedDashboard.data, + layout: layouts.filter((e) => e.i !== PANEL_TYPES.EMPTY_WIDGET), + }, + uuid: selectedDashboard.uuid, + }; + + updateDashboardMutation.mutate(updatedDashboard, { + onSuccess: (updatedDashboard) => { + if (updatedDashboard.payload) { + if (updatedDashboard.payload.data.layout) + setLayouts(updatedDashboard.payload.data.layout); + setSelectedDashboard(updatedDashboard.payload); + } + notifications.success({ + message: t('dashboard:layout_saved_successfully'), + }); + + featureResponse.refetch(); + }, + onError: () => { + notifications.error({ + message: SOMETHING_WENT_WRONG, + }); + }, + }); + }; + + return ( + <> + + {saveLayoutPermission && ( + } + disabled={updateDashboardMutation.isLoading} + > + {t('dashboard:save_layout')} + + )} + + {addPanelPermission && ( + }> + {t('dashboard:add_panel')} + + )} + + ++ {layouts.map((layout) => { + const { i: id } = layout; + const currentWidget = (widgets || [])?.find((e) => e.id === id); + + return ( + + > + ); +} + +export default GraphLayout; diff --git a/frontend/src/container/GridGraphLayout/WidgetHeader/DisplayThreshold.tsx b/frontend/src/container/GridCardLayout/WidgetHeader/DisplayThreshold.tsx similarity index 100% rename from frontend/src/container/GridGraphLayout/WidgetHeader/DisplayThreshold.tsx rename to frontend/src/container/GridCardLayout/WidgetHeader/DisplayThreshold.tsx diff --git a/frontend/src/container/GridGraphLayout/WidgetHeader/config.ts b/frontend/src/container/GridCardLayout/WidgetHeader/config.ts similarity index 57% rename from frontend/src/container/GridGraphLayout/WidgetHeader/config.ts rename to frontend/src/container/GridCardLayout/WidgetHeader/config.ts index 1e15697049..2a8f2d76ee 100644 --- a/frontend/src/container/GridGraphLayout/WidgetHeader/config.ts +++ b/frontend/src/container/GridCardLayout/WidgetHeader/config.ts @@ -1,9 +1,14 @@ import { themeColors } from 'constants/theme'; +import { limit } from 'lib/getChartData'; import { CSSProperties } from 'react'; const positionCss: CSSProperties['position'] = 'absolute'; -export const spinnerStyles = { position: positionCss, right: '0.5rem' }; +export const spinnerStyles = { + position: positionCss, + top: '0', + right: '0', +}; export const tooltipStyles = { fontSize: '1rem', top: '0.313rem', @@ -21,3 +26,5 @@ export const overlayStyles: CSSProperties = { justifyContent: 'center', position: 'absolute', }; + +export const WARNING_MESSAGE = `Too many timeseries in the result. UI has restricted to showing the top ${limit}. Please check the query if this is needed and contact support@signoz.io if you need to show >${limit} timeseries in the panel`; diff --git a/frontend/src/container/GridGraphLayout/WidgetHeader/contants.ts b/frontend/src/container/GridCardLayout/WidgetHeader/contants.ts similarity index 100% rename from frontend/src/container/GridGraphLayout/WidgetHeader/contants.ts rename to frontend/src/container/GridCardLayout/WidgetHeader/contants.ts diff --git a/frontend/src/container/GridGraphLayout/WidgetHeader/index.tsx b/frontend/src/container/GridCardLayout/WidgetHeader/index.tsx similarity index 92% rename from frontend/src/container/GridGraphLayout/WidgetHeader/index.tsx rename to frontend/src/container/GridCardLayout/WidgetHeader/index.tsx index 20ad222e55..a916534082 100644 --- a/frontend/src/container/GridGraphLayout/WidgetHeader/index.tsx +++ b/frontend/src/container/GridCardLayout/WidgetHeader/index.tsx @@ -5,10 +5,12 @@ import { EditFilled, ExclamationCircleOutlined, FullscreenOutlined, + WarningOutlined, } from '@ant-design/icons'; 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 useComponentPermission from 'hooks/useComponentPermission'; import history from 'lib/history'; @@ -27,6 +29,7 @@ import { overlayStyles, spinnerStyles, tooltipStyles, + WARNING_MESSAGE, } from './config'; import { MENUITEM_KEYS_VS_LABELS, MenuItemKeys } from './contants'; import { @@ -52,6 +55,7 @@ interface IWidgetHeaderProps { errorMessage: string | undefined; threshold?: ReactNode; headerMenuList?: MenuItemKeys[]; + isWarning: boolean; } function WidgetHeader({ @@ -65,7 +69,8 @@ function WidgetHeader({ errorMessage, threshold, headerMenuList, -}: IWidgetHeaderProps): JSX.Element { + isWarning, +}: IWidgetHeaderProps): JSX.Element | null { const [localHover, setLocalHover] = useState(false); const [isOpen, setIsOpen] = useState+ + ); + })} ++ ++ (false); @@ -126,7 +131,7 @@ function WidgetHeader({ icon: , label: MENUITEM_KEYS_VS_LABELS[MenuItemKeys.View], isVisible: headerMenuList?.includes(MenuItemKeys.View) || false, - disabled: queryResponse.isLoading, + disabled: queryResponse.isFetching, }, { key: MenuItemKeys.Edit, @@ -158,7 +163,7 @@ function WidgetHeader({ disabled: false, }, ], - [queryResponse.isLoading, headerMenuList, editWidget, deleteWidget], + [headerMenuList, queryResponse.isFetching, editWidget, deleteWidget], ); const updatedMenuList = useMemo(() => generateMenuList(actions), [actions]); @@ -175,6 +180,10 @@ function WidgetHeader({ [updatedMenuList, onMenuItemSelectHandler], ); + if (widget.id === PANEL_TYPES.EMPTY_WIDGET) { + return null; + } + return ( ); } diff --git a/frontend/src/container/GridGraphLayout/WidgetHeader/styles.ts b/frontend/src/container/GridCardLayout/WidgetHeader/styles.ts similarity index 100% rename from frontend/src/container/GridGraphLayout/WidgetHeader/styles.ts rename to frontend/src/container/GridCardLayout/WidgetHeader/styles.ts diff --git a/frontend/src/container/GridGraphLayout/WidgetHeader/types.ts b/frontend/src/container/GridCardLayout/WidgetHeader/types.ts similarity index 100% rename from frontend/src/container/GridGraphLayout/WidgetHeader/types.ts rename to frontend/src/container/GridCardLayout/WidgetHeader/types.ts diff --git a/frontend/src/container/GridGraphLayout/WidgetHeader/utils.ts b/frontend/src/container/GridCardLayout/WidgetHeader/utils.ts similarity index 100% rename from frontend/src/container/GridGraphLayout/WidgetHeader/utils.ts rename to frontend/src/container/GridCardLayout/WidgetHeader/utils.ts diff --git a/frontend/src/container/GridCardLayout/config.ts b/frontend/src/container/GridCardLayout/config.ts new file mode 100644 index 0000000000..3fa9e8e569 --- /dev/null +++ b/frontend/src/container/GridCardLayout/config.ts @@ -0,0 +1,17 @@ +import { PANEL_TYPES } from 'constants/queryBuilder'; +import { MenuItemKeys } from 'container/GridCardLayout/WidgetHeader/contants'; + +export const headerMenuList = [ + MenuItemKeys.View, + MenuItemKeys.Clone, + MenuItemKeys.Delete, + MenuItemKeys.Edit, +]; + +export const EMPTY_WIDGET_LAYOUT = { + i: PANEL_TYPES.EMPTY_WIDGET, + w: 6, + x: 0, + h: 2, + y: 0, +}; diff --git a/frontend/src/container/GridCardLayout/index.tsx b/frontend/src/container/GridCardLayout/index.tsx new file mode 100644 index 0000000000..e715d7d539 --- /dev/null +++ b/frontend/src/container/GridCardLayout/index.tsx @@ -0,0 +1,35 @@ +import { useDashboard } from 'providers/Dashboard/Dashboard'; +import { useCallback } from 'react'; +import { Layout } from 'react-grid-layout'; + +import { EMPTY_WIDGET_LAYOUT } from './config'; +import GraphLayoutContainer from './GridCardLayout'; + +function GridGraph(): JSX.Element { + const { + selectedDashboard, + setLayouts, + handleToggleDashboardSlider, + } = useDashboard(); + + const { data } = selectedDashboard || {}; + const { widgets } = data || {}; + + const onEmptyWidgetHandler = useCallback(() => { + handleToggleDashboardSlider(true); + + setLayouts((preLayout: Layout[]) => [ + EMPTY_WIDGET_LAYOUT, + ...(preLayout || []), + ]); + }, [handleToggleDashboardSlider, setLayouts]); + + return ( + + {threshold} {queryResponse.isFetching && !queryResponse.isError && (@@ -211,6 +221,12 @@ function WidgetHeader({ )} + + {isWarning && ( + + + )}+ + ); +} + +export default GridGraph; diff --git a/frontend/src/container/GridGraphLayout/styles.ts b/frontend/src/container/GridCardLayout/styles.ts similarity index 100% rename from frontend/src/container/GridGraphLayout/styles.ts rename to frontend/src/container/GridCardLayout/styles.ts diff --git a/frontend/src/container/GridCardLayout/types.ts b/frontend/src/container/GridCardLayout/types.ts new file mode 100644 index 0000000000..0d2b678af6 --- /dev/null +++ b/frontend/src/container/GridCardLayout/types.ts @@ -0,0 +1,6 @@ +import { Widgets } from 'types/api/dashboard/getAll'; + +export interface GraphLayoutProps { + onAddPanelHandler: VoidFunction; + widgets?: Widgets[]; +} diff --git a/frontend/src/container/GridGraphLayout/Graph/FullView/GraphManager.tsx b/frontend/src/container/GridGraphLayout/Graph/FullView/GraphManager.tsx deleted file mode 100644 index 61abbf2aa6..0000000000 --- a/frontend/src/container/GridGraphLayout/Graph/FullView/GraphManager.tsx +++ /dev/null @@ -1,207 +0,0 @@ -import { Button, Input } from 'antd'; -import { CheckboxChangeEvent } from 'antd/es/checkbox'; -import { ResizeTable } from 'components/ResizeTable'; -import { Events } from 'constants/events'; -import { useNotifications } from 'hooks/useNotifications'; -import isEqual from 'lodash-es/isEqual'; -import { memo, useCallback, useEffect, useMemo, useState } from 'react'; -import { eventEmitter } from 'utils/getEventEmitter'; - -import { getGraphVisibilityStateOnDataChange } from '../utils'; -import { - FilterTableAndSaveContainer, - FilterTableContainer, - SaveCancelButtonContainer, - SaveContainer, -} from './styles'; -import { getGraphManagerTableColumns } from './TableRender/GraphManagerColumns'; -import { ExtendedChartDataset, GraphManagerProps } from './types'; -import { - getDefaultTableDataSet, - saveLegendEntriesToLocalStorage, -} from './utils'; - -function GraphManager({ - data, - name, - yAxisUnit, - onToggleModelHandler, -}: GraphManagerProps): JSX.Element { - const { - graphVisibilityStates: localstoredVisibilityStates, - legendEntry, - } = useMemo( - () => - getGraphVisibilityStateOnDataChange({ - data, - isExpandedName: false, - name, - }), - [data, name], - ); - - const [graphVisibilityState, setGraphVisibilityState] = useState ( - localstoredVisibilityStates, - ); - - const [tableDataSet, setTableDataSet] = useState ( - getDefaultTableDataSet(data), - ); - - const { notifications } = useNotifications(); - - // useEffect for updating graph visibility state on data change - useEffect(() => { - const newGraphVisibilityStates = Array (data.datasets.length).fill( - true, - ); - data.datasets.forEach((dataset, i) => { - const index = legendEntry.findIndex( - (entry) => entry.label === dataset.label, - ); - if (index !== -1) { - newGraphVisibilityStates[i] = legendEntry[index].show; - } - }); - eventEmitter.emit(Events.UPDATE_GRAPH_VISIBILITY_STATE, { - name, - graphVisibilityStates: newGraphVisibilityStates, - }); - setGraphVisibilityState(newGraphVisibilityStates); - }, [data, name, legendEntry]); - - // useEffect for listening to events event graph legend is clicked - useEffect(() => { - const eventListener = eventEmitter.on( - Events.UPDATE_GRAPH_MANAGER_TABLE, - (data) => { - if (data.name === name) { - const newGraphVisibilityStates = graphVisibilityState; - newGraphVisibilityStates[data.index] = !newGraphVisibilityStates[ - data.index - ]; - eventEmitter.emit(Events.UPDATE_GRAPH_VISIBILITY_STATE, { - name, - graphVisibilityStates: newGraphVisibilityStates, - }); - setGraphVisibilityState([...newGraphVisibilityStates]); - } - }, - ); - return (): void => { - eventListener.off(Events.UPDATE_GRAPH_MANAGER_TABLE); - }; - }, [graphVisibilityState, name]); - - const checkBoxOnChangeHandler = useCallback( - (e: CheckboxChangeEvent, index: number): void => { - graphVisibilityState[index] = e.target.checked; - setGraphVisibilityState([...graphVisibilityState]); - eventEmitter.emit(Events.UPDATE_GRAPH_VISIBILITY_STATE, { - name, - graphVisibilityStates: [...graphVisibilityState], - }); - }, - [graphVisibilityState, name], - ); - - const labelClickedHandler = useCallback( - (labelIndex: number): void => { - const newGraphVisibilityStates = Array (data.datasets.length).fill( - false, - ); - newGraphVisibilityStates[labelIndex] = true; - setGraphVisibilityState([...newGraphVisibilityStates]); - eventEmitter.emit(Events.UPDATE_GRAPH_VISIBILITY_STATE, { - name, - graphVisibilityStates: newGraphVisibilityStates, - }); - }, - [data.datasets.length, name], - ); - - const columns = useMemo( - () => - getGraphManagerTableColumns({ - data, - checkBoxOnChangeHandler, - graphVisibilityState, - labelClickedHandler, - yAxisUnit, - }), - [ - checkBoxOnChangeHandler, - data, - graphVisibilityState, - labelClickedHandler, - yAxisUnit, - ], - ); - - const filterHandler = useCallback( - (event: React.ChangeEvent ): void => { - const value = event.target.value.toString().toLowerCase(); - const updatedDataSet = tableDataSet.map((item) => { - if (item.label?.toLocaleLowerCase().includes(value)) { - return { ...item, show: true }; - } - return { ...item, show: false }; - }); - setTableDataSet(updatedDataSet); - }, - [tableDataSet], - ); - - const saveHandler = useCallback((): void => { - saveLegendEntriesToLocalStorage({ - data, - graphVisibilityState, - name, - }); - notifications.success({ - message: 'The updated graphs & legends are saved', - }); - if (onToggleModelHandler) { - onToggleModelHandler(); - } - }, [data, graphVisibilityState, name, notifications, onToggleModelHandler]); - - const dataSource = tableDataSet.filter((item) => item.show); - - return ( - - - ); -} - -GraphManager.defaultProps = { - graphVisibilityStateHandler: undefined, -}; - -export default memo( - GraphManager, - (prevProps, nextProps) => - isEqual(prevProps.data, nextProps.data) && prevProps.name === nextProps.name, -); diff --git a/frontend/src/container/GridGraphLayout/Graph/FullView/TableRender/GetCheckBox.tsx b/frontend/src/container/GridGraphLayout/Graph/FullView/TableRender/GetCheckBox.tsx deleted file mode 100644 index 55485be5ad..0000000000 --- a/frontend/src/container/GridGraphLayout/Graph/FullView/TableRender/GetCheckBox.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { CheckboxChangeEvent } from 'antd/es/checkbox'; -import { ColumnType } from 'antd/es/table'; -import { ChartData } from 'chart.js'; - -import { DataSetProps } from '../types'; -import CustomCheckBox from './CustomCheckBox'; - -export const getCheckBox = ({ - data, - checkBoxOnChangeHandler, - graphVisibilityState, -}: GetCheckBoxProps): ColumnType- - -- - -- - -- - -=> ({ - render: (index: number): JSX.Element => ( - - ), -}); - -interface GetCheckBoxProps { - data: ChartData; - checkBoxOnChangeHandler: (e: CheckboxChangeEvent, index: number) => void; - graphVisibilityState: boolean[]; -} diff --git a/frontend/src/container/GridGraphLayout/Graph/WidgetGraphComponent.tsx b/frontend/src/container/GridGraphLayout/Graph/WidgetGraphComponent.tsx deleted file mode 100644 index f40d1e4190..0000000000 --- a/frontend/src/container/GridGraphLayout/Graph/WidgetGraphComponent.tsx +++ /dev/null @@ -1,334 +0,0 @@ -import { Typography } from 'antd'; -import { ToggleGraphProps } from 'components/Graph/types'; -import { Events } from 'constants/events'; -import GridPanelSwitch from 'container/GridPanelSwitch'; -import { useChartMutable } from 'hooks/useChartMutable'; -import { useNotifications } from 'hooks/useNotifications'; -import createQueryParams from 'lib/createQueryParams'; -import history from 'lib/history'; -import { isEmpty, isEqual } from 'lodash-es'; -import { - Dispatch, - memo, - SetStateAction, - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from 'react'; -import { useTranslation } from 'react-i18next'; -import { connect, useSelector } from 'react-redux'; -import { useLocation } from 'react-router-dom'; -import { bindActionCreators } from 'redux'; -import { ThunkDispatch } from 'redux-thunk'; -import { DeleteWidget } from 'store/actions/dashboard/deleteWidget'; -import { AppState } from 'store/reducers'; -import AppActions from 'types/actions'; -import AppReducer from 'types/reducer/app'; -import DashboardReducer from 'types/reducer/dashboards'; -import { eventEmitter } from 'utils/getEventEmitter'; -import { v4 } from 'uuid'; - -import { UpdateDashboard } from '../utils'; -import WidgetHeader from '../WidgetHeader'; -import FullView from './FullView'; -import { PANEL_TYPES_VS_FULL_VIEW_TABLE } from './FullView/contants'; -import { FullViewContainer, Modal } from './styles'; -import { DispatchProps, WidgetGraphComponentProps } from './types'; -import { - getGraphVisibilityStateOnDataChange, - toggleGraphsVisibilityInChart, -} from './utils'; - -function WidgetGraphComponent({ - enableModel, - enableWidgetHeader, - data, - widget, - queryResponse, - errorMessage, - name, - yAxisUnit, - layout = [], - deleteWidget, - setLayout, - onDragSelect, - onClickHandler, - threshold, - headerMenuList, -}: WidgetGraphComponentProps): JSX.Element { - const [deleteModal, setDeleteModal] = useState(false); - const [modal, setModal] = useState (false); - const [hovered, setHovered] = useState(false); - const { notifications } = useNotifications(); - const { t } = useTranslation(['common']); - const { pathname } = useLocation(); - - const { graphVisibilityStates: localstoredVisibilityStates } = useMemo( - () => - getGraphVisibilityStateOnDataChange({ - data, - isExpandedName: true, - name, - }), - [data, name], - ); - - const [graphsVisibilityStates, setGraphsVisilityStates] = useState ( - localstoredVisibilityStates, - ); - - const { dashboards } = useSelector ( - (state) => state.dashboards, - ); - const [selectedDashboard] = dashboards; - - const canModifyChart = useChartMutable({ - panelType: widget.panelTypes, - panelTypeAndGraphManagerVisibility: PANEL_TYPES_VS_FULL_VIEW_TABLE, - }); - - const lineChartRef = useRef (); - - // Updating the visibility state of the graph on data change according to global time range - useEffect(() => { - if (canModifyChart) { - const newGraphVisibilityState = getGraphVisibilityStateOnDataChange({ - data, - isExpandedName: true, - name, - }); - setGraphsVisilityStates(newGraphVisibilityState.graphVisibilityStates); - } - }, [canModifyChart, data, name]); - - useEffect(() => { - const eventListener = eventEmitter.on( - Events.UPDATE_GRAPH_VISIBILITY_STATE, - (data) => { - if (data.name === `${name}expanded` && canModifyChart) { - setGraphsVisilityStates([...data.graphVisibilityStates]); - } - }, - ); - return (): void => { - eventListener.off(Events.UPDATE_GRAPH_VISIBILITY_STATE); - }; - }, [canModifyChart, name]); - - useEffect(() => { - if (canModifyChart && lineChartRef.current) { - toggleGraphsVisibilityInChart({ - graphsVisibilityStates, - lineChartRef, - }); - } - }, [graphsVisibilityStates, canModifyChart]); - - const { featureResponse } = useSelector ( - (state) => state.app, - ); - const onToggleModal = useCallback( - (func: Dispatch >) => { - func((value) => !value); - }, - [], - ); - - const onDeleteHandler = useCallback(() => { - const isEmptyWidget = widget?.id === 'empty' || isEmpty(widget); - const widgetId = isEmptyWidget ? layout[0].i : widget?.id; - - featureResponse - .refetch() - .then(() => { - deleteWidget({ widgetId, setLayout }); - onToggleModal(setDeleteModal); - }) - .catch(() => { - notifications.error({ - message: t('common:something_went_wrong'), - }); - }); - }, [ - widget, - layout, - featureResponse, - deleteWidget, - setLayout, - onToggleModal, - notifications, - t, - ]); - - const onCloneHandler = async (): Promise => { - const uuid = v4(); - - const layout = [ - { - i: uuid, - w: 6, - x: 0, - h: 2, - y: 0, - }, - ...(selectedDashboard.data.layout || []), - ]; - - if (widget) { - await UpdateDashboard( - { - data: selectedDashboard.data, - generateWidgetId: uuid, - graphType: widget?.panelTypes, - selectedDashboard, - layout, - widgetData: widget, - isRedirected: false, - }, - notifications, - ).then(() => { - notifications.success({ - message: 'Panel cloned successfully, redirecting to new copy.', - }); - - const queryParams = { - graphType: widget?.panelTypes, - widgetId: uuid, - }; - history.push(`${pathname}/new?${createQueryParams(queryParams)}`); - }); - } - }; - - const handleOnView = (): void => { - onToggleModal(setModal); - }; - - const handleOnDelete = (): void => { - onToggleModal(setDeleteModal); - }; - - const onDeleteModelHandler = (): void => { - onToggleModal(setDeleteModal); - }; - - const onToggleModelHandler = (): void => { - onToggleModal(setModal); - }; - - const getModals = (): JSX.Element => ( - <> - - - -Are you sure you want to delete this widget -- - > - ); - - return ( - { - setHovered(true); - }} - onFocus={(): void => { - setHovered(true); - }} - onMouseOut={(): void => { - setHovered(false); - }} - onBlur={(): void => { - setHovered(false); - }} - > - {enableModel && getModals()} - {!isEmpty(widget) && data && ( - <> - {enableWidgetHeader && ( -- -- -- )} -- - > - )} - - ); -} - -WidgetGraphComponent.defaultProps = { - yAxisUnit: undefined, - layout: undefined, - setLayout: undefined, - onDragSelect: undefined, - onClickHandler: undefined, -}; - -const mapDispatchToProps = ( - dispatch: ThunkDispatch , -): DispatchProps => ({ - deleteWidget: bindActionCreators(DeleteWidget, dispatch), -}); - -export default connect( - null, - mapDispatchToProps, -)( - memo( - WidgetGraphComponent, - (prevProps, nextProps) => - isEqual(prevProps.data, nextProps.data) && prevProps.name === nextProps.name, - ), -); diff --git a/frontend/src/container/GridGraphLayout/Graph/index.tsx b/frontend/src/container/GridGraphLayout/Graph/index.tsx deleted file mode 100644 index 94b9d10252..0000000000 --- a/frontend/src/container/GridGraphLayout/Graph/index.tsx +++ /dev/null @@ -1,187 +0,0 @@ -import { ChartData } from 'chart.js'; -import Spinner from 'components/Spinner'; -import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange'; -import { useStepInterval } from 'hooks/queryBuilder/useStepInterval'; -import usePreviousValue from 'hooks/usePreviousValue'; -import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables'; -import getChartData from 'lib/getChartData'; -import isEmpty from 'lodash-es/isEmpty'; -import { memo, useMemo, useState } from 'react'; -import { useInView } from 'react-intersection-observer'; -import { useSelector } from 'react-redux'; -import { AppState } from 'store/reducers'; -import DashboardReducer from 'types/reducer/dashboards'; -import { GlobalReducer } from 'types/reducer/globalTime'; -import { getSelectedDashboardVariable } from 'utils/dashboard/selectedDashboard'; - -import EmptyWidget from '../EmptyWidget'; -import { MenuItemKeys } from '../WidgetHeader/contants'; -import { GridCardGraphProps } from './types'; -import WidgetGraphComponent from './WidgetGraphComponent'; - -function GridCardGraph({ - widget, - name, - yAxisUnit, - layout = [], - setLayout, - onDragSelect, - onClickHandler, - headerMenuList = [MenuItemKeys.View], - isQueryEnabled, - threshold, -}: GridCardGraphProps): JSX.Element { - const { isAddWidget } = useSelector ( - (state) => state.dashboards, - ); - - const { ref: graphRef, inView: isGraphVisible } = useInView({ - threshold: 0, - triggerOnce: true, - initialInView: false, - }); - - const [errorMessage, setErrorMessage] = useState (''); - - const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector< - AppState, - GlobalReducer - >((state) => state.globalTime); - const { dashboards } = useSelector ( - (state) => state.dashboards, - ); - - const variables = getSelectedDashboardVariable(dashboards); - - const updatedQuery = useStepInterval(widget?.query); - - const isEmptyWidget = useMemo( - () => widget?.id === 'empty' || isEmpty(widget), - [widget], - ); - - const queryResponse = useGetQueryRange( - { - selectedTime: widget?.timePreferance, - graphType: widget?.panelTypes, - query: updatedQuery, - globalSelectedInterval, - variables: getDashboardVariables(), - }, - { - queryKey: [ - `GetMetricsQueryRange-${widget?.timePreferance}-${globalSelectedInterval}-${widget?.id}`, - maxTime, - minTime, - globalSelectedInterval, - variables, - widget?.query, - widget?.panelTypes, - ], - keepPreviousData: true, - enabled: isGraphVisible && !isEmptyWidget && isQueryEnabled && !isAddWidget, - refetchOnMount: false, - onError: (error) => { - setErrorMessage(error.message); - }, - }, - ); - - const chartData = useMemo( - () => - getChartData({ - queryData: [ - { - queryData: queryResponse?.data?.payload?.data?.result || [], - }, - ], - }), - [queryResponse], - ); - - const prevChartDataSetRef = usePreviousValue (chartData); - - const isEmptyLayout = widget?.id === 'empty' || isEmpty(widget); - - if (queryResponse.isRefetching || queryResponse.isLoading) { - return ; - } - - if ((queryResponse.isError && !isEmptyLayout) || !isQueryEnabled) { - return ( - - {!isEmpty(widget) && prevChartDataSetRef && ( - - )} - - ); - } - - if (!isEmpty(widget) && prevChartDataSetRef?.labels) { - return ( - - - - ); - } - - return ( - - {!isEmpty(widget) && !!queryResponse.data?.payload && ( - - )} - - {isEmptyLayout && } - - ); -} - -GridCardGraph.defaultProps = { - onDragSelect: undefined, - onClickHandler: undefined, - isQueryEnabled: true, - threshold: undefined, - headerMenuList: [MenuItemKeys.View], -}; - -export default memo(GridCardGraph); diff --git a/frontend/src/container/GridGraphLayout/GraphLayout.tsx b/frontend/src/container/GridGraphLayout/GraphLayout.tsx deleted file mode 100644 index 6fc6aca63f..0000000000 --- a/frontend/src/container/GridGraphLayout/GraphLayout.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import { PlusOutlined, SaveFilled } from '@ant-design/icons'; -import { PANEL_TYPES } from 'constants/queryBuilder'; -import useComponentPermission from 'hooks/useComponentPermission'; -import { useIsDarkMode } from 'hooks/useDarkMode'; -import { Dispatch, SetStateAction } from 'react'; -import { Layout } from 'react-grid-layout'; -import { useSelector } from 'react-redux'; -import { AppState } from 'store/reducers'; -import { Widgets } from 'types/api/dashboard/getAll'; -import AppReducer from 'types/reducer/app'; -import DashboardReducer from 'types/reducer/dashboards'; - -import { LayoutProps, State } from '.'; -import { - Button, - ButtonContainer, - Card, - CardContainer, - ReactGridLayout, -} from './styles'; - -function GraphLayout({ - layouts, - saveLayoutState, - onLayoutSaveHandler, - addPanelLoading, - onAddPanelHandler, - onLayoutChangeHandler, - widgets, - setLayout, -}: GraphLayoutProps): JSX.Element { - const { isAddWidget } = useSelector ( - (state) => state.dashboards, - ); - const { role } = useSelector ((state) => state.app); - const isDarkMode = useIsDarkMode(); - - const [saveLayoutPermission, addPanelPermission] = useComponentPermission( - ['save_layout', 'add_panel'], - role, - ); - - return ( - <> - - {saveLayoutPermission && ( - - )} - - {addPanelPermission && ( - } - > - Add Panel - - )} - - -- {layouts.map(({ Component, ...rest }) => { - const currentWidget = (widgets || [])?.find((e) => e.id === rest.i); - - return ( - - > - ); -} - -interface GraphLayoutProps { - layouts: LayoutProps[]; - saveLayoutState: State; - onLayoutSaveHandler: (layout: Layout[]) => Promise- - ); - })} -- -- ; - addPanelLoading: boolean; - onAddPanelHandler: VoidFunction; - onLayoutChangeHandler: (layout: Layout[]) => Promise ; - widgets: Widgets[] | undefined; - setLayout: Dispatch >; -} - -export default GraphLayout; diff --git a/frontend/src/container/GridGraphLayout/config.ts b/frontend/src/container/GridGraphLayout/config.ts deleted file mode 100644 index 0357c7795c..0000000000 --- a/frontend/src/container/GridGraphLayout/config.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { MenuItemKeys } from 'container/GridGraphLayout/WidgetHeader/contants'; - -export const headerMenuList = [ - MenuItemKeys.View, - MenuItemKeys.Clone, - MenuItemKeys.Delete, - MenuItemKeys.Edit, -]; diff --git a/frontend/src/container/GridGraphLayout/index.tsx b/frontend/src/container/GridGraphLayout/index.tsx deleted file mode 100644 index fe65154fd9..0000000000 --- a/frontend/src/container/GridGraphLayout/index.tsx +++ /dev/null @@ -1,383 +0,0 @@ -/* eslint-disable react/no-unstable-nested-components */ - -import updateDashboardApi from 'api/dashboard/update'; -import { PANEL_TYPES } from 'constants/queryBuilder'; -import useComponentPermission from 'hooks/useComponentPermission'; -import { useNotifications } from 'hooks/useNotifications'; -import { - Dispatch, - SetStateAction, - useCallback, - useEffect, - useState, -} from 'react'; -import { Layout } from 'react-grid-layout'; -import { useTranslation } from 'react-i18next'; -import { connect, useDispatch, useSelector } from 'react-redux'; -import { bindActionCreators, Dispatch as ReduxDispatch } from 'redux'; -import { ThunkDispatch } from 'redux-thunk'; -import { AppDispatch } from 'store'; -import { UpdateTimeInterval } from 'store/actions'; -import { - ToggleAddWidget, - ToggleAddWidgetProps, -} from 'store/actions/dashboard/toggleAddWidget'; -import { AppState } from 'store/reducers'; -import AppActions from 'types/actions'; -import { UPDATE_DASHBOARD } from 'types/actions/dashboard'; -import { Dashboard, Widgets } from 'types/api/dashboard/getAll'; -import AppReducer from 'types/reducer/app'; -import DashboardReducer from 'types/reducer/dashboards'; - -import { headerMenuList } from './config'; -import Graph from './Graph'; -import GraphLayoutContainer from './GraphLayout'; -import { UpdateDashboard } from './utils'; - -export const getPreLayouts = ( - widgets: Widgets[] | undefined, - layout: Layout[], -): LayoutProps[] => - layout.map((e, index) => ({ - ...e, - Component: ({ setLayout }: ComponentProps): JSX.Element => { - const widget = widgets?.find((widget) => widget.id === e.i); - - return ( - - ); - }, - })); - -function GridGraph(props: Props): JSX.Element { - const { toggleAddWidget } = props; - const [addPanelLoading, setAddPanelLoading] = useState(false); - const { t } = useTranslation(['common']); - const { dashboards, isAddWidget } = useSelector ( - (state) => state.dashboards, - ); - const { role } = useSelector ((state) => state.app); - - const [saveLayoutPermission] = useComponentPermission(['save_layout'], role); - const [saveLayoutState, setSaveLayoutState] = useState ({ - loading: false, - error: false, - errorMessage: '', - payload: [], - }); - const [selectedDashboard] = dashboards; - const { data } = selectedDashboard; - const { widgets } = data; - const dispatch: AppDispatch = useDispatch >(); - - const [layouts, setLayout] = useState ( - getPreLayouts(widgets, selectedDashboard.data.layout || []), - ); - - const onDragSelect = useCallback( - (start: number, end: number) => { - const startTimestamp = Math.trunc(start); - const endTimestamp = Math.trunc(end); - - if (startTimestamp !== endTimestamp) { - dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp])); - } - }, - [dispatch], - ); - - const { notifications } = useNotifications(); - - useEffect(() => { - (async (): Promise => { - if (!isAddWidget) { - const isEmptyLayoutPresent = layouts.find((e) => e.i === 'empty'); - if (isEmptyLayoutPresent) { - // non empty layout - const updatedLayout = layouts.filter((e) => e.i !== 'empty'); - // non widget - const updatedWidget = widgets?.filter((e) => e.id !== 'empty'); - setLayout(updatedLayout); - - const updatedDashboard: Dashboard = { - ...selectedDashboard, - data: { - ...selectedDashboard.data, - layout: updatedLayout, - widgets: updatedWidget, - }, - }; - - await updateDashboardApi({ - data: updatedDashboard.data, - uuid: updatedDashboard.uuid, - }); - - dispatch({ - type: UPDATE_DASHBOARD, - payload: updatedDashboard, - }); - } - } - })(); - }, [dispatch, isAddWidget, layouts, selectedDashboard, widgets]); - - const { featureResponse } = useSelector ( - (state) => state.app, - ); - - const errorMessage = t('common:something_went_wrong'); - - const onLayoutSaveHandler = useCallback( - async (layout: Layout[]) => { - try { - setSaveLayoutState((state) => ({ - ...state, - error: false, - errorMessage: '', - loading: true, - })); - - featureResponse - .refetch() - .then(async () => { - const updatedDashboard: Dashboard = { - ...selectedDashboard, - data: { - title: data.title, - description: data.description, - name: data.name, - tags: data.tags, - widgets: data.widgets, - variables: data.variables, - layout, - }, - uuid: selectedDashboard.uuid, - }; - // Save layout only when users has the has the permission to do so. - if (saveLayoutPermission) { - const response = await updateDashboardApi(updatedDashboard); - if (response.statusCode === 200) { - setSaveLayoutState((state) => ({ - ...state, - error: false, - errorMessage: '', - loading: false, - })); - dispatch({ - type: UPDATE_DASHBOARD, - payload: updatedDashboard, - }); - } else { - setSaveLayoutState((state) => ({ - ...state, - error: true, - errorMessage: response.error || errorMessage, - loading: false, - })); - } - } - }) - .catch(() => { - setSaveLayoutState((state) => ({ - ...state, - error: true, - errorMessage, - loading: false, - })); - notifications.error({ - message: errorMessage, - }); - }); - } catch (error) { - notifications.error({ - message: errorMessage, - }); - } - }, - [ - data.description, - data.name, - data.tags, - data.title, - data.variables, - data.widgets, - dispatch, - errorMessage, - featureResponse, - notifications, - saveLayoutPermission, - selectedDashboard, - ], - ); - - const setLayoutFunction = useCallback( - (layout: Layout[]) => { - setLayout( - layout.map((e) => { - const currentWidget = - widgets?.find((widget) => widget.id === e.i) || ({} as Widgets); - - return { - ...e, - Component: (): JSX.Element => ( - - ), - }; - }), - ); - }, - [widgets, onDragSelect], - ); - - const onEmptyWidgetHandler = useCallback(async () => { - try { - const id = 'empty'; - - const layout = [ - { - i: id, - w: 6, - x: 0, - h: 2, - y: 0, - }, - ...(data.layout || []), - ]; - - await UpdateDashboard( - { - data, - generateWidgetId: id, - graphType: PANEL_TYPES.EMPTY_WIDGET, - selectedDashboard, - layout, - isRedirected: false, - }, - notifications, - ); - - setLayoutFunction(layout); - } catch (error) { - notifications.error({ - message: error instanceof Error ? error.toString() : errorMessage, - }); - } - }, [data, selectedDashboard, setLayoutFunction, notifications, errorMessage]); - - const onLayoutChangeHandler = async (layout: Layout[]): Promise => { - setLayoutFunction(layout); - - // await onLayoutSaveHandler(layout); - }; - - const onAddPanelHandler = useCallback(() => { - try { - setAddPanelLoading(true); - featureResponse - .refetch() - .then(() => { - const isEmptyLayoutPresent = - layouts.find((e) => e.i === 'empty') !== undefined; - - if (!isEmptyLayoutPresent) { - onEmptyWidgetHandler() - .then(() => { - setAddPanelLoading(false); - toggleAddWidget(true); - }) - .catch(() => { - notifications.error({ - message: errorMessage, - }); - }); - } else { - toggleAddWidget(true); - setAddPanelLoading(false); - } - }) - .catch(() => - notifications.error({ - message: errorMessage, - }), - ); - } catch (error) { - notifications.error({ - message: errorMessage, - }); - } - }, [ - featureResponse, - layouts, - onEmptyWidgetHandler, - toggleAddWidget, - notifications, - errorMessage, - ]); - - useEffect( - () => (): void => { - toggleAddWidget(false); - }, - [toggleAddWidget], - ); - - return ( - - ); -} - -interface ComponentProps { - setLayout: Dispatch >; -} - -export interface LayoutProps extends Layout { - Component: (props: ComponentProps) => JSX.Element; -} - -export interface State { - loading: boolean; - error: boolean; - payload: Layout[]; - errorMessage: string; -} - -interface DispatchProps { - toggleAddWidget: ( - props: ToggleAddWidgetProps, - ) => (dispatch: ReduxDispatch ) => void; -} - -const mapDispatchToProps = ( - dispatch: ThunkDispatch , -): DispatchProps => ({ - toggleAddWidget: bindActionCreators(ToggleAddWidget, dispatch), -}); - -type Props = DispatchProps; - -export default connect(null, mapDispatchToProps)(GridGraph); diff --git a/frontend/src/container/GridGraphLayout/utils.ts b/frontend/src/container/GridGraphLayout/utils.ts deleted file mode 100644 index a18fe52886..0000000000 --- a/frontend/src/container/GridGraphLayout/utils.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { NotificationInstance } from 'antd/es/notification/interface'; -import updateDashboardApi from 'api/dashboard/update'; -import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder'; -import { Layout } from 'react-grid-layout'; -import store from 'store'; -import { Dashboard, Widgets } from 'types/api/dashboard/getAll'; - -export const UpdateDashboard = async ( - { - data, - graphType, - generateWidgetId, - layout, - selectedDashboard, - isRedirected, - widgetData, - }: UpdateDashboardProps, - notify: NotificationInstance, -): Promise => { - const copyTitle = `${widgetData?.title} - Copy`; - const updatedSelectedDashboard: Dashboard = { - ...selectedDashboard, - data: { - title: data.title, - description: data.description, - name: data.name, - tags: data.tags, - variables: data.variables, - widgets: [ - ...(data.widgets || []), - { - description: widgetData?.description || '', - id: generateWidgetId, - isStacked: false, - nullZeroValues: widgetData?.nullZeroValues || '', - opacity: '', - panelTypes: graphType, - query: widgetData?.query || initialQueriesMap.metrics, - timePreferance: widgetData?.timePreferance || 'GLOBAL_TIME', - title: widgetData ? copyTitle : '', - yAxisUnit: widgetData?.yAxisUnit, - }, - ], - layout, - }, - uuid: selectedDashboard.uuid, - }; - - const response = await updateDashboardApi(updatedSelectedDashboard); - - if (response.payload) { - store.dispatch({ - type: 'UPDATE_DASHBOARD', - payload: response.payload, - }); - } - - if (isRedirected) { - if (response.statusCode === 200) { - return response.payload; - } - notify.error({ - message: response.error || 'Something went wrong', - }); - return undefined; - } - return undefined; -}; - -interface UpdateDashboardProps { - data: Dashboard['data']; - graphType: PANEL_TYPES; - generateWidgetId: string; - layout: Layout[]; - selectedDashboard: Dashboard; - isRedirected: boolean; - widgetData?: Widgets; -} diff --git a/frontend/src/container/IngestionSettings/IngestionSettings.styles.scss b/frontend/src/container/IngestionSettings/IngestionSettings.styles.scss new file mode 100644 index 0000000000..3d5f41ab33 --- /dev/null +++ b/frontend/src/container/IngestionSettings/IngestionSettings.styles.scss @@ -0,0 +1,3 @@ +.ingestion-settings-container { + color: white; +} diff --git a/frontend/src/container/IngestionSettings/IngestionSettings.tsx b/frontend/src/container/IngestionSettings/IngestionSettings.tsx new file mode 100644 index 0000000000..0971ecc960 --- /dev/null +++ b/frontend/src/container/IngestionSettings/IngestionSettings.tsx @@ -0,0 +1,82 @@ +import './IngestionSettings.styles.scss'; + +import { Table, Typography } from 'antd'; +import type { ColumnsType } from 'antd/es/table'; +import getIngestionData from 'api/settings/getIngestionData'; +import { useQuery } from 'react-query'; +import { useSelector } from 'react-redux'; +import { AppState } from 'store/reducers'; +import { IngestionDataType } from 'types/api/settings/ingestion'; +import AppReducer from 'types/reducer/app'; + +export default function IngestionSettings(): JSX.Element { + const { user } = useSelector ((state) => state.app); + + const { data: ingestionData } = useQuery({ + queryFn: getIngestionData, + queryKey: ['getIngestionData', user?.userId], + }); + + const columns: ColumnsType = [ + { + title: 'Name', + dataIndex: 'name', + key: 'name', + render: (text): JSX.Element => {text} , + }, + { + title: 'Value', + dataIndex: 'value', + key: 'value', + render: (text): JSX.Element => ( +{text} + ), + }, + ]; + + const injectionDataPayload = + ingestionData && + ingestionData.payload && + Array.isArray(ingestionData.payload) && + ingestionData?.payload[0]; + + const data: IngestionDataType[] = [ + { + key: '1', + name: 'Ingestion URL', + value: injectionDataPayload?.ingestionURL, + }, + { + key: '2', + name: 'Ingestion Key', + value: injectionDataPayload?.ingestionKey, + }, + { + key: '3', + name: 'Ingestion Region', + value: injectionDataPayload?.dataRegion, + }, + ]; + + return ( +++ ); +} diff --git a/frontend/src/container/ListOfDashboard/ImportJSON/index.tsx b/frontend/src/container/ListOfDashboard/ImportJSON/index.tsx index 3a7f406b4e..aa658d56cc 100644 --- a/frontend/src/container/ListOfDashboard/ImportJSON/index.tsx +++ b/frontend/src/container/ListOfDashboard/ImportJSON/index.tsx @@ -9,11 +9,7 @@ import { useNotifications } from 'hooks/useNotifications'; import history from 'lib/history'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useDispatch } from 'react-redux'; import { generatePath } from 'react-router-dom'; -import { Dispatch } from 'redux'; -import AppActions from 'types/actions'; -import { FLUSH_DASHBOARD } from 'types/actions/dashboard'; import { DashboardData } from 'types/api/dashboard/getAll'; import { EditorContainer, FooterContainer } from './styles'; @@ -31,8 +27,6 @@ function ImportJSON({ ); const [isFeatureAlert, setIsFeatureAlert] = useState+ You can use the following ingestion credentials to start sending your + telemetry data to SigNoz + + ++
(false); - const dispatch = useDispatch >(); - const [dashboardCreating, setDashboardCreating] = useState (false); const [editorValue, setEditorValue] = useState (''); @@ -77,16 +71,11 @@ function ImportJSON({ }); if (response.statusCode === 200) { - dispatch({ - type: FLUSH_DASHBOARD, - }); - setTimeout(() => { - history.push( - generatePath(ROUTES.DASHBOARD, { - dashboardId: response.payload.uuid, - }), - ); - }, 10); + history.push( + generatePath(ROUTES.DASHBOARD, { + dashboardId: response.payload.uuid, + }), + ); } else if (response.error === 'feature usage exceeded') { setIsFeatureAlert(true); notifications.error({ diff --git a/frontend/src/container/ListOfDashboard/TableComponents/DeleteButton.tsx b/frontend/src/container/ListOfDashboard/TableComponents/DeleteButton.tsx index 39d3c02a23..03c0ac9912 100644 --- a/frontend/src/container/ListOfDashboard/TableComponents/DeleteButton.tsx +++ b/frontend/src/container/ListOfDashboard/TableComponents/DeleteButton.tsx @@ -1,37 +1,36 @@ import { ExclamationCircleOutlined } from '@ant-design/icons'; import { Modal } from 'antd'; +import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; +import { useDeleteDashboard } from 'hooks/dashboard/useDeleteDashboard'; import { useCallback } from 'react'; -import { connect } from 'react-redux'; -import { bindActionCreators, Dispatch } from 'redux'; -import { ThunkDispatch } from 'redux-thunk'; -import { DeleteDashboard, DeleteDashboardProps } from 'store/actions'; -import AppActions from 'types/actions'; +import { useQueryClient } from 'react-query'; import { Data } from '../index'; import { TableLinkText } from './styles'; -function DeleteButton({ - deleteDashboard, - id, - refetchDashboardList, -}: DeleteButtonProps): JSX.Element { +function DeleteButton({ id }: Data): JSX.Element { const [modal, contextHolder] = Modal.useModal(); + const queryClient = useQueryClient(); + + const deleteDashboardMutation = useDeleteDashboard(id); + const openConfirmationDialog = useCallback((): void => { modal.confirm({ title: 'Do you really want to delete this dashboard?', icon: , onOk() { - deleteDashboard({ - uuid: id, - refetch: refetchDashboardList, + deleteDashboardMutation.mutateAsync(undefined, { + onSuccess: () => { + queryClient.invalidateQueries([REACT_QUERY_KEY.GET_ALL_DASHBOARDS]); + }, }); }, okText: 'Delete', okButtonProps: { danger: true }, centered: true, }); - }, [modal, deleteDashboard, id, refetchDashboardList]); + }, [modal, deleteDashboardMutation, queryClient]); return ( <> @@ -44,37 +43,12 @@ function DeleteButton({ ); } -interface DispatchProps { - deleteDashboard: ({ - uuid, - }: DeleteDashboardProps) => (dispatch: Dispatch ) => void; -} - -const mapDispatchToProps = ( - dispatch: ThunkDispatch , -): DispatchProps => ({ - deleteDashboard: bindActionCreators(DeleteDashboard, dispatch), -}); - -export type DeleteButtonProps = Data & DispatchProps; - -const WrapperDeleteButton = connect(null, mapDispatchToProps)(DeleteButton); - // This is to avoid the type collision function Wrapper(props: Data): JSX.Element { - const { - createdBy, - description, - id, - key, - refetchDashboardList, - lastUpdatedTime, - name, - tags, - } = props; + const { createdBy, description, id, key, lastUpdatedTime, name, tags } = props; return ( - ); diff --git a/frontend/src/container/ListOfDashboard/index.tsx b/frontend/src/container/ListOfDashboard/index.tsx index 6041b8baf4..87f47e54af 100644 --- a/frontend/src/container/ListOfDashboard/index.tsx +++ b/frontend/src/container/ListOfDashboard/index.tsx @@ -17,21 +17,11 @@ import SearchFilter from 'container/ListOfDashboard/SearchFilter'; import { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard'; import useComponentPermission from 'hooks/useComponentPermission'; import history from 'lib/history'; -import { - Dispatch, - Key, - useCallback, - useEffect, - useMemo, - useState, -} from 'react'; +import { Key, useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { UseQueryResult } from 'react-query'; -import { useDispatch, useSelector } from 'react-redux'; +import { useSelector } from 'react-redux'; import { generatePath } from 'react-router-dom'; import { AppState } from 'store/reducers'; -import AppActions from 'types/actions'; -import { GET_ALL_DASHBOARD_SUCCESS } from 'types/actions/dashboard'; import { Dashboard } from 'types/api/dashboard/getAll'; import AppReducer from 'types/reducer/app'; import { popupContainer } from 'utils/selectPopupContainer'; @@ -40,9 +30,7 @@ import ImportJSON from './ImportJSON'; import { ButtonContainer, NewDashboardButton, TableContainer } from './styles'; import Createdby from './TableComponents/CreatedBy'; import DateComponent from './TableComponents/Date'; -import DeleteButton, { - DeleteButtonProps, -} from './TableComponents/DeleteButton'; +import DeleteButton from './TableComponents/DeleteButton'; import Name from './TableComponents/Name'; import Tags from './TableComponents/Tags'; @@ -53,7 +41,6 @@ function ListOfAllDashboard(): JSX.Element { refetch: refetchDashboardList, } = useGetAllDashboard(); - const dispatch = useDispatch >(); const { role } = useSelector ((state) => state.app); const [action, createNewDashboard, newDashboard] = useComponentPermission( @@ -134,31 +121,12 @@ function ListOfAllDashboard(): JSX.Element { title: 'Action', dataIndex: '', width: 40, - render: ({ - createdBy, - description, - id, - key, - lastUpdatedTime, - name, - tags, - }: DeleteButtonProps) => ( - - ), + render: DeleteButton, }); } return tableColumns; - }, [action, refetchDashboardList]); + }, [action]); const data: Data[] = filteredDashboards?.map((e) => ({ @@ -186,10 +154,6 @@ function ListOfAllDashboard(): JSX.Element { }); if (response.statusCode === 200) { - dispatch({ - type: GET_ALL_DASHBOARD_SUCCESS, - payload: [], - }); history.push( generatePath(ROUTES.DASHBOARD, { dashboardId: response.payload.uuid, @@ -210,7 +174,7 @@ function ListOfAllDashboard(): JSX.Element { errorMessage: (error as AxiosError).toString() || 'Something went Wrong', }); } - }, [newDashboardState, t, dispatch]); + }, [newDashboardState, t]); const getText = useCallback(() => { if (!newDashboardState.error && !newDashboardState.loading) { @@ -352,7 +316,6 @@ export interface Data { createdBy: string; lastUpdatedTime: string; id: string; - refetchDashboardList: UseQueryResult['refetch']; } export default ListOfAllDashboard; diff --git a/frontend/src/container/LiveLogs/LiveLogsContainer/index.tsx b/frontend/src/container/LiveLogs/LiveLogsContainer/index.tsx index 49a071d17e..e47ae535a6 100644 --- a/frontend/src/container/LiveLogs/LiveLogsContainer/index.tsx +++ b/frontend/src/container/LiveLogs/LiveLogsContainer/index.tsx @@ -11,11 +11,11 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import useDebouncedFn from 'hooks/useDebouncedFunction'; import { useEventSourceEvent } from 'hooks/useEventSourceEvent'; import { useNotifications } from 'hooks/useNotifications'; +import { prepareQueryRangePayload } from 'lib/dashboard/prepareQueryRangePayload'; import { useEventSource } from 'providers/EventSource'; import { useCallback, useEffect, useRef, useState } from 'react'; import { useSelector } from 'react-redux'; import { useLocation } from 'react-router-dom'; -import { prepareQueryRangePayload } from 'store/actions/dashboard/prepareQueryRangePayload'; import { AppState } from 'store/reducers'; import { ILog } from 'types/api/logs/log'; import { Query } from 'types/api/queryBuilder/queryBuilderData'; diff --git a/frontend/src/container/LogDetailedView/TableView.tsx b/frontend/src/container/LogDetailedView/TableView.tsx index 28910afbde..17446b0118 100644 --- a/frontend/src/container/LogDetailedView/TableView.tsx +++ b/frontend/src/container/LogDetailedView/TableView.tsx @@ -21,7 +21,12 @@ import { ILog } from 'types/api/logs/log'; import ActionItem, { ActionItemProps } from './ActionItem'; import FieldRenderer from './FieldRenderer'; -import { flattenObject, jsonToDataNodes, recursiveParseJSON } from './utils'; +import { + flattenObject, + jsonToDataNodes, + recursiveParseJSON, + removeEscapeCharacters, +} from './utils'; // Fields which should be restricted from adding it to query const RESTRICTED_FIELDS = ['timestamp']; @@ -58,7 +63,7 @@ function TableView({ .map((key) => ({ key, field: key, - value: JSON.stringify(flattenLogData[key]), + value: removeEscapeCharacters(JSON.stringify(flattenLogData[key])), })); const onTraceHandler = (record: DataType) => (): void => { @@ -164,6 +169,8 @@ function TableView({ width: 70, ellipsis: false, render: (field, record): JSX.Element => { + const textToCopy = field.slice(1, -1); + if (record.field === 'body') { const parsedBody = recursiveParseJSON(field); if (!isEmpty(parsedBody)) { @@ -174,7 +181,7 @@ function TableView({ } return ( - + {field} ); diff --git a/frontend/src/container/LogDetailedView/config.ts b/frontend/src/container/LogDetailedView/config.ts new file mode 100644 index 0000000000..cd34023699 --- /dev/null +++ b/frontend/src/container/LogDetailedView/config.ts @@ -0,0 +1,13 @@ +import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse'; + +export const typeToArrayTypeMapper: { [key in DataTypes]: DataTypes } = { + [DataTypes.String]: DataTypes.ArrayString, + [DataTypes.Float64]: DataTypes.ArrayFloat64, + [DataTypes.Int64]: DataTypes.ArrayInt64, + [DataTypes.bool]: DataTypes.ArrayBool, + [DataTypes.EMPTY]: DataTypes.EMPTY, + [DataTypes.ArrayFloat64]: DataTypes.ArrayFloat64, + [DataTypes.ArrayInt64]: DataTypes.ArrayInt64, + [DataTypes.ArrayString]: DataTypes.ArrayString, + [DataTypes.ArrayBool]: DataTypes.ArrayBool, +}; diff --git a/frontend/src/container/LogDetailedView/util.test.ts b/frontend/src/container/LogDetailedView/util.test.ts index 4f080e23c1..d5918f2bca 100644 --- a/frontend/src/container/LogDetailedView/util.test.ts +++ b/frontend/src/container/LogDetailedView/util.test.ts @@ -176,8 +176,8 @@ describe('Get Data Types utils', () => { }); // Edge cases - it('should return Int64 for empty array input', () => { - expect(getDataTypes([])).toBe(DataTypes.Int64); + it('should return Empty for empty array input', () => { + expect(getDataTypes([])).toBe(DataTypes.EMPTY); }); it('should handle mixed array (return based on first element)', () => { diff --git a/frontend/src/container/LogDetailedView/utils.tsx b/frontend/src/container/LogDetailedView/utils.tsx index 02890e7dc9..f31534ace8 100644 --- a/frontend/src/container/LogDetailedView/utils.tsx +++ b/frontend/src/container/LogDetailedView/utils.tsx @@ -5,6 +5,7 @@ import { ILog, ILogAggregateAttributesResources } from 'types/api/logs/log'; import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse'; import BodyTitleRenderer from './BodyTitleRenderer'; +import { typeToArrayTypeMapper } from './config'; import { AnyObject, IFieldAttributes } from './LogDetailedView.types'; export const recursiveParseJSON = (obj: string): Record=> { @@ -107,40 +108,6 @@ export function flattenObject(obj: AnyObject, prefix = ''): AnyObject { }, {}); } -const isFloat = (num: number): boolean => num % 1 !== 0; - -export const getDataTypes = (value: unknown): DataTypes => { - if (typeof value === 'string') { - return DataTypes.String; - } - - if (typeof value === 'number') { - return isFloat(value) ? DataTypes.Float64 : DataTypes.Int64; - } - - if (typeof value === 'boolean') { - return DataTypes.bool; - } - - if (Array.isArray(value)) { - const firstElement = value[0]; - - if (typeof firstElement === 'string') { - return DataTypes.ArrayString; - } - - if (typeof firstElement === 'boolean') { - return DataTypes.ArrayBool; - } - - if (typeof firstElement === 'number') { - return isFloat(firstElement) ? DataTypes.ArrayFloat64 : DataTypes.ArrayInt64; - } - } - - return DataTypes.Int64; -}; - export const generateFieldKeyForArray = ( fieldKey: string, dataType: DataTypes, @@ -217,3 +184,59 @@ export const aggregateAttributesResourcesToString = (logData: ILog): string => { return JSON.stringify(outputJson, null, 2); }; + +const isFloat = (num: number): boolean => num % 1 !== 0; + +const isBooleanString = (str: string): boolean => + str.toLowerCase() === 'true' || str.toLowerCase() === 'false'; + +const determineType = (val: unknown): DataTypes => { + if (typeof val === 'string') { + if (isBooleanString(val)) { + return DataTypes.bool; + } + + const numberValue = parseFloat(val); + + if (!Number.isNaN(numberValue)) { + return isFloat(numberValue) ? DataTypes.Float64 : DataTypes.Int64; + } + + return DataTypes.String; + } + + if (typeof val === 'number') { + return isFloat(val) ? DataTypes.Float64 : DataTypes.Int64; + } + + if (typeof val === 'boolean') { + return DataTypes.bool; + } + + return DataTypes.EMPTY; +}; + +export const getDataTypes = (value: unknown): DataTypes => { + const getArrayType = (elementType: DataTypes): DataTypes => + typeToArrayTypeMapper[elementType] || DataTypes.EMPTY; + + if (Array.isArray(value)) { + return getArrayType(determineType(value[0])); + } + + return determineType(value); +}; + +export const removeEscapeCharacters = (str: string): string => + str.replace(/\\([ntfr'"\\])/g, (_: string, char: string) => { + const escapeMap: Record = { + n: '\n', + t: '\t', + f: '\f', + r: '\r', + "'": "'", + '"': '"', + '\\': '\\', + }; + return escapeMap[char as keyof typeof escapeMap]; + }); diff --git a/frontend/src/container/LogsExplorerChart/index.tsx b/frontend/src/container/LogsExplorerChart/index.tsx index a64f8eb382..ec329907f3 100644 --- a/frontend/src/container/LogsExplorerChart/index.tsx +++ b/frontend/src/container/LogsExplorerChart/index.tsx @@ -48,7 +48,7 @@ function LogsExplorerChart({ ) : ( ({ description: '', id: v4(), @@ -17,4 +18,5 @@ export const getWidgetQueryBuilder = ({ query, timePreferance: 'GLOBAL_TIME', title, + yAxisUnit, }); diff --git a/frontend/src/container/MetricsApplication/Tabs/DBCall.tsx b/frontend/src/container/MetricsApplication/Tabs/DBCall.tsx index 83e6da6db2..15af9981c0 100644 --- a/frontend/src/container/MetricsApplication/Tabs/DBCall.tsx +++ b/frontend/src/container/MetricsApplication/Tabs/DBCall.tsx @@ -1,6 +1,6 @@ import { Col } from 'antd'; import { PANEL_TYPES } from 'constants/queryBuilder'; -import Graph from 'container/GridGraphLayout/Graph/'; +import Graph from 'container/GridCardLayout/GridCard'; import { databaseCallsAvgDuration, databaseCallsRPS, @@ -65,6 +65,7 @@ function DBCall(): JSX.Element { }, title: GraphTitle.DATABASE_CALLS_RPS, panelTypes: PANEL_TYPES.TIME_SERIES, + yAxisUnit: 'reqps', }), [servicename, tagFilterItems], ); @@ -83,6 +84,7 @@ function DBCall(): JSX.Element { }, title: GraphTitle.DATABASE_CALLS_AVG_DURATION, panelTypes: PANEL_TYPES.TIME_SERIES, + yAxisUnit: 'ms', }), [servicename, tagFilterItems], ); @@ -107,7 +109,6 @@ function DBCall(): JSX.Element { { onGraphClickHandler(setSelectedTimeStamp)( ChartEvent, @@ -135,12 +136,12 @@ function DBCall(): JSX.Element { > View Traces + { onGraphClickHandler(setSelectedTimeStamp)( ChartEvent, diff --git a/frontend/src/container/MetricsApplication/Tabs/External.tsx b/frontend/src/container/MetricsApplication/Tabs/External.tsx index 3abe8d4ca4..cda6e9275c 100644 --- a/frontend/src/container/MetricsApplication/Tabs/External.tsx +++ b/frontend/src/container/MetricsApplication/Tabs/External.tsx @@ -1,6 +1,6 @@ import { Col } from 'antd'; import { PANEL_TYPES } from 'constants/queryBuilder'; -import Graph from 'container/GridGraphLayout/Graph/'; +import Graph from 'container/GridCardLayout/GridCard'; import { externalCallDuration, externalCallDurationByAddress, @@ -56,6 +56,7 @@ function External(): JSX.Element { }, title: GraphTitle.EXTERNAL_CALL_ERROR_PERCENTAGE, panelTypes: PANEL_TYPES.TIME_SERIES, + yAxisUnit: '%', }), [servicename, tagFilterItems], ); @@ -80,6 +81,7 @@ function External(): JSX.Element { }, title: GraphTitle.EXTERNAL_CALL_DURATION, panelTypes: PANEL_TYPES.TIME_SERIES, + yAxisUnit: 'ms', }), [servicename, tagFilterItems], ); @@ -100,6 +102,7 @@ function External(): JSX.Element { }, title: GraphTitle.EXTERNAL_CALL_RPS_BY_ADDRESS, panelTypes: PANEL_TYPES.TIME_SERIES, + yAxisUnit: 'reqps', }), [servicename, tagFilterItems], ); @@ -120,6 +123,7 @@ function External(): JSX.Element { }, title: GraphTitle.EXTERNAL_CALL_DURATION_BY_ADDRESS, panelTypes: PANEL_TYPES.TIME_SERIES, + yAxisUnit: 'ms', }), [servicename, tagFilterItems], ); @@ -146,7 +150,6 @@ function External(): JSX.Element { { onGraphClickHandler(setSelectedTimeStamp)( ChartEvent, @@ -181,7 +184,6 @@ function External(): JSX.Element { { onGraphClickHandler(setSelectedTimeStamp)( ChartEvent, @@ -217,7 +219,6 @@ function External(): JSX.Element { { onGraphClickHandler(setSelectedTimeStamp)( ChartEvent, @@ -252,7 +253,6 @@ function External(): JSX.Element { { onGraphClickHandler(setSelectedTimeStamp)( ChartEvent, diff --git a/frontend/src/container/MetricsApplication/Tabs/Overview.tsx b/frontend/src/container/MetricsApplication/Tabs/Overview.tsx index b7ab171ccd..d67053e1e0 100644 --- a/frontend/src/container/MetricsApplication/Tabs/Overview.tsx +++ b/frontend/src/container/MetricsApplication/Tabs/Overview.tsx @@ -131,6 +131,7 @@ function Application(): JSX.Element { }, title: GraphTitle.RATE_PER_OPS, panelTypes: PANEL_TYPES.TIME_SERIES, + yAxisUnit: 'ops', }), [servicename, tagFilterItems, topLevelOperationsRoute], ); @@ -151,6 +152,7 @@ function Application(): JSX.Element { }, title: GraphTitle.ERROR_PERCENTAGE, panelTypes: PANEL_TYPES.TIME_SERIES, + yAxisUnit: '%', }), [servicename, tagFilterItems, topLevelOperationsRoute], ); @@ -222,7 +224,6 @@ function Application(): JSX.Element { topLevelOperationsIsError={topLevelOperationsIsError} name="operations_per_sec" widget={operationPerSecWidget} - yAxisUnit="ops" opName="Rate" /> @@ -267,7 +268,6 @@ function Application(): JSX.Element { topLevelOperationsIsError={topLevelOperationsIsError} name="error_percentage_%" widget={errorPercentageWidget} - yAxisUnit="%" opName="Error" /> diff --git a/frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/ApDexMetrics.tsx b/frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/ApDexMetrics.tsx index b20613bae7..ade8a1bec3 100644 --- a/frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/ApDexMetrics.tsx +++ b/frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/ApDexMetrics.tsx @@ -6,8 +6,8 @@ import { apDexToolTipUrlText, } from 'constants/apDex'; import { PANEL_TYPES } from 'constants/queryBuilder'; -import Graph from 'container/GridGraphLayout/Graph'; -import DisplayThreshold from 'container/GridGraphLayout/WidgetHeader/DisplayThreshold'; +import Graph from 'container/GridCardLayout/GridCard'; +import DisplayThreshold from 'container/GridCardLayout/WidgetHeader/DisplayThreshold'; import { GraphTitle } from 'container/MetricsApplication/constant'; import { getWidgetQueryBuilder } from 'container/MetricsApplication/MetricsApplication.factory'; import { apDexMetricsQueryBuilderQueries } from 'container/MetricsApplication/MetricsPageQueries/OverviewQueries'; @@ -87,7 +87,6 @@ function ApDexMetrics({ widget={apDexMetricsWidget} onDragSelect={onDragSelect} onClickHandler={handleGraphClick('ApDex')} - yAxisUnit="" threshold={threshold} isQueryEnabled={isQueryEnabled} /> diff --git a/frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/ApDexTraces.tsx b/frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/ApDexTraces.tsx index bf6297785f..1b2e5ba0cd 100644 --- a/frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/ApDexTraces.tsx +++ b/frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/ApDexTraces.tsx @@ -1,7 +1,7 @@ // This component is not been used in the application as we support only metrics for ApDex as of now. // This component is been kept for future reference. import { PANEL_TYPES } from 'constants/queryBuilder'; -import Graph from 'container/GridGraphLayout/Graph'; +import Graph from 'container/GridCardLayout/GridCard'; import { GraphTitle } from 'container/MetricsApplication/constant'; import { getWidgetQueryBuilder } from 'container/MetricsApplication/MetricsApplication.factory'; import { apDexTracesQueryBuilderQueries } from 'container/MetricsApplication/MetricsPageQueries/OverviewQueries'; @@ -52,7 +52,6 @@ function ApDexTraces({ widget={apDexTracesWidget} onDragSelect={onDragSelect} onClickHandler={handleGraphClick('ApDex')} - yAxisUnit="" threshold={thresholdValue} isQueryEnabled={isQueryEnabled} /> diff --git a/frontend/src/container/MetricsApplication/Tabs/Overview/ServiceOverview.tsx b/frontend/src/container/MetricsApplication/Tabs/Overview/ServiceOverview.tsx index 52014ac48b..cb124f545a 100644 --- a/frontend/src/container/MetricsApplication/Tabs/Overview/ServiceOverview.tsx +++ b/frontend/src/container/MetricsApplication/Tabs/Overview/ServiceOverview.tsx @@ -1,7 +1,7 @@ import Spinner from 'components/Spinner'; import { FeatureKeys } from 'constants/features'; import { PANEL_TYPES } from 'constants/queryBuilder'; -import Graph from 'container/GridGraphLayout/Graph/'; +import Graph from 'container/GridCardLayout/GridCard'; import { GraphTitle } from 'container/MetricsApplication/constant'; import { getWidgetQueryBuilder } from 'container/MetricsApplication/MetricsApplication.factory'; import { latency } from 'container/MetricsApplication/MetricsPageQueries/OverviewQueries'; @@ -59,6 +59,7 @@ function ServiceOverview({ }, title: GraphTitle.LATENCY, panelTypes: PANEL_TYPES.TIME_SERIES, + yAxisUnit: 'ns', }), [servicename, isSpanMetricEnable, topLevelOperationsRoute, tagFilterItems], ); @@ -93,7 +94,6 @@ function ServiceOverview({ name="service_latency" onDragSelect={onDragSelect} widget={latencyWidget} - yAxisUnit="ns" onClickHandler={handleGraphClick('Service')} isQueryEnabled={isQueryEnabled} /> diff --git a/frontend/src/container/MetricsApplication/Tabs/Overview/TopLevelOperations.tsx b/frontend/src/container/MetricsApplication/Tabs/Overview/TopLevelOperations.tsx index 903ff3a15f..f71a0f7301 100644 --- a/frontend/src/container/MetricsApplication/Tabs/Overview/TopLevelOperations.tsx +++ b/frontend/src/container/MetricsApplication/Tabs/Overview/TopLevelOperations.tsx @@ -2,7 +2,7 @@ import { Typography } from 'antd'; import axios from 'axios'; import Spinner from 'components/Spinner'; import { SOMETHING_WENT_WRONG } from 'constants/api'; -import Graph from 'container/GridGraphLayout/Graph/'; +import Graph from 'container/GridCardLayout/GridCard'; import { Card, GraphContainer } from 'container/MetricsApplication/styles'; import { Widgets } from 'types/api/dashboard/getAll'; @@ -17,7 +17,6 @@ function TopLevelOperation({ onDragSelect, handleGraphClick, widget, - yAxisUnit, }: TopLevelOperationProps): JSX.Element { return ( @@ -37,7 +36,6 @@ function TopLevelOperation({ name={name} widget={widget} onClickHandler={handleGraphClick(opName)} - yAxisUnit={yAxisUnit} onDragSelect={onDragSelect} /> )} @@ -56,7 +54,6 @@ interface TopLevelOperationProps { onDragSelect: (start: number, end: number) => void; handleGraphClick: (type: string) => ClickHandlerType; widget: Widgets; - yAxisUnit: string; } export default TopLevelOperation; diff --git a/frontend/src/container/MetricsApplication/Tabs/Overview/TopOperationMetrics.tsx b/frontend/src/container/MetricsApplication/Tabs/Overview/TopOperationMetrics.tsx index 4ca191b68c..b0385e2ab0 100644 --- a/frontend/src/container/MetricsApplication/Tabs/Overview/TopOperationMetrics.tsx +++ b/frontend/src/container/MetricsApplication/Tabs/Overview/TopOperationMetrics.tsx @@ -7,9 +7,7 @@ import { useStepInterval } from 'hooks/queryBuilder/useStepInterval'; import { useNotifications } from 'hooks/useNotifications'; import useResourceAttribute from 'hooks/useResourceAttribute'; import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils'; -import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables'; import { RowData } from 'lib/query/createTableColumnsFromQuery'; -import { isEmpty } from 'lodash-es'; import { ReactNode, useMemo } from 'react'; import { useSelector } from 'react-redux'; import { useParams } from 'react-router-dom'; @@ -58,10 +56,7 @@ function TopOperationMetrics(): JSX.Element { const updatedQuery = useStepInterval(keyOperationWidget.query); - const isEmptyWidget = useMemo( - () => keyOperationWidget.id === 'empty' || isEmpty(keyOperationWidget), - [keyOperationWidget], - ); + const isEmptyWidget = keyOperationWidget.id === PANEL_TYPES.EMPTY_WIDGET; const { data, isLoading } = useGetQueryRange( { @@ -69,7 +64,7 @@ function TopOperationMetrics(): JSX.Element { graphType: keyOperationWidget?.panelTypes, query: updatedQuery, globalSelectedInterval, - variables: getDashboardVariables(), + variables: {}, }, { queryKey: [ diff --git a/frontend/src/container/MetricsApplication/TopOperationsTable.tsx b/frontend/src/container/MetricsApplication/TopOperationsTable.tsx index e753a965a5..4c895716e7 100644 --- a/frontend/src/container/MetricsApplication/TopOperationsTable.tsx +++ b/frontend/src/container/MetricsApplication/TopOperationsTable.tsx @@ -10,7 +10,7 @@ import { GlobalReducer } from 'types/reducer/globalTime'; import { getErrorRate, navigateToTrace } from './utils'; -function TopOperationsTable(props: TopOperationsTableProps): JSX.Element { +function TopOperationsTable({ data }: TopOperationsTableProps): JSX.Element { const { minTime, maxTime } = useSelector ( (state) => state.globalTime, ); @@ -20,8 +20,6 @@ function TopOperationsTable(props: TopOperationsTableProps): JSX.Element { convertRawQueriesToTraceSelectedTags(queries) || [], ); - const { data } = props; - const params = useParams<{ servicename: string }>(); const handleOnClick = (operation: string): void => { diff --git a/frontend/src/container/MetricsApplication/types.ts b/frontend/src/container/MetricsApplication/types.ts index d9a7251745..f87ce66a2a 100644 --- a/frontend/src/container/MetricsApplication/types.ts +++ b/frontend/src/container/MetricsApplication/types.ts @@ -8,6 +8,7 @@ export interface GetWidgetQueryBuilderProps { query: Widgets['query']; title?: ReactNode; panelTypes: Widgets['panelTypes']; + yAxisUnit?: Widgets['yAxisUnit']; } export interface NavigateToTraceProps { diff --git a/frontend/src/container/NewDashboard/ComponentsSlider/index.tsx b/frontend/src/container/NewDashboard/ComponentsSlider/index.tsx index da6e28ccca..c9c1c6dde1 100644 --- a/frontend/src/container/NewDashboard/ComponentsSlider/index.tsx +++ b/frontend/src/container/NewDashboard/ComponentsSlider/index.tsx @@ -1,67 +1,97 @@ +import { SOMETHING_WENT_WRONG } from 'constants/api'; import { QueryParams } from 'constants/query'; import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder'; +import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard'; import { useIsDarkMode } from 'hooks/useDarkMode'; import { useNotifications } from 'hooks/useNotifications'; import createQueryParams from 'lib/createQueryParams'; import history from 'lib/history'; -import { CSSProperties, useCallback } from 'react'; -import { connect, useSelector } from 'react-redux'; -import { useLocation } from 'react-router-dom'; -import { bindActionCreators, Dispatch } from 'redux'; -import { ThunkDispatch } from 'redux-thunk'; -import { - ToggleAddWidget, - ToggleAddWidgetProps, -} from 'store/actions/dashboard/toggleAddWidget'; -import { AppState } from 'store/reducers'; -import AppActions from 'types/actions'; -import DashboardReducer from 'types/reducer/dashboards'; +import { useDashboard } from 'providers/Dashboard/Dashboard'; +import { CSSProperties } from 'react'; +import { v4 as uuid } from 'uuid'; import menuItems from './menuItems'; import { Card, Container, Text } from './styles'; -function DashboardGraphSlider({ toggleAddWidget }: Props): JSX.Element { - const { dashboards } = useSelector ( - (state) => state.dashboards, - ); +function DashboardGraphSlider(): JSX.Element { + const isDarkMode = useIsDarkMode(); - const { pathname } = useLocation(); + const { + handleToggleDashboardSlider, + layouts, + selectedDashboard, + } = useDashboard(); + + const { data } = selectedDashboard || {}; const { notifications } = useNotifications(); - const [selectedDashboard] = dashboards; - const { data } = selectedDashboard; + const updateDashboardMutation = useUpdateDashboard(); - const onClickHandler = useCallback( - (name: PANEL_TYPES) => (): void => { - try { - const emptyLayout = data.layout?.find((e) => e.i === 'empty'); + const onClickHandler = (name: PANEL_TYPES) => (): void => { + const id = uuid(); - if (emptyLayout === undefined) { - notifications.error({ - message: 'Please click on Add Panel Button', + updateDashboardMutation.mutateAsync( + { + uuid: selectedDashboard?.uuid || '', + data: { + title: data?.title || '', + variables: data?.variables || {}, + description: data?.description || '', + name: data?.name || '', + tags: data?.tags || [], + layout: [ + { + i: id, + w: 6, + x: 0, + h: 2, + y: 0, + }, + ...(layouts.filter((layout) => layout.i !== PANEL_TYPES.EMPTY_WIDGET) || + []), + ], + widgets: [ + ...(data?.widgets || []), + { + id, + title: '', + description: '', + isStacked: false, + nullZeroValues: '', + opacity: '', + panelTypes: name, + query: initialQueriesMap.metrics, + timePreferance: 'GLOBAL_TIME', + }, + ], + }, + }, + { + onSuccess: (data) => { + if (data.payload) { + handleToggleDashboardSlider(false); + + const queryParams = { + graphType: name, + widgetId: id, + [QueryParams.compositeQuery]: JSON.stringify(initialQueriesMap.metrics), + }; + + history.push( + `${history.location.pathname}/new?${createQueryParams(queryParams)}`, + ); + } + }, + onError: () => { + notifications.success({ + message: SOMETHING_WENT_WRONG, }); - return; - } + }, + }, + ); + }; - toggleAddWidget(false); - - const queryParams = { - graphType: name, - widgetId: emptyLayout.i, - [QueryParams.compositeQuery]: JSON.stringify(initialQueriesMap.metrics), - }; - - history.push(`${pathname}/new?${createQueryParams(queryParams)}`); - } catch (error) { - notifications.error({ - message: 'Something went wrong', - }); - } - }, - [data, toggleAddWidget, notifications, pathname], - ); - const isDarkMode = useIsDarkMode(); const fillColor: CSSProperties['color'] = isDarkMode ? 'white' : 'black'; return ( @@ -76,18 +106,4 @@ function DashboardGraphSlider({ toggleAddWidget }: Props): JSX.Element { ); } -interface DispatchProps { - toggleAddWidget: ( - props: ToggleAddWidgetProps, - ) => (dispatch: Dispatch ) => void; -} - -const mapDispatchToProps = ( - dispatch: ThunkDispatch , -): DispatchProps => ({ - toggleAddWidget: bindActionCreators(ToggleAddWidget, dispatch), -}); - -type Props = DispatchProps; - -export default connect(null, mapDispatchToProps)(DashboardGraphSlider); +export default DashboardGraphSlider; diff --git a/frontend/src/container/NewDashboard/DashboardSettings/General/index.tsx b/frontend/src/container/NewDashboard/DashboardSettings/General/index.tsx index a8f1892fa4..3f6eec23b4 100644 --- a/frontend/src/container/NewDashboard/DashboardSettings/General/index.tsx +++ b/frontend/src/container/NewDashboard/DashboardSettings/General/index.tsx @@ -1,33 +1,23 @@ import { SaveOutlined } from '@ant-design/icons'; import { Col, Divider, Input, Space, Typography } from 'antd'; +import { SOMETHING_WENT_WRONG } from 'constants/api'; import AddTags from 'container/NewDashboard/DashboardSettings/General/AddTags'; -import { useCallback, useState } from 'react'; +import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard'; +import { useNotifications } from 'hooks/useNotifications'; +import { useDashboard } from 'providers/Dashboard/Dashboard'; +import { useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { connect, useSelector } from 'react-redux'; -import { bindActionCreators, Dispatch } from 'redux'; -import { ThunkDispatch } from 'redux-thunk'; -import { - UpdateDashboardTitleDescriptionTags, - UpdateDashboardTitleDescriptionTagsProps, -} from 'store/actions'; -import { AppState } from 'store/reducers'; -import AppActions from 'types/actions'; -import DashboardReducer from 'types/reducer/dashboards'; import { Button } from './styles'; -function GeneralDashboardSettings({ - updateDashboardTitleDescriptionTags, -}: DescriptionOfDashboardProps): JSX.Element { - const { dashboards } = useSelector ( - (state) => state.dashboards, - ); +function GeneralDashboardSettings(): JSX.Element { + const { selectedDashboard, setSelectedDashboard } = useDashboard(); - const [selectedDashboard] = dashboards; - const selectedData = selectedDashboard.data; - const { title } = selectedData; - const { tags } = selectedData; - const { description } = selectedData; + const updateDashboardMutation = useUpdateDashboard(); + + const selectedData = selectedDashboard?.data; + + const { title = '', tags = [], description = '' } = selectedData || {}; const [updatedTitle, setUpdatedTitle] = useState (title); const [updatedTags, setUpdatedTags] = useState (tags || []); @@ -37,27 +27,35 @@ function GeneralDashboardSettings({ const { t } = useTranslation('common'); - const onSaveHandler = useCallback(() => { - const dashboard = selectedDashboard; - // @TODO need to update this function to take title,description,tags only - updateDashboardTitleDescriptionTags({ - dashboard: { - ...dashboard, + const { notifications } = useNotifications(); + + const onSaveHandler = (): void => { + if (!selectedDashboard) return; + + updateDashboardMutation.mutateAsync( + { + ...selectedDashboard, data: { - ...dashboard.data, + ...selectedDashboard.data, description: updatedDescription, tags: updatedTags, title: updatedTitle, }, }, - }); - }, [ - updatedTitle, - updatedTags, - updatedDescription, - selectedDashboard, - updateDashboardTitleDescriptionTags, - ]); + { + onSuccess: (updatedDashboard) => { + if (updatedDashboard.payload) { + setSelectedDashboard(updatedDashboard.payload); + } + }, + onError: () => { + notifications.error({ + message: SOMETHING_WENT_WRONG, + }); + }, + }, + ); + }; return ( @@ -83,7 +81,13 @@ function GeneralDashboardSettings({ @@ -92,21 +96,4 @@ function GeneralDashboardSettings({ ); } -interface DispatchProps { - updateDashboardTitleDescriptionTags: ( - props: UpdateDashboardTitleDescriptionTagsProps, - ) => (dispatch: Dispatch- } onClick={onSaveHandler} type="primary"> + } + onClick={onSaveHandler} + type="primary" + > {t('save')} ) => void; -} - -const mapDispatchToProps = ( - dispatch: ThunkDispatch , -): DispatchProps => ({ - updateDashboardTitleDescriptionTags: bindActionCreators( - UpdateDashboardTitleDescriptionTags, - dispatch, - ), -}); - -type DescriptionOfDashboardProps = DispatchProps; - -export default connect(null, mapDispatchToProps)(GeneralDashboardSettings); +export default GeneralDashboardSettings; diff --git a/frontend/src/container/NewDashboard/DashboardSettings/Variables/index.tsx b/frontend/src/container/NewDashboard/DashboardSettings/Variables/index.tsx index 3f90bf565b..388dffc286 100644 --- a/frontend/src/container/NewDashboard/DashboardSettings/Variables/index.tsx +++ b/frontend/src/container/NewDashboard/DashboardSettings/Variables/index.tsx @@ -1,39 +1,28 @@ import { blue, red } from '@ant-design/colors'; import { PlusOutlined } from '@ant-design/icons'; import { Button, Modal, Row, Space, Tag } from 'antd'; -import { NotificationInstance } from 'antd/es/notification/interface'; import { ResizeTable } from 'components/ResizeTable'; +import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard'; import { useNotifications } from 'hooks/useNotifications'; +import { useDashboard } from 'providers/Dashboard/Dashboard'; import { useRef, useState } from 'react'; -import { connect, useSelector } from 'react-redux'; -import { bindActionCreators, Dispatch } from 'redux'; -import { ThunkDispatch } from 'redux-thunk'; -import { UpdateDashboardVariables } from 'store/actions/dashboard/updatedDashboardVariables'; -import { AppState } from 'store/reducers'; -import AppActions from 'types/actions'; -import { IDashboardVariable } from 'types/api/dashboard/getAll'; -import DashboardReducer from 'types/reducer/dashboards'; +import { useTranslation } from 'react-i18next'; +import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll'; import { TVariableViewMode } from './types'; import VariableItem from './VariableItem/VariableItem'; -function VariablesSetting({ - updateDashboardVariables, -}: DispatchProps): JSX.Element { +function VariablesSetting(): JSX.Element { const variableToDelete = useRef (null); const [deleteVariableModal, setDeleteVariableModal] = useState(false); - const { dashboards } = useSelector ( - (state) => state.dashboards, - ); + const { t } = useTranslation(['dashboard']); + + const { selectedDashboard, setSelectedDashboard } = useDashboard(); const { notifications } = useNotifications(); - const [selectedDashboard] = dashboards; - - const { - data: { variables = {} }, - } = selectedDashboard; + const { variables = {} } = selectedDashboard?.data || {}; const variablesTableData = Object.keys(variables).map((variableName) => ({ key: variableName, @@ -64,6 +53,41 @@ function VariablesSetting({ setVariableViewMode(viewType); }; + const updateMutation = useUpdateDashboard(); + + const updateVariables = ( + updatedVariablesData: Dashboard['data']['variables'], + ): void => { + if (!selectedDashboard) { + return; + } + + updateMutation.mutateAsync( + { + ...selectedDashboard, + data: { + ...selectedDashboard.data, + variables: updatedVariablesData, + }, + }, + { + onSuccess: (updatedDashboard) => { + if (updatedDashboard.payload) { + setSelectedDashboard(updatedDashboard.payload); + notifications.success({ + message: t('variable_updated_successfully'), + }); + } + }, + onError: () => { + notifications.error({ + message: t('error_while_updating_variable'), + }); + }, + }, + ); + }; + const onVariableSaveHandler = ( name: string, variableData: IDashboardVariable, @@ -79,7 +103,7 @@ function VariablesSetting({ if (oldName) { delete newVariables[oldName]; } - updateDashboardVariables(newVariables, notifications); + updateVariables(newVariables); onDoneVariableViewMode(); }; @@ -91,7 +115,7 @@ function VariablesSetting({ const handleDeleteConfirm = (): void => { const newVariables = { ...variables }; if (variableToDelete?.current) delete newVariables[variableToDelete?.current]; - updateDashboardVariables(newVariables, notifications); + updateVariables(newVariables); variableToDelete.current = null; setDeleteVariableModal(false); }; @@ -182,20 +206,4 @@ function VariablesSetting({ ); } -interface DispatchProps { - updateDashboardVariables: ( - props: Record , - notify: NotificationInstance, - ) => (dispatch: Dispatch ) => void; -} - -const mapDispatchToProps = ( - dispatch: ThunkDispatch , -): DispatchProps => ({ - updateDashboardVariables: bindActionCreators( - UpdateDashboardVariables, - dispatch, - ), -}); - -export default connect(null, mapDispatchToProps)(VariablesSetting); +export default VariablesSetting; diff --git a/frontend/src/container/NewDashboard/DashboardSettings/index.tsx b/frontend/src/container/NewDashboard/DashboardSettings/index.tsx index 50a69495fa..5a1bc6afac 100644 --- a/frontend/src/container/NewDashboard/DashboardSettings/index.tsx +++ b/frontend/src/container/NewDashboard/DashboardSettings/index.tsx @@ -3,12 +3,12 @@ import { Tabs } from 'antd'; import GeneralDashboardSettings from './General'; import VariablesSetting from './Variables'; -function DashboardSettingsContent(): JSX.Element { - const items = [ - { label: 'General', key: 'general', children: }, - { label: 'Variables', key: 'variables', children: }, - ]; +const items = [ + { label: 'General', key: 'general', children: }, + { label: 'Variables', key: 'variables', children: }, +]; +function DashboardSettingsContent(): JSX.Element { return ; } diff --git a/frontend/src/container/NewDashboard/DashboardVariablesSelection/index.tsx b/frontend/src/container/NewDashboard/DashboardVariablesSelection/index.tsx index d318e684f8..561af111ae 100644 --- a/frontend/src/container/NewDashboard/DashboardVariablesSelection/index.tsx +++ b/frontend/src/container/NewDashboard/DashboardVariablesSelection/index.tsx @@ -1,34 +1,25 @@ import { Row } from 'antd'; -import { NotificationInstance } from 'antd/es/notification/interface'; +import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard'; import { useNotifications } from 'hooks/useNotifications'; import { map, sortBy } from 'lodash-es'; +import { useDashboard } from 'providers/Dashboard/Dashboard'; import { useState } from 'react'; -import { connect, useSelector } from 'react-redux'; -import { bindActionCreators, Dispatch } from 'redux'; -import { ThunkDispatch } from 'redux-thunk'; -import { UpdateDashboardVariables } from 'store/actions/dashboard/updatedDashboardVariables'; +import { useSelector } from 'react-redux'; import { AppState } from 'store/reducers'; -import AppActions from 'types/actions'; -import { IDashboardVariable } from 'types/api/dashboard/getAll'; +import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll'; import AppReducer from 'types/reducer/app'; -import DashboardReducer from 'types/reducer/dashboards'; import VariableItem from './VariableItem'; -function DashboardVariableSelection({ - updateDashboardVariables, -}: DispatchProps): JSX.Element { - const { dashboards } = useSelector ( - (state) => state.dashboards, - ); - const [selectedDashboard] = dashboards; - const { - data: { variables = {} }, - } = selectedDashboard; +function DashboardVariableSelection(): JSX.Element | null { + const { selectedDashboard, setSelectedDashboard } = useDashboard(); + + const { data } = selectedDashboard || {}; + + const { variables } = data || {}; const [update, setUpdate] = useState (false); const [lastUpdatedVar, setLastUpdatedVar] = useState (''); - const { notifications } = useNotifications(); const { role } = useSelector ((state) => state.app); @@ -37,6 +28,42 @@ function DashboardVariableSelection({ setUpdate(!update); }; + const updateMutation = useUpdateDashboard(); + const { notifications } = useNotifications(); + + const updateVariables = ( + updatedVariablesData: Dashboard['data']['variables'], + ): void => { + if (!selectedDashboard) { + return; + } + + updateMutation.mutateAsync( + { + ...selectedDashboard, + data: { + ...selectedDashboard.data, + variables: updatedVariablesData, + }, + }, + { + onSuccess: (updatedDashboard) => { + if (updatedDashboard.payload) { + setSelectedDashboard(updatedDashboard.payload); + notifications.success({ + message: 'Variable updated successfully', + }); + } + }, + onError: () => { + notifications.error({ + message: 'Error while updating variable', + }); + }, + }, + ); + }; + const onValueUpdate = ( name: string, value: IDashboardVariable['selectedValue'], @@ -44,8 +71,8 @@ function DashboardVariableSelection({ const updatedVariablesData = { ...variables }; updatedVariablesData[name].selectedValue = value; - if (role !== 'VIEWER') { - updateDashboardVariables(updatedVariablesData, notifications); + if (role !== 'VIEWER' && selectedDashboard) { + updateVariables(updatedVariablesData); } onVarChanged(name); @@ -58,13 +85,17 @@ function DashboardVariableSelection({ updatedVariablesData[name].allSelected = value; if (role !== 'VIEWER') { - updateDashboardVariables(updatedVariablesData, notifications); + updateVariables(updatedVariablesData); } onVarChanged(name); }; + if (!variables) { + return null; + } + return ( - +
{map(sortBy(Object.keys(variables)), (variableName) => (
))} @@ -83,20 +114,4 @@ function DashboardVariableSelection({ ); } -interface DispatchProps { - updateDashboardVariables: ( - props: Parameters [0], - notify: NotificationInstance, - ) => (dispatch: Dispatch ) => void; -} - -const mapDispatchToProps = ( - dispatch: ThunkDispatch , -): DispatchProps => ({ - updateDashboardVariables: bindActionCreators( - UpdateDashboardVariables, - dispatch, - ), -}); - -export default connect(null, mapDispatchToProps)(DashboardVariableSelection); +export default DashboardVariableSelection; diff --git a/frontend/src/container/NewDashboard/DescriptionOfDashboard/SettingsDrawer.tsx b/frontend/src/container/NewDashboard/DescriptionOfDashboard/SettingsDrawer.tsx index 72a10a3f30..fc51efd69a 100644 --- a/frontend/src/container/NewDashboard/DescriptionOfDashboard/SettingsDrawer.tsx +++ b/frontend/src/container/NewDashboard/DescriptionOfDashboard/SettingsDrawer.tsx @@ -6,7 +6,7 @@ import DashboardSettingsContent from '../DashboardSettings'; import { DrawerContainer } from './styles'; function SettingsDrawer(): JSX.Element { - const [visible, setVisible] = useState(false); // TODO Make it False + const [visible, setVisible] = useState (false); const showDrawer = (): void => { setVisible(true); @@ -25,7 +25,7 @@ function SettingsDrawer(): JSX.Element { placement="right" width="70%" onClose={onClose} - visible={visible} + open={visible} > diff --git a/frontend/src/container/NewDashboard/DescriptionOfDashboard/index.tsx b/frontend/src/container/NewDashboard/DescriptionOfDashboard/index.tsx index 2faafd7cbb..3c6ca326a3 100644 --- a/frontend/src/container/NewDashboard/DescriptionOfDashboard/index.tsx +++ b/frontend/src/container/NewDashboard/DescriptionOfDashboard/index.tsx @@ -1,25 +1,22 @@ import { ShareAltOutlined } from '@ant-design/icons'; import { Button, Card, Col, Row, Space, Tag, Typography } from 'antd'; import useComponentPermission from 'hooks/useComponentPermission'; +import { useDashboard } from 'providers/Dashboard/Dashboard'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; import { AppState } from 'store/reducers'; import AppReducer from 'types/reducer/app'; -import DashboardReducer from 'types/reducer/dashboards'; import DashboardVariableSelection from '../DashboardVariablesSelection'; import SettingsDrawer from './SettingsDrawer'; import ShareModal from './ShareModal'; function DescriptionOfDashboard(): JSX.Element { - const { dashboards } = useSelector ( - (state) => state.dashboards, - ); + const { selectedDashboard } = useDashboard(); - const [selectedDashboard] = dashboards; - const selectedData = selectedDashboard.data; - const { title, tags, description } = selectedData; + const selectedData = selectedDashboard?.data; + const { title, tags, description } = selectedData || {}; const [isJSONModalVisible, isIsJSONModalVisible] = useState (false); @@ -34,26 +31,29 @@ function DescriptionOfDashboard(): JSX.Element { return ( -
+ {title} {description} +- {tags?.map((e) => ( -+{e} + {tags?.map((tag) => ( +{tag} ))}- + {selectedData && ( + + )} + {editDashboard && }