From eb4abe900c4363e19a3a042d1504af2813a8ce37 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Mon, 28 Mar 2022 09:14:40 +0530 Subject: [PATCH 001/106] chore: generate uuid on server side --- pkg/query-service/app/dashboards/model.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/query-service/app/dashboards/model.go b/pkg/query-service/app/dashboards/model.go index 480fadf69d..77b66046c0 100644 --- a/pkg/query-service/app/dashboards/model.go +++ b/pkg/query-service/app/dashboards/model.go @@ -7,6 +7,7 @@ import ( "strings" "time" + "github.com/google/uuid" "github.com/gosimple/slug" "github.com/jmoiron/sqlx" "go.signoz.io/query-service/model" @@ -109,8 +110,7 @@ func CreateDashboard(data *map[string]interface{}) (*Dashboard, *model.ApiError) dash.CreatedAt = time.Now() dash.UpdatedAt = time.Now() dash.UpdateSlug() - // dash.Uuid = uuid.New().String() - dash.Uuid = dash.Data["uuid"].(string) + dash.Uuid = uuid.New().String() map_data, err := json.Marshal(dash.Data) if err != nil { From dce9f36a8e7db0ab9eb5ae5d9ff337dfb03586ac Mon Sep 17 00:00:00 2001 From: Palash gupta Date: Fri, 8 Apr 2022 02:12:54 +0530 Subject: [PATCH 002/106] chore: refetchOnWindowFocus is made false to global level --- frontend/src/index.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index 0abe9a6453..99c7758c41 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -13,7 +13,13 @@ if (process.env.NODE_ENV === 'development') { reportWebVitals(console.log); } -const queryClient = new QueryClient(); +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + refetchOnWindowFocus: false, + }, + }, +}); ReactDOM.render( From 0cbe17a315418470d21e721d07b096f6b0f5d522 Mon Sep 17 00:00:00 2001 From: Palash gupta Date: Fri, 8 Apr 2022 02:15:50 +0530 Subject: [PATCH 003/106] chore(eslint): @typescript-eslint/no-unused-vars is made to error --- frontend/.eslintrc.js | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js index fb9d999579..54cc5e016c 100644 --- a/frontend/.eslintrc.js +++ b/frontend/.eslintrc.js @@ -101,6 +101,7 @@ module.exports = { }, }, ], + '@typescript-eslint/no-unused-vars': 'error', // eslint rules need to remove 'no-shadow': 'off', From 60288f7ba030c5d8736b5086a0dc328de5e25f6e Mon Sep 17 00:00:00 2001 From: palash-signoz Date: Fri, 8 Apr 2022 13:49:33 +0530 Subject: [PATCH 004/106] chore(refactor): Signup app layout (#969) * feat: useFetch is upgraded to useFetchQueries * chore: en-gb and common.json is updated over public locale --- frontend/public/locales/en-GB/common.json | 3 + .../public/locales/en-GB/translation.json | 27 +++++++ frontend/public/locales/en/common.json | 3 + frontend/src/container/AppLayout/index.tsx | 80 +++++++++++++------ frontend/src/pages/SignUp/index.tsx | 53 +++++++----- 5 files changed, 125 insertions(+), 41 deletions(-) create mode 100644 frontend/public/locales/en-GB/common.json create mode 100644 frontend/public/locales/en-GB/translation.json create mode 100644 frontend/public/locales/en/common.json diff --git a/frontend/public/locales/en-GB/common.json b/frontend/public/locales/en-GB/common.json new file mode 100644 index 0000000000..c17e7e73dd --- /dev/null +++ b/frontend/public/locales/en-GB/common.json @@ -0,0 +1,3 @@ +{ + "something_went_wrong": "Something went wrong" +} diff --git a/frontend/public/locales/en-GB/translation.json b/frontend/public/locales/en-GB/translation.json new file mode 100644 index 0000000000..23330080e6 --- /dev/null +++ b/frontend/public/locales/en-GB/translation.json @@ -0,0 +1,27 @@ +{ + "monitor_signup": "Monitor your applications. Find what is causing issues.", + "version": "Version", + "latest_version": "Latest version", + "current_version": "Current version", + "release_notes": "Release Notes", + "read_how_to_upgrade": "Read instructions on how to upgrade", + "latest_version_signoz": "You are running the latest version of SigNoz.", + "stale_version": "You are on an older version and may be loosing on the latest features we have shipped. We recommend to upgrade to the latest version", + "oops_something_went_wrong_version": "Oops.. facing issues with fetching updated version information", + "n_a": "N/A", + "routes": { + "general": "General", + "alert_channels": "Alert Channels" + }, + "settings": { + "total_retention_period": "Total Retention Period", + "move_to_s3": "Move to S3\n(should be lower than total retention period)", + "retention_success_message": "Congrats. The retention periods for {{name}} has been updated successfully.", + "retention_error_message": "There was an issue in changing the retention period for {{name}}. Please try again or reach out to support@signoz.io", + "retention_failed_message": "There was an issue in changing the retention period. Please try again or reach out to support@signoz.io", + "retention_comparison_error": "Total retention period for {{name}} can’t be lower or equal to the period after which data is moved to s3.", + "retention_null_value_error": "Retention Period for {{name}} is not set yet. Please set by choosing below", + "retention_confirmation": "Are you sure you want to change the retention period?", + "retention_confirmation_description": "This will change the amount of storage needed for saving metrics & traces." + } +} diff --git a/frontend/public/locales/en/common.json b/frontend/public/locales/en/common.json new file mode 100644 index 0000000000..c17e7e73dd --- /dev/null +++ b/frontend/public/locales/en/common.json @@ -0,0 +1,3 @@ +{ + "something_went_wrong": "Something went wrong" +} diff --git a/frontend/src/container/AppLayout/index.tsx b/frontend/src/container/AppLayout/index.tsx index 1fa47522c5..98910c60c5 100644 --- a/frontend/src/container/AppLayout/index.tsx +++ b/frontend/src/container/AppLayout/index.tsx @@ -1,13 +1,13 @@ import { notification } from 'antd'; -import getLatestVersion from 'api/user/getLatestVersion'; -import getVersion from 'api/user/getVersion'; +import getUserLatestVersion from 'api/user/getLatestVersion'; +import getUserVersion from 'api/user/getVersion'; import ROUTES from 'constants/routes'; import TopNav from 'container/Header'; import SideNav from 'container/SideNav'; -import useFetch from 'hooks/useFetch'; import history from 'lib/history'; import React, { ReactNode, useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { useQueries } from 'react-query'; import { useDispatch, useSelector } from 'react-redux'; import { useLocation } from 'react-router-dom'; import { Dispatch } from 'redux'; @@ -30,15 +30,28 @@ function AppLayout(props: AppLayoutProps): JSX.Element { const [isSignUpPage, setIsSignUpPage] = useState(ROUTES.SIGN_UP === pathname); - const { payload: versionPayload, loading, error: getVersionError } = useFetch( - getVersion, - ); + const [getUserVersionResponse, getUserLatestVersionResponse] = useQueries([ + { + queryFn: getUserVersion, + queryKey: 'getUserVersion', + enabled: isLoggedIn, + }, + { + queryFn: getUserLatestVersion, + queryKey: 'getUserLatestVersion', + enabled: isLoggedIn, + }, + ]); - const { - payload: latestVersionPayload, - loading: latestLoading, - error: latestError, - } = useFetch(getLatestVersion); + useEffect(() => { + if (getUserLatestVersionResponse.status === 'idle' && isLoggedIn) { + getUserLatestVersionResponse.refetch(); + } + + if (getUserVersionResponse.status === 'idle' && isLoggedIn) { + getUserVersionResponse.refetch(); + } + }, [getUserLatestVersionResponse, getUserVersionResponse, isLoggedIn]); const { children } = props; @@ -61,7 +74,11 @@ function AppLayout(props: AppLayoutProps): JSX.Element { history.push(ROUTES.APPLICATION); } - if (!latestLoading && latestError && latestCurrentCounter.current === 0) { + if ( + getUserLatestVersionResponse.isFetched && + getUserLatestVersionResponse.isError && + latestCurrentCounter.current === 0 + ) { latestCurrentCounter.current = 1; dispatch({ @@ -75,7 +92,11 @@ function AppLayout(props: AppLayoutProps): JSX.Element { }); } - if (!loading && getVersionError && latestVersionCounter.current === 0) { + if ( + getUserVersionResponse.isFetched && + getUserVersionResponse.isError && + latestVersionCounter.current === 0 + ) { latestVersionCounter.current = 1; dispatch({ @@ -89,34 +110,47 @@ function AppLayout(props: AppLayoutProps): JSX.Element { }); } - if (!latestLoading && versionPayload) { + if ( + getUserVersionResponse.isFetched && + getUserLatestVersionResponse.isSuccess && + getUserVersionResponse.data && + getUserVersionResponse.data.payload + ) { dispatch({ type: UPDATE_CURRENT_VERSION, payload: { - currentVersion: versionPayload.version, + currentVersion: getUserVersionResponse.data.payload.version, }, }); } - if (!loading && latestVersionPayload) { + if ( + getUserLatestVersionResponse.isFetched && + getUserLatestVersionResponse.isSuccess && + getUserLatestVersionResponse.data && + getUserLatestVersionResponse.data.payload + ) { dispatch({ type: UPDATE_LATEST_VERSION, payload: { - latestVersion: latestVersionPayload.name, + latestVersion: getUserLatestVersionResponse.data.payload.name, }, }); } }, [ dispatch, - loading, - latestLoading, - versionPayload, - latestVersionPayload, isLoggedIn, pathname, - getVersionError, - latestError, t, + getUserLatestVersionResponse.isLoading, + getUserLatestVersionResponse.isError, + getUserLatestVersionResponse.data, + getUserVersionResponse.isLoading, + getUserVersionResponse.isError, + getUserVersionResponse.data, + getUserLatestVersionResponse.isFetched, + getUserVersionResponse.isFetched, + getUserLatestVersionResponse.isSuccess, ]); return ( diff --git a/frontend/src/pages/SignUp/index.tsx b/frontend/src/pages/SignUp/index.tsx index 1bf16285a4..2518567adc 100644 --- a/frontend/src/pages/SignUp/index.tsx +++ b/frontend/src/pages/SignUp/index.tsx @@ -1,41 +1,58 @@ import { Typography } from 'antd'; -import getPreference from 'api/user/getPreference'; -import getVersion from 'api/user/getVersion'; +import getUserPreference from 'api/user/getPreference'; +import getUserVersion from 'api/user/getVersion'; import Spinner from 'components/Spinner'; -import useFetch from 'hooks/useFetch'; import React from 'react'; -import { PayloadProps as UserPrefPayload } from 'types/api/user/getUserPreference'; -import { PayloadProps as VersionPayload } from 'types/api/user/getVersion'; +import { useTranslation } from 'react-i18next'; +import { useQueries } from 'react-query'; +import { useSelector } from 'react-redux'; +import { AppState } from 'store/reducers'; +import AppReducer from 'types/reducer/app'; import SignUpComponent from './SignUp'; function SignUp(): JSX.Element { - const versionResponse = useFetch(getVersion); + const { t } = useTranslation('common'); + const { isLoggedIn } = useSelector((state) => state.app); - const userPrefResponse = useFetch(getPreference); + const [versionResponse, userPrefResponse] = useQueries([ + { + queryFn: getUserVersion, + queryKey: 'getUserVersion', + enabled: !isLoggedIn, + }, + { + queryFn: getUserPreference, + queryKey: 'getUserPreference', + enabled: !isLoggedIn, + }, + ]); - if (versionResponse.error || userPrefResponse.error) { + if ( + versionResponse.status === 'error' || + userPrefResponse.status === 'error' + ) { return ( - {versionResponse.errorMessage || - userPrefResponse.errorMessage || - 'Somehthing went wrong'} + {versionResponse.data?.error || + userPrefResponse.data?.error || + t('something_went_wrong')} ); } if ( - versionResponse.loading || - versionResponse.payload === undefined || - userPrefResponse.loading || - userPrefResponse.payload === undefined + versionResponse.status === 'loading' || + userPrefResponse.status === 'loading' || + !(versionResponse.data && versionResponse.data.payload) || + !(userPrefResponse.data && userPrefResponse.data.payload) ) { - return ; + return ; } - const { version } = versionResponse.payload; + const { version } = versionResponse.data.payload; - const userpref = userPrefResponse.payload; + const userpref = userPrefResponse.data.payload; return ; } From d102c94670de8cea3179213748a63006f533d73c Mon Sep 17 00:00:00 2001 From: palash-signoz Date: Fri, 8 Apr 2022 14:05:16 +0530 Subject: [PATCH 005/106] bug: unused import is removed and two unwanted eslint rule is removed (#968) --- frontend/.eslintrc.js | 2 -- .../Trace/Search/AllTags/Tag/TagValue.tsx | 2 -- .../src/container/Trace/TraceTable/index.tsx | 1 - .../src/container/TriggeredAlerts/index.tsx | 1 - frontend/src/wdyr.ts | 18 ++++++++++-------- frontend/tsconfig.json | 10 +++++++++- 6 files changed, 19 insertions(+), 15 deletions(-) diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js index fb9d999579..b0234bbee3 100644 --- a/frontend/.eslintrc.js +++ b/frontend/.eslintrc.js @@ -105,8 +105,6 @@ module.exports = { // eslint rules need to remove 'no-shadow': 'off', '@typescript-eslint/no-shadow': 'off', - 'global-require': 'off', - '@typescript-eslint/no-var-requires': 'off', 'import/no-cycle': 'off', 'prettier/prettier': [ diff --git a/frontend/src/container/Trace/Search/AllTags/Tag/TagValue.tsx b/frontend/src/container/Trace/Search/AllTags/Tag/TagValue.tsx index d58675ffad..1d90703246 100644 --- a/frontend/src/container/Trace/Search/AllTags/Tag/TagValue.tsx +++ b/frontend/src/container/Trace/Search/AllTags/Tag/TagValue.tsx @@ -1,5 +1,4 @@ import { Select } from 'antd'; -import { DefaultOptionType } from 'antd/lib/select'; import getTagValue from 'api/trace/getTagValue'; import useFetch from 'hooks/useFetch'; import React from 'react'; @@ -9,7 +8,6 @@ import { PayloadProps, Props } from 'types/api/trace/getTagValue'; import { GlobalReducer } from 'types/reducer/globalTime'; import { TraceReducer } from 'types/reducer/trace'; -import { Value } from '.'; import { SelectComponent } from './styles'; function TagValue(props: TagValueProps): JSX.Element { diff --git a/frontend/src/container/Trace/TraceTable/index.tsx b/frontend/src/container/Trace/TraceTable/index.tsx index b68a180ad7..951c981ddd 100644 --- a/frontend/src/container/Trace/TraceTable/index.tsx +++ b/frontend/src/container/Trace/TraceTable/index.tsx @@ -25,7 +25,6 @@ function TraceTable(): JSX.Element { selectedTags, filterLoading, userSelectedFilter, - filter, isFilterExclude, filterToFetchData, } = useSelector((state) => state.traces); diff --git a/frontend/src/container/TriggeredAlerts/index.tsx b/frontend/src/container/TriggeredAlerts/index.tsx index 88ebb2e138..a3762762df 100644 --- a/frontend/src/container/TriggeredAlerts/index.tsx +++ b/frontend/src/container/TriggeredAlerts/index.tsx @@ -2,7 +2,6 @@ import getTriggeredApi from 'api/alerts/getTriggered'; import Spinner from 'components/Spinner'; import { State } from 'hooks/useFetch'; import React, { useCallback, useEffect, useState } from 'react'; -import { Alerts } from 'types/api/alerts/getAll'; import { PayloadProps } from 'types/api/alerts/getTriggered'; import TriggerComponent from './TriggeredAlert'; diff --git a/frontend/src/wdyr.ts b/frontend/src/wdyr.ts index 8ba678a4d6..a69c889035 100644 --- a/frontend/src/wdyr.ts +++ b/frontend/src/wdyr.ts @@ -1,15 +1,17 @@ +/* eslint-disable global-require */ /// // ^ https://github.com/welldone-software/why-did-you-render/issues/161 import React from 'react'; if (process.env.NODE_ENV === 'development') { - const whyDidYouRender = require('@welldone-software/why-did-you-render'); - whyDidYouRender(React, { - trackAllPureComponents: false, - trackExtraHooks: [[require('react-redux/lib'), 'useSelector']], - include: [/^ConnectFunction/], - logOnDifferentValues: true, + import('@welldone-software/why-did-you-render').then((whyDidYouRender) => { + whyDidYouRender.default(React, { + trackAllPureComponents: true, + trackHooks: true, + // https://github.com/welldone-software/why-did-you-render/issues/85#issuecomment-596682587 + trackExtraHooks: [require('react-redux/lib'), 'useSelector'], + include: [/^ConnectFunction/], + logOnDifferentValues: true, + }); }); } - -export default ''; diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index fcc3663f1b..4790ab4740 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -22,5 +22,13 @@ "plugins": [{ "name": "typescript-plugin-css-modules" }] }, "exclude": ["node_modules"], - "include": ["./src", "./babel.config.js", "./jest.config.ts"] + "include": [ + "./src", + "./babel.config.js", + "./jest.config.ts", + "./.eslintrc.js", + "./__mocks__", + "./conf/default.conf", + "./public" + ] } From a6c41f312df9d269ab766c572fc24013d22d426b Mon Sep 17 00:00:00 2001 From: Pranay Prateek Date: Fri, 8 Apr 2022 10:09:24 -0700 Subject: [PATCH 006/106] Update CONTRIBUTING.md --- CONTRIBUTING.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d0e7a7169b..26a1fb4ed4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -18,10 +18,10 @@ Need to update [https://github.com/SigNoz/signoz/tree/main/frontend](https://git ### Contribute to Frontend with Docker installation of SigNoz - `git clone https://github.com/SigNoz/signoz.git && cd signoz` -- comment out frontend service section at `deploy/docker/clickhouse-setup/docker-compose.yaml#L59` +- comment out frontend service section at `deploy/docker/clickhouse-setup/docker-compose.yaml` - run `cd deploy` to move to deploy directory - Install signoz locally without the frontend - - Add below configuration to query-service section at `docker/clickhouse-setup/docker-compose.yaml#L36` + - Add below configuration to query-service section at `docker/clickhouse-setup/docker-compose.yaml` ```docker ports: @@ -55,9 +55,9 @@ Need to update [https://github.com/SigNoz/signoz/tree/main/pkg/query-service](ht - git clone https://github.com/SigNoz/signoz.git - run `cd signoz` to move to signoz directory - run `sudo make dev-setup` to configure local setup to run query-service -- comment out frontend service section at `docker/clickhouse-setup/docker-compose.yaml#L45` -- comment out query-service section at `docker/clickhouse-setup/docker-compose.yaml#L28` -- add below configuration to clickhouse section at `docker/clickhouse-setup/docker-compose.yaml#L6` +- comment out frontend service section at `docker/clickhouse-setup/docker-compose.yaml` +- comment out query-service section at `docker/clickhouse-setup/docker-compose.yaml` +- add below configuration to clickhouse section at `docker/clickhouse-setup/docker-compose.yaml` ```docker expose: - 9000 From dc9508269d9f12d6de2a8d5308a6a203353bad59 Mon Sep 17 00:00:00 2001 From: Pranay Prateek Date: Fri, 8 Apr 2022 10:12:58 -0700 Subject: [PATCH 007/106] Update commentLinesForSetup.sh Updating frontend service line number --- .scripts/commentLinesForSetup.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.scripts/commentLinesForSetup.sh b/.scripts/commentLinesForSetup.sh index 7ea6b468ad..c0dfd40e9f 100644 --- a/.scripts/commentLinesForSetup.sh +++ b/.scripts/commentLinesForSetup.sh @@ -4,4 +4,4 @@ # Update the Line Numbers when deploy/docker/clickhouse-setup/docker-compose.yaml chnages. # Docs Ref.: https://github.com/SigNoz/signoz/blob/main/CONTRIBUTING.md#contribute-to-frontend-with-docker-installation-of-signoz -sed -i 38,70's/.*/# &/' .././deploy/docker/clickhouse-setup/docker-compose.yaml +sed -i 38,62's/.*/# &/' .././deploy/docker/clickhouse-setup/docker-compose.yaml From f6aece6349cd82d6531d414079b6de14ea0fbfbb Mon Sep 17 00:00:00 2001 From: Pranay Prateek Date: Fri, 8 Apr 2022 10:13:36 -0700 Subject: [PATCH 008/106] Update CONTRIBUTING.md --- CONTRIBUTING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 26a1fb4ed4..6205d85884 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -18,10 +18,10 @@ Need to update [https://github.com/SigNoz/signoz/tree/main/frontend](https://git ### Contribute to Frontend with Docker installation of SigNoz - `git clone https://github.com/SigNoz/signoz.git && cd signoz` -- comment out frontend service section at `deploy/docker/clickhouse-setup/docker-compose.yaml` +- comment out frontend service section at `deploy/docker/clickhouse-setup/docker-compose.yaml#L62` - run `cd deploy` to move to deploy directory - Install signoz locally without the frontend - - Add below configuration to query-service section at `docker/clickhouse-setup/docker-compose.yaml` + - Add below configuration to query-service section at `docker/clickhouse-setup/docker-compose.yaml#L38` ```docker ports: From d454482f433341e5770f0b46ab90cd7397ec9927 Mon Sep 17 00:00:00 2001 From: palash-signoz Date: Mon, 11 Apr 2022 17:17:31 +0530 Subject: [PATCH 009/106] wdyr is updated (#981) --- frontend/src/wdyr.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/frontend/src/wdyr.ts b/frontend/src/wdyr.ts index a69c889035..e64dcca566 100644 --- a/frontend/src/wdyr.ts +++ b/frontend/src/wdyr.ts @@ -1,17 +1,17 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ /* eslint-disable global-require */ /// // ^ https://github.com/welldone-software/why-did-you-render/issues/161 import React from 'react'; if (process.env.NODE_ENV === 'development') { - import('@welldone-software/why-did-you-render').then((whyDidYouRender) => { - whyDidYouRender.default(React, { - trackAllPureComponents: true, - trackHooks: true, - // https://github.com/welldone-software/why-did-you-render/issues/85#issuecomment-596682587 - trackExtraHooks: [require('react-redux/lib'), 'useSelector'], - include: [/^ConnectFunction/], - logOnDifferentValues: true, - }); + const whyDidYouRender = require('@welldone-software/why-did-you-render'); + whyDidYouRender(React, { + trackAllPureComponents: false, + trackExtraHooks: [[require('react-redux/lib'), 'useSelector']], + include: [/^ConnectFunction/], + logOnDifferentValues: true, }); } + +export default ''; From 61d01fa2d5710cf16080ca987667f7d61f04dbd9 Mon Sep 17 00:00:00 2001 From: Ankit Nayan Date: Fri, 15 Apr 2022 11:32:19 +0530 Subject: [PATCH 010/106] feat: added action to verify that every pr has a linked issue --- .github/workflows/pr_verify_linked_issue.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 .github/workflows/pr_verify_linked_issue.yml diff --git a/.github/workflows/pr_verify_linked_issue.yml b/.github/workflows/pr_verify_linked_issue.yml new file mode 100644 index 0000000000..adf2718a46 --- /dev/null +++ b/.github/workflows/pr_verify_linked_issue.yml @@ -0,0 +1,20 @@ +# This workflow will inspect a pull request to ensure there is a linked issue or a +# valid issue is mentioned in the body. If neither is present it fails the check and adds +# a comment alerting users of this missing requirement. +name: VerifyIssue + +on: + pull_request: + types: [edited, synchronize, opened, reopened] + check_run: + +jobs: + verify_linked_issue: + runs-on: ubuntu-latest + name: Ensure Pull Request has a linked issue. + steps: + - name: Verify Linked Issue + uses: hattan/verify-linked-issue-action@v1.1.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + From b2eec25f330e78c0f11bd6cd625f14e94b74f44a Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Mon, 18 Apr 2022 14:12:49 +0530 Subject: [PATCH 011/106] (bugfix): remove validation on post data id --- pkg/query-service/app/dashboards/model.go | 11 ++--------- pkg/query-service/app/http_handler.go | 9 ++------- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/pkg/query-service/app/dashboards/model.go b/pkg/query-service/app/dashboards/model.go index 77b66046c0..0b4dc6847c 100644 --- a/pkg/query-service/app/dashboards/model.go +++ b/pkg/query-service/app/dashboards/model.go @@ -182,9 +182,7 @@ func GetDashboard(uuid string) (*Dashboard, *model.ApiError) { return &dashboard, nil } -func UpdateDashboard(data *map[string]interface{}) (*Dashboard, *model.ApiError) { - - uuid := (*data)["uuid"].(string) +func UpdateDashboard(uuid string, data *map[string]interface{}) (*Dashboard, *model.ApiError) { map_data, err := json.Marshal(data) if err != nil { @@ -224,12 +222,7 @@ func (d *Dashboard) UpdateSlug() { func IsPostDataSane(data *map[string]interface{}) error { - val, ok := (*data)["uuid"] - if !ok || val == nil { - return fmt.Errorf("uuid not found in post data") - } - - val, ok = (*data)["title"] + val, ok := (*data)["title"] if !ok || val == nil { return fmt.Errorf("title not found in post data") } diff --git a/pkg/query-service/app/http_handler.go b/pkg/query-service/app/http_handler.go index 771637935a..f3884127b5 100644 --- a/pkg/query-service/app/http_handler.go +++ b/pkg/query-service/app/http_handler.go @@ -13,10 +13,10 @@ import ( "github.com/prometheus/prometheus/promql" "go.signoz.io/query-service/app/dashboards" "go.signoz.io/query-service/dao/interfaces" + am "go.signoz.io/query-service/integrations/alertManager" "go.signoz.io/query-service/model" "go.signoz.io/query-service/telemetry" "go.signoz.io/query-service/version" - am "go.signoz.io/query-service/integrations/alertManager" "go.uber.org/zap" ) @@ -337,12 +337,7 @@ func (aH *APIHandler) updateDashboard(w http.ResponseWriter, r *http.Request) { return } - if postData["uuid"] != uuid { - aH.respondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("uuid in request param and uuid in request body do not match")}, "Error reading request body") - return - } - - dashboard, apiError := dashboards.UpdateDashboard(&postData) + dashboard, apiError := dashboards.UpdateDashboard(uuid, &postData) if apiError != nil { aH.respondError(w, apiError, nil) From 93638d5615a6f79f9019b7b8dc89c8ad9241bba0 Mon Sep 17 00:00:00 2001 From: palash-signoz Date: Mon, 18 Apr 2022 15:24:51 +0530 Subject: [PATCH 012/106] Use fetch fix (#995) * feat: useFetch in tag value is removed and moved to use query * feat: useFetch in all channels is removed and moved to use query * feat: useFetch in edit rule is removed and moved to use query * feat: useFetch in general settings is removed and moved to use query * feat: useFetch in all alerts is changed into use query --- .../GeneralSettings/GeneralSettings.tsx | 323 +++++++++++++++++ .../src/container/GeneralSettings/index.tsx | 339 ++---------------- .../src/container/ListAlertRules/index.tsx | 20 +- .../Trace/Search/AllTags/Tag/TagValue.tsx | 26 +- frontend/src/pages/ChannelsEdit/index.tsx | 27 +- frontend/src/pages/EditRules/index.tsx | 23 +- 6 files changed, 405 insertions(+), 353 deletions(-) create mode 100644 frontend/src/container/GeneralSettings/GeneralSettings.tsx diff --git a/frontend/src/container/GeneralSettings/GeneralSettings.tsx b/frontend/src/container/GeneralSettings/GeneralSettings.tsx new file mode 100644 index 0000000000..768322d91d --- /dev/null +++ b/frontend/src/container/GeneralSettings/GeneralSettings.tsx @@ -0,0 +1,323 @@ +import { Button, Col, Modal, notification, Row, Typography } from 'antd'; +import setRetentionApi from 'api/settings/setRetention'; +import TextToolTip from 'components/TextToolTip'; +import find from 'lodash-es/find'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + IDiskType, + PayloadProps as GetDisksPayload, +} from 'types/api/disks/getDisks'; +import { PayloadProps as GetRetentionPayload } from 'types/api/settings/getRetention'; + +import Retention from './Retention'; +import { ButtonContainer, ErrorText, ErrorTextContainer } from './styles'; + +type NumberOrNull = number | null; + +function GeneralSettings({ + ttlValuesPayload, + getAvailableDiskPayload, +}: GeneralSettingsProps): JSX.Element { + const { t } = useTranslation(); + const [modal, setModal] = useState(false); + const [postApiLoading, setPostApiLoading] = useState(false); + + const [availableDisks] = useState(getAvailableDiskPayload); + + const [currentTTLValues, setCurrentTTLValues] = useState(ttlValuesPayload); + + const [ + metricsTotalRetentionPeriod, + setMetricsTotalRetentionPeriod, + ] = useState(null); + const [ + metricsS3RetentionPeriod, + setMetricsS3RetentionPeriod, + ] = useState(null); + const [ + tracesTotalRetentionPeriod, + setTracesTotalRetentionPeriod, + ] = useState(null); + const [ + tracesS3RetentionPeriod, + setTracesS3RetentionPeriod, + ] = useState(null); + + useEffect(() => { + if (currentTTLValues) { + setMetricsTotalRetentionPeriod(currentTTLValues.metrics_ttl_duration_hrs); + setMetricsS3RetentionPeriod( + currentTTLValues.metrics_move_ttl_duration_hrs + ? currentTTLValues.metrics_move_ttl_duration_hrs + : null, + ); + setTracesTotalRetentionPeriod(currentTTLValues.traces_ttl_duration_hrs); + setTracesS3RetentionPeriod( + currentTTLValues.traces_move_ttl_duration_hrs + ? currentTTLValues.traces_move_ttl_duration_hrs + : null, + ); + } + }, [currentTTLValues]); + + const onModalToggleHandler = (): void => { + setModal((modal) => !modal); + }; + + const onClickSaveHandler = useCallback(() => { + onModalToggleHandler(); + }, []); + + const s3Enabled = useMemo( + () => !!find(availableDisks, (disks: IDiskType) => disks?.type === 's3'), + [availableDisks], + ); + + const renderConfig = [ + { + name: 'Metrics', + retentionFields: [ + { + name: t('settings.total_retention_period'), + value: metricsTotalRetentionPeriod, + setValue: setMetricsTotalRetentionPeriod, + }, + { + name: t('settings.move_to_s3'), + value: metricsS3RetentionPeriod, + setValue: setMetricsS3RetentionPeriod, + hide: !s3Enabled, + }, + ], + }, + { + name: 'Traces', + retentionFields: [ + { + name: t('settings.total_retention_period'), + value: tracesTotalRetentionPeriod, + setValue: setTracesTotalRetentionPeriod, + }, + { + name: t('settings.move_to_s3'), + value: tracesS3RetentionPeriod, + setValue: setTracesS3RetentionPeriod, + hide: !s3Enabled, + }, + ], + }, + ].map((category): JSX.Element | null => { + if ( + Array.isArray(category.retentionFields) && + category.retentionFields.length > 0 + ) { + return ( + + {category.name} + + {category.retentionFields.map((retentionField) => ( + + ))} + + ); + } + return null; + }); + + // eslint-disable-next-line sonarjs/cognitive-complexity + const onOkHandler = async (): Promise => { + try { + setPostApiLoading(true); + const apiCalls = []; + + if ( + !( + currentTTLValues?.metrics_move_ttl_duration_hrs === + metricsS3RetentionPeriod && + currentTTLValues.metrics_ttl_duration_hrs === metricsTotalRetentionPeriod + ) + ) { + apiCalls.push(() => + setRetentionApi({ + type: 'metrics', + totalDuration: `${metricsTotalRetentionPeriod || -1}h`, + coldStorage: s3Enabled ? 's3' : null, + toColdDuration: `${metricsS3RetentionPeriod || -1}h`, + }), + ); + } else { + apiCalls.push(() => Promise.resolve(null)); + } + + if ( + !( + currentTTLValues?.traces_move_ttl_duration_hrs === + tracesS3RetentionPeriod && + currentTTLValues.traces_ttl_duration_hrs === tracesTotalRetentionPeriod + ) + ) { + apiCalls.push(() => + setRetentionApi({ + type: 'traces', + totalDuration: `${tracesTotalRetentionPeriod || -1}h`, + coldStorage: s3Enabled ? 's3' : null, + toColdDuration: `${tracesS3RetentionPeriod || -1}h`, + }), + ); + } else { + apiCalls.push(() => Promise.resolve(null)); + } + const apiCallSequence = ['metrics', 'traces']; + const apiResponses = await Promise.all(apiCalls.map((api) => api())); + + apiResponses.forEach((apiResponse, idx) => { + const name = apiCallSequence[idx]; + if (apiResponse) { + if (apiResponse.statusCode === 200) { + notification.success({ + message: 'Success!', + placement: 'topRight', + + description: t('settings.retention_success_message', { name }), + }); + } else { + notification.error({ + message: 'Error', + description: t('settings.retention_error_message', { name }), + placement: 'topRight', + }); + } + } + }); + onModalToggleHandler(); + setPostApiLoading(false); + } catch (error) { + notification.error({ + message: 'Error', + description: t('settings.retention_failed_message'), + placement: 'topRight', + }); + } + // Updates the currentTTL Values in order to avoid pushing the same values. + setCurrentTTLValues({ + metrics_ttl_duration_hrs: metricsTotalRetentionPeriod || -1, + metrics_move_ttl_duration_hrs: metricsS3RetentionPeriod || -1, + traces_ttl_duration_hrs: tracesTotalRetentionPeriod || -1, + traces_move_ttl_duration_hrs: tracesS3RetentionPeriod || -1, + }); + + setModal(false); + }; + + // eslint-disable-next-line sonarjs/cognitive-complexity + const [isDisabled, errorText] = useMemo((): [boolean, string] => { + // Various methods to return dynamic error message text. + const messages = { + compareError: (name: string | number): string => + t('settings.retention_comparison_error', { name }), + nullValueError: (name: string | number): string => + t('settings.retention_null_value_error', { name }), + }; + + // Defaults to button not disabled and empty error message text. + let isDisabled = false; + let errorText = ''; + + if (s3Enabled) { + if ( + (metricsTotalRetentionPeriod || metricsS3RetentionPeriod) && + Number(metricsTotalRetentionPeriod) <= Number(metricsS3RetentionPeriod) + ) { + isDisabled = true; + errorText = messages.compareError('metrics'); + } else if ( + (tracesTotalRetentionPeriod || tracesS3RetentionPeriod) && + Number(tracesTotalRetentionPeriod) <= Number(tracesS3RetentionPeriod) + ) { + isDisabled = true; + errorText = messages.compareError('traces'); + } + } + + if (!metricsTotalRetentionPeriod || !tracesTotalRetentionPeriod) { + isDisabled = true; + if (!metricsTotalRetentionPeriod && !tracesTotalRetentionPeriod) { + errorText = messages.nullValueError('metrics and traces'); + } else if (!metricsTotalRetentionPeriod) { + errorText = messages.nullValueError('metrics'); + } else if (!tracesTotalRetentionPeriod) { + errorText = messages.nullValueError('traces'); + } + } + if ( + currentTTLValues?.metrics_ttl_duration_hrs === metricsTotalRetentionPeriod && + currentTTLValues.metrics_move_ttl_duration_hrs === + metricsS3RetentionPeriod && + currentTTLValues.traces_ttl_duration_hrs === tracesTotalRetentionPeriod && + currentTTLValues.traces_move_ttl_duration_hrs === tracesS3RetentionPeriod + ) { + isDisabled = true; + } + return [isDisabled, errorText]; + }, [ + currentTTLValues, + metricsS3RetentionPeriod, + metricsTotalRetentionPeriod, + s3Enabled, + t, + tracesS3RetentionPeriod, + tracesTotalRetentionPeriod, + ]); + + return ( + + {Element} + + + {errorText && {errorText}} + + + {renderConfig} + + + {t('settings.retention_confirmation_description')} + + + + + + + ); +} + +interface GeneralSettingsProps { + ttlValuesPayload: GetRetentionPayload; + getAvailableDiskPayload: GetDisksPayload; +} + +export default GeneralSettings; diff --git a/frontend/src/container/GeneralSettings/index.tsx b/frontend/src/container/GeneralSettings/index.tsx index 6a76282192..633c4822e6 100644 --- a/frontend/src/container/GeneralSettings/index.tsx +++ b/frontend/src/container/GeneralSettings/index.tsx @@ -1,331 +1,52 @@ -/* eslint-disable sonarjs/cognitive-complexity */ -import { Button, Col, Modal, notification, Row, Typography } from 'antd'; +import { Typography } from 'antd'; import getDisks from 'api/disks/getDisks'; import getRetentionPeriodApi from 'api/settings/getRetention'; -import setRetentionApi from 'api/settings/setRetention'; import Spinner from 'components/Spinner'; -import TextToolTip from 'components/TextToolTip'; -import useFetch from 'hooks/useFetch'; -import { find } from 'lodash-es'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React from 'react'; import { useTranslation } from 'react-i18next'; -import { IDiskType } from 'types/api/disks/getDisks'; -import { PayloadProps } from 'types/api/settings/getRetention'; +import { useQueries } from 'react-query'; -import Retention from './Retention'; -import { ButtonContainer, ErrorText, ErrorTextContainer } from './styles'; +import GeneralSettingsContainer from './GeneralSettings'; function GeneralSettings(): JSX.Element { - const { t } = useTranslation(); - const [notifications, Element] = notification.useNotification(); - const [modal, setModal] = useState(false); - const [postApiLoading, setPostApiLoading] = useState(false); - - const [availableDisks, setAvailableDisks] = useState(null); - - useEffect(() => { - getDisks().then((response) => setAvailableDisks(response.payload)); - }, []); - - const { payload, loading, error, errorMessage } = useFetch< - PayloadProps, - undefined - >(getRetentionPeriodApi, undefined); - - const [currentTTLValues, setCurrentTTLValues] = useState(payload); - - useEffect(() => { - setCurrentTTLValues(payload); - }, [payload]); - - const [metricsTotalRetentionPeriod, setMetricsTotalRetentionPeriod] = useState< - number | null - >(null); - const [metricsS3RetentionPeriod, setMetricsS3RetentionPeriod] = useState< - number | null - >(null); - const [tracesTotalRetentionPeriod, setTracesTotalRetentionPeriod] = useState< - number | null - >(null); - const [tracesS3RetentionPeriod, setTracesS3RetentionPeriod] = useState< - number | null - >(null); - - useEffect(() => { - if (currentTTLValues) { - setMetricsTotalRetentionPeriod(currentTTLValues.metrics_ttl_duration_hrs); - setMetricsS3RetentionPeriod( - currentTTLValues.metrics_move_ttl_duration_hrs - ? currentTTLValues.metrics_move_ttl_duration_hrs - : null, - ); - setTracesTotalRetentionPeriod(currentTTLValues.traces_ttl_duration_hrs); - setTracesS3RetentionPeriod( - currentTTLValues.traces_move_ttl_duration_hrs - ? currentTTLValues.traces_move_ttl_duration_hrs - : null, - ); - } - console.log({ changed: currentTTLValues }); - }, [currentTTLValues]); - - const onModalToggleHandler = (): void => { - setModal((modal) => !modal); - }; - - const onClickSaveHandler = useCallback(() => { - onModalToggleHandler(); - }, []); - - const s3Enabled = useMemo( - () => !!find(availableDisks, (disks: IDiskType) => disks?.type === 's3'), - [availableDisks], - ); - - const renderConfig = [ + const { t } = useTranslation('common'); + const [getRetentionPeriodApiResponse, getDisksResponse] = useQueries([ { - name: 'Metrics', - retentionFields: [ - { - name: t('settings.total_retention_period'), - value: metricsTotalRetentionPeriod, - setValue: setMetricsTotalRetentionPeriod, - }, - { - name: t('settings.move_to_s3'), - value: metricsS3RetentionPeriod, - setValue: setMetricsS3RetentionPeriod, - hide: !s3Enabled, - }, - ], + queryFn: getRetentionPeriodApi, + queryKey: 'getRetentionPeriodApi', }, { - name: 'Traces', - retentionFields: [ - { - name: t('settings.total_retention_period'), - value: tracesTotalRetentionPeriod, - setValue: setTracesTotalRetentionPeriod, - }, - { - name: t('settings.move_to_s3'), - value: tracesS3RetentionPeriod, - setValue: setTracesS3RetentionPeriod, - hide: !s3Enabled, - }, - ], + queryFn: getDisks, + queryKey: 'getDisks', }, - ].map((category): JSX.Element | null => { - if ( - Array.isArray(category.retentionFields) && - category.retentionFields.length > 0 - ) { - return ( - - {category.name} - - {category.retentionFields.map((retentionField) => ( - - ))} - - ); - } - return null; - }); - - const onOkHandler = async (): Promise => { - try { - setPostApiLoading(true); - const apiCalls = []; - - if ( - !( - currentTTLValues?.metrics_move_ttl_duration_hrs === - metricsS3RetentionPeriod && - currentTTLValues.metrics_ttl_duration_hrs === metricsTotalRetentionPeriod - ) - ) { - apiCalls.push(() => - setRetentionApi({ - type: 'metrics', - totalDuration: `${metricsTotalRetentionPeriod || -1}h`, - coldStorage: s3Enabled ? 's3' : null, - toColdDuration: `${metricsS3RetentionPeriod || -1}h`, - }), - ); - } else { - apiCalls.push(() => Promise.resolve(null)); - } - - if ( - !( - currentTTLValues?.traces_move_ttl_duration_hrs === - tracesS3RetentionPeriod && - currentTTLValues.traces_ttl_duration_hrs === tracesTotalRetentionPeriod - ) - ) { - apiCalls.push(() => - setRetentionApi({ - type: 'traces', - totalDuration: `${tracesTotalRetentionPeriod || -1}h`, - coldStorage: s3Enabled ? 's3' : null, - toColdDuration: `${tracesS3RetentionPeriod || -1}h`, - }), - ); - } else { - apiCalls.push(() => Promise.resolve(null)); - } - const apiCallSequence = ['metrics', 'traces']; - const apiResponses = await Promise.all(apiCalls.map((api) => api())); - - apiResponses.forEach((apiResponse, idx) => { - const name = apiCallSequence[idx]; - if (apiResponse) { - if (apiResponse.statusCode === 200) { - notifications.success({ - message: 'Success!', - placement: 'topRight', - - description: t('settings.retention_success_message', { name }), - }); - } else { - notifications.error({ - message: 'Error', - description: t('settings.retention_error_message', { name }), - placement: 'topRight', - }); - } - } - }); - onModalToggleHandler(); - setPostApiLoading(false); - } catch (error) { - notifications.error({ - message: 'Error', - description: t('settings.retention_failed_message'), - placement: 'topRight', - }); - } - // Updates the currentTTL Values in order to avoid pushing the same values. - setCurrentTTLValues({ - metrics_ttl_duration_hrs: metricsTotalRetentionPeriod || -1, - metrics_move_ttl_duration_hrs: metricsS3RetentionPeriod || -1, - traces_ttl_duration_hrs: tracesTotalRetentionPeriod || -1, - traces_move_ttl_duration_hrs: tracesS3RetentionPeriod || -1, - }); - - setModal(false); - }; - - const [isDisabled, errorText] = useMemo((): [boolean, string] => { - // Various methods to return dynamic error message text. - const messages = { - compareError: (name: string | number): string => - t('settings.retention_comparison_error', { name }), - nullValueError: (name: string | number): string => - t('settings.retention_null_value_error', { name }), - }; - - // Defaults to button not disabled and empty error message text. - let isDisabled = false; - let errorText = ''; - - if (s3Enabled) { - if ( - (metricsTotalRetentionPeriod || metricsS3RetentionPeriod) && - Number(metricsTotalRetentionPeriod) <= Number(metricsS3RetentionPeriod) - ) { - isDisabled = true; - errorText = messages.compareError('metrics'); - } else if ( - (tracesTotalRetentionPeriod || tracesS3RetentionPeriod) && - Number(tracesTotalRetentionPeriod) <= Number(tracesS3RetentionPeriod) - ) { - isDisabled = true; - errorText = messages.compareError('traces'); - } - } - - if (!metricsTotalRetentionPeriod || !tracesTotalRetentionPeriod) { - isDisabled = true; - if (!metricsTotalRetentionPeriod && !tracesTotalRetentionPeriod) { - errorText = messages.nullValueError('metrics and traces'); - } else if (!metricsTotalRetentionPeriod) { - errorText = messages.nullValueError('metrics'); - } else if (!tracesTotalRetentionPeriod) { - errorText = messages.nullValueError('traces'); - } - } - if ( - currentTTLValues?.metrics_ttl_duration_hrs === metricsTotalRetentionPeriod && - currentTTLValues.metrics_move_ttl_duration_hrs === - metricsS3RetentionPeriod && - currentTTLValues.traces_ttl_duration_hrs === tracesTotalRetentionPeriod && - currentTTLValues.traces_move_ttl_duration_hrs === tracesS3RetentionPeriod - ) { - isDisabled = true; - } - return [isDisabled, errorText]; - }, [ - currentTTLValues, - metricsS3RetentionPeriod, - metricsTotalRetentionPeriod, - s3Enabled, - t, - tracesS3RetentionPeriod, - tracesTotalRetentionPeriod, ]); - if (error) { - return {errorMessage}; + if (getRetentionPeriodApiResponse.isError || getDisksResponse.isError) { + return ( + + {getRetentionPeriodApiResponse.data?.error || + getDisksResponse.data?.error || + t('something_went_wrong')} + + ); } - if (loading || currentTTLValues === undefined) { + if ( + getRetentionPeriodApiResponse.isLoading || + getDisksResponse.isLoading || + !getDisksResponse.data?.payload || + !getRetentionPeriodApiResponse.data?.payload + ) { return ; } return ( - - {Element} - - - {errorText && {errorText}} - - - {renderConfig} - - - {t('settings.retention_confirmation_description')} - - - - - - + ); } diff --git a/frontend/src/container/ListAlertRules/index.tsx b/frontend/src/container/ListAlertRules/index.tsx index ce44e871e9..8fd131736c 100644 --- a/frontend/src/container/ListAlertRules/index.tsx +++ b/frontend/src/container/ListAlertRules/index.tsx @@ -1,29 +1,29 @@ import getAll from 'api/alerts/getAll'; import Spinner from 'components/Spinner'; -import useFetch from 'hooks/useFetch'; import React from 'react'; -import { PayloadProps } from 'types/api/alerts/getAll'; +import { useTranslation } from 'react-i18next'; +import { useQuery } from 'react-query'; import ListAlert from './ListAlert'; function ListAlertRules(): JSX.Element { - const { loading, payload, error, errorMessage } = useFetch< - PayloadProps, - undefined - >(getAll); + const { t } = useTranslation('common'); + const { data, isError, isLoading } = useQuery('allAlerts', { + queryFn: getAll, + }); - if (error) { - return
{errorMessage}
; + if (isError) { + return
{data?.error || t('something_went_wrong')}
; } - if (loading || payload === undefined) { + if (isLoading || !data?.payload) { return ; } return ( ); diff --git a/frontend/src/container/Trace/Search/AllTags/Tag/TagValue.tsx b/frontend/src/container/Trace/Search/AllTags/Tag/TagValue.tsx index 1d90703246..756bb54225 100644 --- a/frontend/src/container/Trace/Search/AllTags/Tag/TagValue.tsx +++ b/frontend/src/container/Trace/Search/AllTags/Tag/TagValue.tsx @@ -1,10 +1,9 @@ import { Select } from 'antd'; import getTagValue from 'api/trace/getTagValue'; -import useFetch from 'hooks/useFetch'; import React from 'react'; +import { useQuery } from 'react-query'; import { useSelector } from 'react-redux'; import { AppState } from 'store/reducers'; -import { PayloadProps, Props } from 'types/api/trace/getTagValue'; import { GlobalReducer } from 'types/reducer/globalTime'; import { TraceReducer } from 'types/reducer/trace'; @@ -22,11 +21,17 @@ function TagValue(props: TagValueProps): JSX.Element { (state) => state.globalTime, ); - const valueSuggestion = useFetch(getTagValue, { - end: globalReducer.maxTime, - start: globalReducer.minTime, - tagKey, - }); + const { isLoading, data } = useQuery( + ['tagKey', globalReducer.minTime, globalReducer.maxTime, tagKey], + { + queryFn: () => + getTagValue({ + end: globalReducer.maxTime, + start: globalReducer.minTime, + tagKey, + }), + }, + ); return ( - {valueSuggestion.payload && - valueSuggestion.payload.map((suggestion) => ( + {data && + data.payload && + data.payload.map((suggestion) => ( {suggestion.tagValues} diff --git a/frontend/src/pages/ChannelsEdit/index.tsx b/frontend/src/pages/ChannelsEdit/index.tsx index 4048eda81c..dc2804da53 100644 --- a/frontend/src/pages/ChannelsEdit/index.tsx +++ b/frontend/src/pages/ChannelsEdit/index.tsx @@ -8,32 +8,33 @@ import { WebhookType, } from 'container/CreateAlertChannels/config'; import EditAlertChannels from 'container/EditAlertChannels'; -import useFetch from 'hooks/useFetch'; import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useQuery } from 'react-query'; import { useParams } from 'react-router-dom'; -import { PayloadProps, Props } from 'types/api/channels/get'; function ChannelsEdit(): JSX.Element { const { id } = useParams(); + const { t } = useTranslation(); - const { errorMessage, payload, error, loading } = useFetch< - PayloadProps, - Props - >(get, { - id, + const { isLoading, isError, data } = useQuery(['getChannel', id], { + queryFn: () => + get({ + id, + }), }); - if (error) { - return {errorMessage}; + if (isError) { + return {data?.error || t('something_went_wrong')}; } - if (loading || payload === undefined) { + if (isLoading || !data?.payload) { return ; } - const { data } = payload; + const { data: ChannelData } = data.payload; - const value = JSON.parse(data); + const value = JSON.parse(ChannelData); let type = ''; let channel: SlackChannel & WebhookChannel = { name: '' }; @@ -57,7 +58,7 @@ function ChannelsEdit(): JSX.Element { } type = WebhookType; } - console.log('channel:', channel); + return ( (); + const { t } = useTranslation('common'); - const { loading, error, payload, errorMessage } = useFetch< - PayloadProps, - Props - >(get, { - id: parseInt(ruleId, 10), + const { isLoading, data, isError } = useQuery(['ruleId', ruleId], { + queryFn: () => + get({ + id: parseInt(ruleId, 10), + }), }); - if (error) { - return
{errorMessage}
; + if (isError) { + return
{data?.error || t('something_went_wrong')}
; } - if (loading || payload === undefined) { + if (isLoading || !data?.payload) { return ; } - return ; + return ; } interface EditRulesParam { From 08bbb0259d6d15afcb7b6d63f1f42643eb8ef289 Mon Sep 17 00:00:00 2001 From: Palash gupta Date: Tue, 19 Apr 2022 00:21:30 +0530 Subject: [PATCH 013/106] feat: httpCode and httpStatus is updated to code and method --- frontend/src/container/Trace/TraceTable/index.tsx | 10 +++++----- frontend/src/types/reducer/trace.ts | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/frontend/src/container/Trace/TraceTable/index.tsx b/frontend/src/container/Trace/TraceTable/index.tsx index 951c981ddd..18d73bf5f0 100644 --- a/frontend/src/container/Trace/TraceTable/index.tsx +++ b/frontend/src/container/Trace/TraceTable/index.tsx @@ -44,7 +44,7 @@ function TraceTable(): JSX.Element { }; const getHttpMethodOrStatus = ( - value: TableType['httpMethod'], + value: TableType['statusCode'], ): JSX.Element => { if (value.length === 0) { return -; @@ -90,14 +90,14 @@ function TraceTable(): JSX.Element { }, { title: 'Method', - dataIndex: 'httpMethod', - key: 'httpMethod', + dataIndex: 'method', + key: 'method', render: getHttpMethodOrStatus, }, { title: 'Status Code', - dataIndex: 'httpCode', - key: 'httpCode', + dataIndex: 'statusCode', + key: 'statusCode', render: getHttpMethodOrStatus, }, ]; diff --git a/frontend/src/types/reducer/trace.ts b/frontend/src/types/reducer/trace.ts index 339fd63483..5989c58498 100644 --- a/frontend/src/types/reducer/trace.ts +++ b/frontend/src/types/reducer/trace.ts @@ -40,8 +40,8 @@ interface SpansAggregateData { serviceName: string; operation: string; durationNano: number; - httpCode: string; - httpMethod: string; + statusCode: string; + method: string; } export interface Tags { From 3c2173de9e5f30a99ddcaca8939a8a52a7559f7e Mon Sep 17 00:00:00 2001 From: Pranshu Chittora Date: Tue, 19 Apr 2022 10:57:56 +0530 Subject: [PATCH 014/106] feat: new dashboard widget's option selection (#982) * feat: new dashboard widget's option selection * fix: overflowing legend * feat: delete menu item is of type danger * feat: added keyboard events onFocus and onBlur --- .../GridGraphLayout/Graph/Bar/index.tsx | 39 ------- .../GridGraphLayout/Graph/Bar/styles.ts | 15 --- .../container/GridGraphLayout/Graph/index.tsx | 38 ++++-- .../GridGraphLayout/WidgetHeader/index.tsx | 109 ++++++++++++++++++ .../GridGraphLayout/WidgetHeader/styles.ts | 30 +++++ 5 files changed, 167 insertions(+), 64 deletions(-) delete mode 100644 frontend/src/container/GridGraphLayout/Graph/Bar/index.tsx delete mode 100644 frontend/src/container/GridGraphLayout/Graph/Bar/styles.ts create mode 100644 frontend/src/container/GridGraphLayout/WidgetHeader/index.tsx create mode 100644 frontend/src/container/GridGraphLayout/WidgetHeader/styles.ts diff --git a/frontend/src/container/GridGraphLayout/Graph/Bar/index.tsx b/frontend/src/container/GridGraphLayout/Graph/Bar/index.tsx deleted file mode 100644 index 7214d4839e..0000000000 --- a/frontend/src/container/GridGraphLayout/Graph/Bar/index.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { - DeleteOutlined, - EditFilled, - FullscreenOutlined, -} from '@ant-design/icons'; -import history from 'lib/history'; -import React from 'react'; -import { Widgets } from 'types/api/dashboard/getAll'; - -import { Container } from './styles'; - -function Bar({ - widget, - onViewFullScreenHandler, - onDeleteHandler, -}: BarProps): JSX.Element { - const onEditHandler = (): void => { - const widgetId = widget.id; - history.push( - `${window.location.pathname}/new?widgetId=${widgetId}&graphType=${widget.panelTypes}`, - ); - }; - - return ( - - - - - - ); -} - -interface BarProps { - widget: Widgets; - onViewFullScreenHandler: () => void; - onDeleteHandler: () => void; -} - -export default Bar; diff --git a/frontend/src/container/GridGraphLayout/Graph/Bar/styles.ts b/frontend/src/container/GridGraphLayout/Graph/Bar/styles.ts deleted file mode 100644 index ca8073a672..0000000000 --- a/frontend/src/container/GridGraphLayout/Graph/Bar/styles.ts +++ /dev/null @@ -1,15 +0,0 @@ -import styled from 'styled-components'; - -export const Container = styled.div` - height: 15%; - align-items: center; - justify-content: flex-end; - display: flex; - gap: 1rem; - padding-right: 1rem; - padding-left: 1rem; - padding-top: 0.5rem; - position: absolute; - top: 0; - right: 0; -`; diff --git a/frontend/src/container/GridGraphLayout/Graph/index.tsx b/frontend/src/container/GridGraphLayout/Graph/index.tsx index 0447cfca94..eb1ed0e2d6 100644 --- a/frontend/src/container/GridGraphLayout/Graph/index.tsx +++ b/frontend/src/container/GridGraphLayout/Graph/index.tsx @@ -20,7 +20,7 @@ import AppActions from 'types/actions'; import { GlobalTime } from 'types/actions/globalTime'; import { Widgets } from 'types/api/dashboard/getAll'; -import Bar from './Bar'; +import WidgetHeader from '../WidgetHeader'; import FullView from './FullView'; import { ErrorContainer, FullViewContainer, Modal } from './styles'; @@ -37,6 +37,7 @@ function GridCardGraph({ error: false, payload: undefined, }); + const [hovered, setHovered] = useState(false); const [modal, setModal] = useState(false); const { minTime, maxTime } = useSelector( (state) => state.globalTime, @@ -171,10 +172,12 @@ function GridCardGraph({ return ( <> {getModals()} - onToggleModal(setModal)} + onToggleModal(setDeletModal)} + onView={(): void => onToggleModal(setModal)} + onDelete={(): void => onToggleModal(setDeletModal)} /> {state.errorMessage} @@ -187,11 +190,26 @@ function GridCardGraph({ } return ( - <> - onToggleModal(setModal)} + { + setHovered(true); + }} + onFocus={(): void => { + setHovered(true); + }} + onMouseOut={(): void => { + setHovered(false); + }} + onBlur={(): void => { + setHovered(false); + }} + > + onToggleModal(setDeletModal)} + onView={(): void => onToggleModal(setModal)} + onDelete={(): void => onToggleModal(setDeletModal)} /> {getModals()} @@ -202,12 +220,12 @@ function GridCardGraph({ data: state.payload, isStacked: widget.isStacked, opacity: widget.opacity, - title: widget.title, + title: ' ', // empty title to accommodate absolutely positioned widget header name, yAxisUnit, }} /> - + ); } diff --git a/frontend/src/container/GridGraphLayout/WidgetHeader/index.tsx b/frontend/src/container/GridGraphLayout/WidgetHeader/index.tsx new file mode 100644 index 0000000000..ce9478d264 --- /dev/null +++ b/frontend/src/container/GridGraphLayout/WidgetHeader/index.tsx @@ -0,0 +1,109 @@ +import { + DeleteOutlined, + DownOutlined, + EditFilled, + FullscreenOutlined, +} from '@ant-design/icons'; +import { Dropdown, Menu, Typography } from 'antd'; +import history from 'lib/history'; +import React, { useState } from 'react'; +import { Widgets } from 'types/api/dashboard/getAll'; + +import { + ArrowContainer, + HeaderContainer, + HeaderContentContainer, + MenuItemContainer, +} from './styles'; + +type TWidgetOptions = 'view' | 'edit' | 'delete' | string; +interface IWidgetHeaderProps { + title: string; + widget: Widgets; + onView: VoidFunction; + onDelete: VoidFunction; + parentHover: boolean; +} +function WidgetHeader({ + title, + widget, + onView, + onDelete, + parentHover, +}: IWidgetHeaderProps): JSX.Element { + const [localHover, setLocalHover] = useState(false); + + const onEditHandler = (): void => { + const widgetId = widget.id; + history.push( + `${window.location.pathname}/new?widgetId=${widgetId}&graphType=${widget.panelTypes}`, + ); + }; + + const keyMethodMapping: { + [K in TWidgetOptions]: { key: TWidgetOptions; method: VoidFunction }; + } = { + view: { + key: 'view', + method: onView, + }, + edit: { + key: 'edit', + method: onEditHandler, + }, + delete: { + key: 'delete', + method: onDelete, + }, + }; + const onMenuItemSelectHandler = ({ key }: { key: TWidgetOptions }): void => { + keyMethodMapping[key]?.method(); + }; + + const menu = ( + + + + View + + + + + Edit + + + + + + Delete + + + + ); + + return ( + + setLocalHover(true)} + onMouseOut={(): void => setLocalHover(false)} + hover={localHover} + > + e.preventDefault()}> + + {title} + + + + + + + + ); +} + +export default WidgetHeader; diff --git a/frontend/src/container/GridGraphLayout/WidgetHeader/styles.ts b/frontend/src/container/GridGraphLayout/WidgetHeader/styles.ts new file mode 100644 index 0000000000..9600a6bcb4 --- /dev/null +++ b/frontend/src/container/GridGraphLayout/WidgetHeader/styles.ts @@ -0,0 +1,30 @@ +import { grey } from '@ant-design/colors'; +import styled from 'styled-components'; + +export const MenuItemContainer = styled.div` + display: flex; + justify-content: space-between; + align-items: center; +`; + +export const HeaderContainer = styled.div<{ hover: boolean }>` + width: 100%; + text-align: center; + background: ${({ hover }): string => (hover ? `${grey[0]}66` : 'inherit')}; + padding: 0.25rem 0; + font-size: 0.8rem; + cursor: all-scroll; + position: absolute; +`; + +export const HeaderContentContainer = styled.span` + cursor: pointer; + position: relative; + text-align: center; +`; + +export const ArrowContainer = styled.span<{ hover: boolean }>` + visibility: ${({ hover }): string => (hover ? 'visible' : 'hidden')}; + position: absolute; + right: -1rem; +`; From 508c6ced807e4be406035497af0db13958a3ab51 Mon Sep 17 00:00:00 2001 From: Amol Umbark Date: Fri, 22 Apr 2022 12:11:19 +0530 Subject: [PATCH 015/106] (feature): API - Implement receiver/channel test functionality (#993) * (feature): Added test receiver/channel functionality --- pkg/query-service/app/http_handler.go | 31 ++++++++++ .../integrations/alertManager/manager.go | 58 ++++++++++++++----- 2 files changed, 75 insertions(+), 14 deletions(-) diff --git a/pkg/query-service/app/http_handler.go b/pkg/query-service/app/http_handler.go index f3884127b5..fd4ecf4360 100644 --- a/pkg/query-service/app/http_handler.go +++ b/pkg/query-service/app/http_handler.go @@ -39,6 +39,7 @@ type APIHandler struct { basePath string apiPrefix string reader *Reader + alertManager am.Manager relationalDB *interfaces.ModelDao ready func(http.HandlerFunc) http.HandlerFunc } @@ -46,9 +47,11 @@ type APIHandler struct { // NewAPIHandler returns an APIHandler func NewAPIHandler(reader *Reader, relationalDB *interfaces.ModelDao) (*APIHandler, error) { + alertManager := am.New("") aH := &APIHandler{ reader: reader, relationalDB: relationalDB, + alertManager: alertManager, } aH.ready = aH.testReady @@ -172,6 +175,7 @@ func (aH *APIHandler) RegisterRoutes(router *mux.Router) { router.HandleFunc("/api/v1/channels/{id}", aH.editChannel).Methods(http.MethodPut) router.HandleFunc("/api/v1/channels/{id}", aH.deleteChannel).Methods(http.MethodDelete) router.HandleFunc("/api/v1/channels", aH.createChannel).Methods(http.MethodPost) + router.HandleFunc("/api/v1/testChannel", aH.testChannel).Methods(http.MethodPost) router.HandleFunc("/api/v1/rules", aH.listRulesFromProm).Methods(http.MethodGet) router.HandleFunc("/api/v1/rules/{id}", aH.getRule).Methods(http.MethodGet) router.HandleFunc("/api/v1/rules", aH.createRule).Methods(http.MethodPost) @@ -451,6 +455,33 @@ func (aH *APIHandler) listChannels(w http.ResponseWriter, r *http.Request) { aH.respond(w, channels) } +// testChannels sends test alert to all registered channels +func (aH *APIHandler) testChannel(w http.ResponseWriter, r *http.Request) { + + defer r.Body.Close() + body, err := ioutil.ReadAll(r.Body) + if err != nil { + zap.S().Errorf("Error in getting req body of testChannel API\n", err) + aH.respondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil) + return + } + + receiver := &am.Receiver{} + if err := json.Unmarshal(body, receiver); err != nil { // Parse []byte to go struct pointer + zap.S().Errorf("Error in parsing req body of testChannel API\n", err) + aH.respondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil) + return + } + + // send alert + apiErrorObj := aH.alertManager.TestReceiver(receiver) + if apiErrorObj != nil { + aH.respondError(w, apiErrorObj, nil) + return + } + aH.respond(w, "test alert sent") +} + func (aH *APIHandler) editChannel(w http.ResponseWriter, r *http.Request) { id := mux.Vars(r)["id"] diff --git a/pkg/query-service/integrations/alertManager/manager.go b/pkg/query-service/integrations/alertManager/manager.go index f0e8b024d1..47dc96f366 100644 --- a/pkg/query-service/integrations/alertManager/manager.go +++ b/pkg/query-service/integrations/alertManager/manager.go @@ -2,13 +2,14 @@ package alertManager // Wrapper to connect and process alert manager functions import ( - "fmt" - "encoding/json" "bytes" + "encoding/json" + "fmt" "net/http" - "go.uber.org/zap" + "go.signoz.io/query-service/constants" "go.signoz.io/query-service/model" + "go.uber.org/zap" ) const contentType = "application/json" @@ -17,16 +18,17 @@ type Manager interface { AddRoute(receiver *Receiver) *model.ApiError EditRoute(receiver *Receiver) *model.ApiError DeleteRoute(name string) *model.ApiError + TestReceiver(receiver *Receiver) *model.ApiError } -func New(url string) Manager{ - - if url == ""{ +func New(url string) Manager { + + if url == "" { url = constants.GetAlertManagerApiPrefix() } - return &manager { - url: url, + return &manager{ + url: url, } } @@ -34,11 +36,10 @@ type manager struct { url string } - func prepareAmChannelApiURL() string { basePath := constants.GetAlertManagerApiPrefix() AmChannelApiPath := constants.AmChannelApiPath - + if len(AmChannelApiPath) > 0 && rune(AmChannelApiPath[0]) == rune('/') { AmChannelApiPath = AmChannelApiPath[1:] } @@ -46,13 +47,18 @@ func prepareAmChannelApiURL() string { return fmt.Sprintf("%s%s", basePath, AmChannelApiPath) } -func (m *manager) AddRoute(receiver *Receiver) (*model.ApiError) { - +func prepareTestApiURL() string { + basePath := constants.GetAlertManagerApiPrefix() + return fmt.Sprintf("%s%s", basePath, "v1/testReceiver") +} + +func (m *manager) AddRoute(receiver *Receiver) *model.ApiError { + receiverString, _ := json.Marshal(receiver) amURL := prepareAmChannelApiURL() response, err := http.Post(amURL, contentType, bytes.NewBuffer(receiverString)) - + if err != nil { zap.S().Errorf(fmt.Sprintf("Error in getting response of API call to alertmanager(POST %s)\n", amURL), err) return &model.ApiError{Typ: model.ErrorInternal, Err: err} @@ -81,7 +87,7 @@ func (m *manager) EditRoute(receiver *Receiver) *model.ApiError { client := &http.Client{} response, err := client.Do(req) - + if err != nil { zap.S().Errorf(fmt.Sprintf("Error in getting response of API call to alertmanager(PUT %s)\n", amURL), err) return &model.ApiError{Typ: model.ErrorInternal, Err: err} @@ -125,5 +131,29 @@ func (m *manager) DeleteRoute(name string) *model.ApiError { return nil } +func (m *manager) TestReceiver(receiver *Receiver) *model.ApiError { + receiverBytes, _ := json.Marshal(receiver) + amTestURL := prepareTestApiURL() + response, err := http.Post(amTestURL, contentType, bytes.NewBuffer(receiverBytes)) + + if err != nil { + zap.S().Errorf(fmt.Sprintf("Error in getting response of API call to alertmanager(POST %s)\n", amTestURL), err) + return &model.ApiError{Typ: model.ErrorInternal, Err: err} + } + + if response.StatusCode > 201 && response.StatusCode < 400 { + err := fmt.Errorf(fmt.Sprintf("Invalid parameters in test alert api for alertmanager(POST %s)\n", amTestURL), response.Status) + zap.S().Error(err) + return &model.ApiError{Typ: model.ErrorInternal, Err: err} + } + + if response.StatusCode > 400 { + err := fmt.Errorf(fmt.Sprintf("Received Server Error response for API call to alertmanager(POST %s)\n", amTestURL), response.Status) + zap.S().Error(err) + return &model.ApiError{Typ: model.ErrorInternal, Err: err} + } + + return nil +} From 2b5b79e34a2beda439feac7d2ad234187db50a48 Mon Sep 17 00:00:00 2001 From: Amol Umbark Date: Fri, 22 Apr 2022 16:56:18 +0530 Subject: [PATCH 016/106] (feature): UI for Test alert channels (#994) * (feature): Implemented test channel function for webhook and slack --- frontend/public/locales/en-GB/channels.json | 30 ++++ frontend/public/locales/en/channels.json | 30 ++++ frontend/src/api/channels/testSlack.ts | 35 ++++ frontend/src/api/channels/testWebhook.ts | 51 ++++++ .../container/CreateAlertChannels/index.tsx | 155 ++++++++++++------ .../src/container/EditAlertChannels/index.tsx | 126 +++++++++++--- .../FormAlertChannels/Settings/Slack.tsx | 15 +- .../FormAlertChannels/Settings/Webhook.tsx | 11 +- .../src/container/FormAlertChannels/index.tsx | 24 ++- 9 files changed, 389 insertions(+), 88 deletions(-) create mode 100644 frontend/public/locales/en-GB/channels.json create mode 100644 frontend/public/locales/en/channels.json create mode 100644 frontend/src/api/channels/testSlack.ts create mode 100644 frontend/src/api/channels/testWebhook.ts diff --git a/frontend/public/locales/en-GB/channels.json b/frontend/public/locales/en-GB/channels.json new file mode 100644 index 0000000000..1378ba73b8 --- /dev/null +++ b/frontend/public/locales/en-GB/channels.json @@ -0,0 +1,30 @@ +{ + "page_title_create": "New Notification Channels", + "page_title_edit": "Edit Notification Channels", + "button_save_channel": "Save", + "button_test_channel": "Test", + "button_return": "Back", + "field_channel_name": "Name", + "field_channel_type": "Type", + "field_webhook_url": "Webhook URL", + "field_slack_recipient": "Recipient", + "field_slack_title": "Title", + "field_slack_description": "Description", + "field_webhook_username": "User Name (optional)", + "field_webhook_password": "Password (optional)", + "placeholder_slack_description": "Description", + "help_webhook_username": "Leave empty for bearer auth or when authentication is not necessary.", + "help_webhook_password": "Specify a password or bearer token", + "channel_creation_done": "Successfully created the channel", + "channel_creation_failed": "An unexpected error occurred while creating this channel", + "channel_edit_done": "Channels Edited Successfully", + "channel_edit_failed": "An unexpected error occurred while updating this channel", + "selected_channel_invalid": "Channel type selected is invalid", + "username_no_password": "A Password must be provided with user name", + "test_unsupported": "Sorry, this channel type does not support test yet", + "channel_test_done": "An alert has been sent to this channel", + "channel_test_failed": "Failed to send a test message to this channel, please confirm that the parameters are set correctly", + "channel_test_unexpected": "An unexpected error occurred while sending a message to this channel, please try again", + "webhook_url_required": "Webhook URL is mandatory", + "slack_channel_help": "Specify channel or user, use #channel-name, @username (has to be all lowercase, no whitespace)" +} \ No newline at end of file diff --git a/frontend/public/locales/en/channels.json b/frontend/public/locales/en/channels.json new file mode 100644 index 0000000000..1378ba73b8 --- /dev/null +++ b/frontend/public/locales/en/channels.json @@ -0,0 +1,30 @@ +{ + "page_title_create": "New Notification Channels", + "page_title_edit": "Edit Notification Channels", + "button_save_channel": "Save", + "button_test_channel": "Test", + "button_return": "Back", + "field_channel_name": "Name", + "field_channel_type": "Type", + "field_webhook_url": "Webhook URL", + "field_slack_recipient": "Recipient", + "field_slack_title": "Title", + "field_slack_description": "Description", + "field_webhook_username": "User Name (optional)", + "field_webhook_password": "Password (optional)", + "placeholder_slack_description": "Description", + "help_webhook_username": "Leave empty for bearer auth or when authentication is not necessary.", + "help_webhook_password": "Specify a password or bearer token", + "channel_creation_done": "Successfully created the channel", + "channel_creation_failed": "An unexpected error occurred while creating this channel", + "channel_edit_done": "Channels Edited Successfully", + "channel_edit_failed": "An unexpected error occurred while updating this channel", + "selected_channel_invalid": "Channel type selected is invalid", + "username_no_password": "A Password must be provided with user name", + "test_unsupported": "Sorry, this channel type does not support test yet", + "channel_test_done": "An alert has been sent to this channel", + "channel_test_failed": "Failed to send a test message to this channel, please confirm that the parameters are set correctly", + "channel_test_unexpected": "An unexpected error occurred while sending a message to this channel, please try again", + "webhook_url_required": "Webhook URL is mandatory", + "slack_channel_help": "Specify channel or user, use #channel-name, @username (has to be all lowercase, no whitespace)" +} \ No newline at end of file diff --git a/frontend/src/api/channels/testSlack.ts b/frontend/src/api/channels/testSlack.ts new file mode 100644 index 0000000000..a2b4b1f40a --- /dev/null +++ b/frontend/src/api/channels/testSlack.ts @@ -0,0 +1,35 @@ +import axios from 'api'; +import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; +import { AxiosError } from 'axios'; +import { ErrorResponse, SuccessResponse } from 'types/api'; +import { PayloadProps, Props } from 'types/api/channels/createSlack'; + +const testSlack = async ( + props: Props, +): Promise | ErrorResponse> => { + try { + const response = await axios.post('/testChannel', { + name: props.name, + slack_configs: [ + { + send_resolved: true, + api_url: props.api_url, + channel: props.channel, + title: props.title, + text: props.text, + }, + ], + }); + + return { + statusCode: 200, + error: null, + message: 'Success', + payload: response.data.data, + }; + } catch (error) { + return ErrorResponseHandler(error as AxiosError); + } +}; + +export default testSlack; diff --git a/frontend/src/api/channels/testWebhook.ts b/frontend/src/api/channels/testWebhook.ts new file mode 100644 index 0000000000..4b915e9a3a --- /dev/null +++ b/frontend/src/api/channels/testWebhook.ts @@ -0,0 +1,51 @@ +import axios from 'api'; +import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; +import { AxiosError } from 'axios'; +import { ErrorResponse, SuccessResponse } from 'types/api'; +import { PayloadProps, Props } from 'types/api/channels/createWebhook'; + +const testWebhook = async ( + props: Props, +): Promise | ErrorResponse> => { + try { + let httpConfig = {}; + + if (props.username !== '' && props.password !== '') { + httpConfig = { + basic_auth: { + username: props.username, + password: props.password, + }, + }; + } else if (props.username === '' && props.password !== '') { + httpConfig = { + authorization: { + type: 'bearer', + credentials: props.password, + }, + }; + } + + const response = await axios.post('/testChannel', { + name: props.name, + webhook_configs: [ + { + send_resolved: true, + url: props.api_url, + http_config: httpConfig, + }, + ], + }); + + return { + statusCode: 200, + error: null, + message: 'Success', + payload: response.data.data, + }; + } catch (error) { + return ErrorResponseHandler(error as AxiosError); + } +}; + +export default testWebhook; diff --git a/frontend/src/container/CreateAlertChannels/index.tsx b/frontend/src/container/CreateAlertChannels/index.tsx index 02cd7b274a..dc3eee86a6 100644 --- a/frontend/src/container/CreateAlertChannels/index.tsx +++ b/frontend/src/container/CreateAlertChannels/index.tsx @@ -1,10 +1,13 @@ import { Form, notification } from 'antd'; import createSlackApi from 'api/channels/createSlack'; import createWebhookApi from 'api/channels/createWebhook'; +import testSlackApi from 'api/channels/testSlack'; +import testWebhookApi from 'api/channels/testWebhook'; import ROUTES from 'constants/routes'; import FormAlertChannels from 'container/FormAlertChannels'; import history from 'lib/history'; import React, { useCallback, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { ChannelType, @@ -17,6 +20,9 @@ import { function CreateAlertChannels({ preType = 'slack', }: CreateAlertChannelsProps): JSX.Element { + // init namespace for translations + const { t } = useTranslation('channels'); + const [formInstance] = Form.useForm(); const [selectedConfig, setSelectedConfig] = useState< @@ -45,6 +51,7 @@ function CreateAlertChannels({ {{- end }}`, }); const [savingState, setSavingState] = useState(false); + const [testingState, setTestingState] = useState(false); const [notifications, NotificationElement] = notification.useNotification(); const [type, setType] = useState(preType); @@ -52,26 +59,26 @@ function CreateAlertChannels({ setType(value as ChannelType); }, []); - const onTestHandler = useCallback(() => { - console.log('test'); - }, []); + const prepareSlackRequest = useCallback(() => { + return { + api_url: selectedConfig?.api_url || '', + channel: selectedConfig?.channel || '', + name: selectedConfig?.name || '', + send_resolved: true, + text: selectedConfig?.text || '', + title: selectedConfig?.title || '', + }; + }, [selectedConfig]); const onSlackHandler = useCallback(async () => { try { setSavingState(true); - const response = await createSlackApi({ - api_url: selectedConfig?.api_url || '', - channel: selectedConfig?.channel || '', - name: selectedConfig?.name || '', - send_resolved: true, - text: selectedConfig?.text || '', - title: selectedConfig?.title || '', - }); + const response = await createSlackApi(prepareSlackRequest()); if (response.statusCode === 200) { notifications.success({ message: 'Success', - description: 'Successfully created the channel', + description: t('channel_creation_done'), }); setTimeout(() => { history.replace(ROUTES.SETTINGS); @@ -79,21 +86,20 @@ function CreateAlertChannels({ } else { notifications.error({ message: 'Error', - description: response.error || 'Error while creating the channel', + description: response.error || t('channel_creation_failed'), }); } setSavingState(false); } catch (error) { notifications.error({ message: 'Error', - description: - 'An unexpected error occurred while creating this channel, please try again', + description: t('channel_creation_failed'), }); setSavingState(false); } - }, [notifications, selectedConfig]); + }, [prepareSlackRequest, t, notifications]); - const onWebhookHandler = useCallback(async () => { + const prepareWebhookRequest = useCallback(() => { // initial api request without auth params let request: WebhookChannel = { api_url: selectedConfig?.api_url || '', @@ -101,39 +107,42 @@ function CreateAlertChannels({ send_resolved: true, }; - setSavingState(true); - - try { - if (selectedConfig?.username !== '' || selectedConfig?.password !== '') { - if (selectedConfig?.username !== '') { - // if username is not null then password must be passed - if (selectedConfig?.password !== '') { - request = { - ...request, - username: selectedConfig.username, - password: selectedConfig.password, - }; - } else { - notifications.error({ - message: 'Error', - description: 'A Password must be provided with user name', - }); - } - } else if (selectedConfig?.password !== '') { - // only password entered, set bearer token + if (selectedConfig?.username !== '' || selectedConfig?.password !== '') { + if (selectedConfig?.username !== '') { + // if username is not null then password must be passed + if (selectedConfig?.password !== '') { request = { ...request, - username: '', + username: selectedConfig.username, password: selectedConfig.password, }; + } else { + notifications.error({ + message: 'Error', + description: t('username_no_password'), + }); } + } else if (selectedConfig?.password !== '') { + // only password entered, set bearer token + request = { + ...request, + username: '', + password: selectedConfig.password, + }; } + } + return request; + }, [notifications, t, selectedConfig]); + const onWebhookHandler = useCallback(async () => { + setSavingState(true); + try { + const request = prepareWebhookRequest(); const response = await createWebhookApi(request); if (response.statusCode === 200) { notifications.success({ message: 'Success', - description: 'Successfully created the channel', + description: t('channel_creation_done'), }); setTimeout(() => { history.replace(ROUTES.SETTINGS); @@ -141,19 +150,17 @@ function CreateAlertChannels({ } else { notifications.error({ message: 'Error', - description: response.error || 'Error while creating the channel', + description: response.error || t('channel_creation_failed'), }); } } catch (error) { notifications.error({ message: 'Error', - description: - 'An unexpected error occurred while creating this channel, please try again', + description: t('channel_creation_failed'), }); } setSavingState(false); - }, [notifications, selectedConfig]); - + }, [prepareWebhookRequest, t, notifications]); const onSaveHandler = useCallback( async (value: ChannelType) => { switch (value) { @@ -166,11 +173,64 @@ function CreateAlertChannels({ default: notifications.error({ message: 'Error', - description: 'channel type selected is invalid', + description: t('selected_channel_invalid'), }); } }, - [onSlackHandler, onWebhookHandler, notifications], + [onSlackHandler, t, onWebhookHandler, notifications], + ); + + const performChannelTest = useCallback( + async (channelType: ChannelType) => { + setTestingState(true); + try { + let request; + let response; + switch (channelType) { + case WebhookType: + request = prepareWebhookRequest(); + response = await testWebhookApi(request); + break; + case SlackType: + request = prepareSlackRequest(); + response = await testSlackApi(request); + break; + default: + notifications.error({ + message: 'Error', + description: t('test_unsupported'), + }); + setTestingState(false); + return; + } + + if (response.statusCode === 200) { + notifications.success({ + message: 'Success', + description: t('channel_test_done'), + }); + } else { + notifications.error({ + message: 'Error', + description: t('channel_test_failed'), + }); + } + } catch (error) { + notifications.error({ + message: 'Error', + description: t('channel_test_unexpected'), + }); + } + setTestingState(false); + }, + [prepareWebhookRequest, t, prepareSlackRequest, notifications], + ); + + const onTestHandler = useCallback( + async (value: ChannelType) => { + performChannelTest(value); + }, + [performChannelTest], ); return ( @@ -183,8 +243,9 @@ function CreateAlertChannels({ onTestHandler, onSaveHandler, savingState, + testingState, NotificationElement, - title: 'New Notification Channels', + title: t('page_title_create'), initialValue: { type, ...selectedConfig, diff --git a/frontend/src/container/EditAlertChannels/index.tsx b/frontend/src/container/EditAlertChannels/index.tsx index e4aab19d31..b8db5a0e9b 100644 --- a/frontend/src/container/EditAlertChannels/index.tsx +++ b/frontend/src/container/EditAlertChannels/index.tsx @@ -1,6 +1,8 @@ import { Form, notification } from 'antd'; import editSlackApi from 'api/channels/editSlack'; import editWebhookApi from 'api/channels/editWebhook'; +import testSlackApi from 'api/channels/testSlack'; +import testWebhookApi from 'api/channels/testWebhook'; import ROUTES from 'constants/routes'; import { ChannelType, @@ -12,11 +14,15 @@ import { import FormAlertChannels from 'container/FormAlertChannels'; import history from 'lib/history'; import React, { useCallback, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { useParams } from 'react-router-dom'; function EditAlertChannels({ initialValue, }: EditAlertChannelsProps): JSX.Element { + // init namespace for translations + const { t } = useTranslation('channels'); + const [formInstance] = Form.useForm(); const [selectedConfig, setSelectedConfig] = useState< Partial @@ -24,6 +30,7 @@ function EditAlertChannels({ ...initialValue, }); const [savingState, setSavingState] = useState(false); + const [testingState, setTestingState] = useState(false); const [notifications, NotificationElement] = notification.useNotification(); const { id } = useParams<{ id: string }>(); @@ -35,9 +42,8 @@ function EditAlertChannels({ setType(value as ChannelType); }, []); - const onSlackEditHandler = useCallback(async () => { - setSavingState(true); - const response = await editSlackApi({ + const prepareSlackRequest = useCallback(() => { + return { api_url: selectedConfig?.api_url || '', channel: selectedConfig?.channel || '', name: selectedConfig?.name || '', @@ -45,12 +51,27 @@ function EditAlertChannels({ text: selectedConfig?.text || '', title: selectedConfig?.title || '', id, - }); + }; + }, [id, selectedConfig]); + + const onSlackEditHandler = useCallback(async () => { + setSavingState(true); + + if (selectedConfig?.api_url === '') { + notifications.error({ + message: 'Error', + description: t('webhook_url_required'), + }); + setSavingState(false); + return; + } + + const response = await editSlackApi(prepareSlackRequest()); if (response.statusCode === 200) { notifications.success({ message: 'Success', - description: 'Channels Edited Successfully', + description: t('channel_edit_done'), }); setTimeout(() => { @@ -59,15 +80,27 @@ function EditAlertChannels({ } else { notifications.error({ message: 'Error', - description: response.error || 'error while updating the Channels', + description: response.error || t('channel_edit_failed'), }); } setSavingState(false); - }, [selectedConfig, notifications, id]); + }, [prepareSlackRequest, t, notifications, selectedConfig]); + + const prepareWebhookRequest = useCallback(() => { + const { name, username, password } = selectedConfig; + return { + api_url: selectedConfig?.api_url || '', + name: name || '', + send_resolved: true, + username, + password, + id, + }; + }, [id, selectedConfig]); const onWebhookEditHandler = useCallback(async () => { setSavingState(true); - const { name, username, password } = selectedConfig; + const { username, password } = selectedConfig; const showError = (msg: string): void => { notifications.error({ @@ -77,40 +110,33 @@ function EditAlertChannels({ }; if (selectedConfig?.api_url === '') { - showError('Webhook URL is mandatory'); + showError(t('webhook_url_required')); setSavingState(false); return; } if (username && (!password || password === '')) { - showError('Please enter a password'); + showError(t('username_no_password')); setSavingState(false); return; } - const response = await editWebhookApi({ - api_url: selectedConfig?.api_url || '', - name: name || '', - send_resolved: true, - username, - password, - id, - }); + const response = await editWebhookApi(prepareWebhookRequest()); if (response.statusCode === 200) { notifications.success({ message: 'Success', - description: 'Channels Edited Successfully', + description: t('channel_edit_done'), }); setTimeout(() => { history.replace(ROUTES.SETTINGS); }, 2000); } else { - showError(response.error || 'error while updating the Channels'); + showError(response.error || t('channel_edit_failed')); } setSavingState(false); - }, [selectedConfig, notifications, id]); + }, [prepareWebhookRequest, t, notifications, selectedConfig]); const onSaveHandler = useCallback( (value: ChannelType) => { @@ -123,9 +149,58 @@ function EditAlertChannels({ [onSlackEditHandler, onWebhookEditHandler], ); - const onTestHandler = useCallback(() => { - console.log('test'); - }, []); + const performChannelTest = useCallback( + async (channelType: ChannelType) => { + setTestingState(true); + try { + let request; + let response; + switch (channelType) { + case WebhookType: + request = prepareWebhookRequest(); + response = await testWebhookApi(request); + break; + case SlackType: + request = prepareSlackRequest(); + response = await testSlackApi(request); + break; + default: + notifications.error({ + message: 'Error', + description: t('test_unsupported'), + }); + setTestingState(false); + return; + } + + if (response.statusCode === 200) { + notifications.success({ + message: 'Success', + description: t('channel_test_done'), + }); + } else { + notifications.error({ + message: 'Error', + description: t('channel_test_failed'), + }); + } + } catch (error) { + notifications.error({ + message: 'Error', + description: t('channel_test_failed'), + }); + } + setTestingState(false); + }, + [prepareWebhookRequest, t, prepareSlackRequest, notifications], + ); + + const onTestHandler = useCallback( + async (value: ChannelType) => { + performChannelTest(value); + }, + [performChannelTest], + ); return ( - + { setSelectedConfig((value) => ({ @@ -22,8 +25,8 @@ function Slack({ setSelectedConfig }: SlackProps): JSX.Element { @@ -35,7 +38,7 @@ function Slack({ setSelectedConfig }: SlackProps): JSX.Element { /> - +