diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 80eb0b41d6..6379372261 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -189,6 +189,13 @@ go.mod @therealpandey /frontend/src/container/TriggeredAlerts/ @SigNoz/pulse-frontend /frontend/src/container/AnomalyAlertEvaluationView/ @SigNoz/pulse-frontend +## Notification Channels +/frontend/src/pages/ChannelsEdit/ @SigNoz/pulse-frontend +/frontend/src/pages/ChannelsNew/ @SigNoz/pulse-frontend +/frontend/src/container/AllAlertChannels/ @SigNoz/pulse-frontend +/frontend/src/container/CreateAlertChannels/ @SigNoz/pulse-frontend +/frontend/src/container/EditAlertChannels/ @SigNoz/pulse-frontend + ## OpenAPI Schema - Generated /frontend/src/api/generated/services/ @therealpandey @vikrantgupta25 @srikanthccv /docs/api/openapi.yml @therealpandey @vikrantgupta25 @srikanthccv diff --git a/frontend/jest.config.ts b/frontend/jest.config.ts index 9f6a7fb47e..e5bf5705dc 100644 --- a/frontend/jest.config.ts +++ b/frontend/jest.config.ts @@ -48,13 +48,14 @@ const config: Config.InitialOptions = { ], '^.+\\.(js|jsx)$': 'babel-jest', }, + // TODO: https://github.com/SigNoz/engineering-pod/issues/5334 transformIgnorePatterns: [ // @chenglou/pretext is ESM-only; @signozhq/ui pulls it in via text-ellipsis. // Pattern 1: allow .pnpm virtual store through (handled by pattern 2), plus root-level ESM packages. - 'node_modules/(?!(\\.pnpm|lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@chenglou/pretext|@signozhq/design-tokens|@signozhq|date-fns|d3-interpolate|d3-color|api|@codemirror|@lezer|@marijn|@grafana|nuqs|uuid|copy-text-to-clipboard)/)', + 'node_modules/(?!(\\.pnpm|lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@chenglou/pretext|@signozhq/design-tokens|@signozhq|date-fns|d3-interpolate|d3-color|api|@codemirror|@lezer|@marijn|@grafana|nuqs|uuid|copy-text-to-clipboard|react-markdown|vfile|vfile-message|unist-util-stringify-position|unified|bail|is-plain-obj|trough|remark-parse|mdast-util-from-markdown|mdast-util-to-string|micromark|micromark-core-commonmark|micromark-extension-gfm|micromark-extension-gfm-autolink-literal|micromark-extension-gfm-footnote|micromark-extension-gfm-strikethrough|micromark-extension-gfm-table|micromark-extension-gfm-tagfilter|micromark-extension-gfm-task-list-item|micromark-factory-destination|micromark-factory-label|micromark-factory-space|micromark-factory-title|micromark-factory-whitespace|micromark-util-character|micromark-util-chunked|micromark-util-classify-character|micromark-util-combine-extensions|micromark-util-decode-numeric-character-reference|micromark-util-decode-string|micromark-util-encode|micromark-util-html-tag-name|micromark-util-normalize-identifier|micromark-util-resolve-all|micromark-util-sanitize-uri|micromark-util-subtokenize|micromark-util-symbol|micromark-util-types|decode-named-character-reference|remark-rehype|mdast-util-to-hast|unist-util-position|trim-lines|unist-util-visit|unist-util-visit-parents|unist-util-is|unist-util-generated|mdast-util-definitions|property-information|hast-util-whitespace|space-separated-tokens|comma-separated-tokens|rehype-raw|hast-util-raw|hast-util-from-parse5|devlop|hastscript|hast-util-parse-selector|vfile-location|web-namespaces|hast-util-to-parse5|zwitch|html-void-elements)/)', // Pattern 2: pnpm virtual store — ignore everything except ESM-only packages. // pnpm encodes scoped packages as @scope+name@version, so match on scope prefix. - 'node_modules/\\.pnpm/(?!(lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@chenglou|@signozhq|date-fns|d3-interpolate|d3-color|api|@codemirror|@lezer|@marijn|@grafana|nuqs|uuid|copy-text-to-clipboard)[^/]*/node_modules)', + 'node_modules/\\.pnpm/(?!(lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@chenglou|@signozhq|date-fns|d3-interpolate|d3-color|api|@codemirror|@lezer|@marijn|@grafana|nuqs|uuid|copy-text-to-clipboard|react-markdown|vfile|vfile-message|unist-util-stringify-position|unified|bail|is-plain-obj|trough|remark-parse|mdast-util-from-markdown|mdast-util-to-string|micromark|decode-named-character-reference|remark-rehype|mdast-util-to-hast|unist-util-position|trim-lines|unist-util-visit|unist-util-visit-parents|unist-util-is|unist-util-generated|mdast-util-definitions|property-information|hast-util-whitespace|space-separated-tokens|comma-separated-tokens|rehype-raw|hast-util-raw|hast-util-from-parse5|devlop|hastscript|hast-util-parse-selector|vfile-location|web-namespaces|hast-util-to-parse5|zwitch|html-void-elements)[^/]*/node_modules)', ], setupFilesAfterEnv: ['/jest.setup.ts'], testPathIgnorePatterns: ['/node_modules/', '/public/'], diff --git a/frontend/package.json b/frontend/package.json index 5779685b5a..d41e86e0e2 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -43,7 +43,7 @@ "@dnd-kit/modifiers": "7.0.0", "@dnd-kit/sortable": "8.0.0", "@dnd-kit/utilities": "3.2.2", - "@grafana/data": "^11.6.14", + "@grafana/data": "^11.6.15", "@monaco-editor/react": "^4.7.0", "@sentry/react": "10.57.0", "@sentry/vite-plugin": "5.3.0", @@ -79,7 +79,7 @@ "event-source-polyfill": "1.0.31", "eventemitter3": "5.0.1", "history": "4.10.1", - "http-proxy-middleware": "4.0.0", + "http-proxy-middleware": "4.1.1", "http-status-codes": "2.3.0", "i18next": "^21.6.12", "i18next-browser-languagedetector": "^6.1.3", @@ -231,16 +231,17 @@ "xml2js": "0.5.0", "phin": "^3.7.1", "body-parser": "1.20.3", - "http-proxy-middleware": "4.0.0", + "http-proxy-middleware": "4.1.1", "cross-spawn": "7.0.5", "cookie": "^0.7.1", "serialize-javascript": "6.0.2", "prismjs": "1.30.0", "got": "11.8.5", - "form-data": "4.0.4", + "form-data": "4.0.6", "brace-expansion": "^2.0.2", "on-headers": "^1.1.0", - "tmp": "0.2.4", + "js-cookie": "^3.0.7", + "tmp": "0.2.7", "vite": "npm:rolldown-vite@7.3.1" } } \ No newline at end of file diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index b066cdf2eb..f03b1e877f 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -12,16 +12,17 @@ overrides: xml2js: 0.5.0 phin: ^3.7.1 body-parser: 1.20.3 - http-proxy-middleware: 4.0.0 + http-proxy-middleware: 4.1.1 cross-spawn: 7.0.5 cookie: ^0.7.1 serialize-javascript: 6.0.2 prismjs: 1.30.0 got: 11.8.5 - form-data: 4.0.4 + form-data: 4.0.6 brace-expansion: ^2.0.2 on-headers: ^1.1.0 - tmp: 0.2.4 + js-cookie: ^3.0.7 + tmp: 0.2.7 vite: npm:rolldown-vite@7.3.1 importers: @@ -56,8 +57,8 @@ importers: specifier: 3.2.2 version: 3.2.2(react@18.2.0) '@grafana/data': - specifier: ^11.6.14 - version: 11.6.14(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + specifier: ^11.6.15 + version: 11.6.15(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@monaco-editor/react': specifier: ^4.7.0 version: 4.7.0(monaco-editor@0.55.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) @@ -164,8 +165,8 @@ importers: specifier: 4.10.1 version: 4.10.1 http-proxy-middleware: - specifier: 4.0.0 - version: 4.0.0 + specifier: 4.1.1 + version: 4.1.1 http-status-codes: specifier: 2.3.0 version: 2.3.0 @@ -1636,14 +1637,14 @@ packages: '@gerrit0/mini-shiki@3.23.0': resolution: {integrity: sha512-bEMORlG0cqdjVyCEuU0cDQbORWX+kYCeo0kV1lbxF5bt4r7SID2l9bqsxJEM0zndaxpOUT7riCyIVEuqq/Ynxg==} - '@grafana/data@11.6.14': - resolution: {integrity: sha512-Nsjq1A9m6LbsKsKvOgvAk9Wq7RGjy0V4N9d5YsSnzMwCiw/ov2wblR2bcDpy95uF8KaDTIR2Gf40nJaOYksPMA==} + '@grafana/data@11.6.15': + resolution: {integrity: sha512-q2Zbjr0N9iEGY/zKHm4Z4X5x64806E17W58y7mnvwc0MlbyGPPVulcp/rWA2Nd190mZeafZQPer9u+MaO+0HUQ==} peerDependencies: react: ^18.0.0 react-dom: ^18.0.0 - '@grafana/schema@11.6.14': - resolution: {integrity: sha512-YTqgYekb7kiu5NEoQxKF8czJ6QIARmMkCi9cNcynHqYpcDLOv5pg5Q0QtKgiiqHjlYoEeCV6iejdB4hXxzB+VA==} + '@grafana/schema@11.6.15': + resolution: {integrity: sha512-MPIvGAp9uzkswnH6e+Fmzu+WBTqWMgbv93/8iu56gb+sjCB2LciZLz4KvrPFdw32bWCGSMAGqsML9mgmeJZtGQ==} '@humanfs/core@0.19.2': resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} @@ -5167,8 +5168,8 @@ packages: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} - form-data@4.0.4: - resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} + form-data@4.0.6: + resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==} engines: {node: '>= 6'} format@0.2.2: @@ -5381,6 +5382,10 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + hast-util-from-parse5@8.0.1: resolution: {integrity: sha512-Er/Iixbc7IEa7r/XLtuG52zoqn/b3Xng/w6aZQ0xGVxzhw5xUFxcRqdPzP6yFi/4HBYRaifaI5fQ1RH8n0ZeOQ==} @@ -5456,8 +5461,8 @@ packages: resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} engines: {node: '>= 6'} - http-proxy-middleware@4.0.0: - resolution: {integrity: sha512-wuHwaUtmC0XzJNHqRp41zXtt5ojpHbusXGhq6781VvnjWUYPu7opmOF3eomGNujT07kEOnHWZyV9UZzKimVCKA==} + http-proxy-middleware@4.1.1: + resolution: {integrity: sha512-KX5ZofGXLFXqFAkQoOWZ+rTtaLTut7m0gyL+QzJrdejtIZ+F4bPPDoe7reISg2+v0CAz5OfVwEJEhty7X+e57g==} engines: {node: ^22.15.0 || ^24.0.0 || >=26.0.0} http-status-codes@2.3.0: @@ -5467,8 +5472,8 @@ packages: resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} engines: {node: '>= 6'} - httpxy@0.5.1: - resolution: {integrity: sha512-JPhqYiixe1A1I+MXDewWDZqeudBGU8Q9jCHYN8ML+779RQzLjTi78HBvWz4jMxUD6h2/vUL12g4q/mFM0OUw1A==} + httpxy@0.5.3: + resolution: {integrity: sha512-SMS9V6Sn7VWaS11lYhoAr0ceoaiolTWf4jYdJn0NJhCdKMu9R2H9Fh0LBDWBHQF6HRLI1PmaePYsjanSpE5PEw==} human-signals@2.1.0: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} @@ -6041,8 +6046,8 @@ packages: js-base64@3.7.5: resolution: {integrity: sha512-3MEt5DTINKqfScXKfJFrRbxkrnk2AxPWGBL/ycjz4dK8iqiSJ06UxD8jh8xuh6p10TX4t2+7FsBYVxxQbMg+qA==} - js-cookie@2.2.1: - resolution: {integrity: sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==} + js-cookie@3.0.8: + resolution: {integrity: sha512-yeJd4aNAdYZQjaon2bpD/Gb0B/omw7HQOsynXXcOiWVCacbBcPlgn8S/d1X6blFSaHao7ozqtW7NZW19xpCtIw==} js-levenshtein@1.1.6: resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==} @@ -8394,8 +8399,8 @@ packages: resolution: {integrity: sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==} engines: {node: ^20.0.0 || >=22.0.0} - tmp@0.2.4: - resolution: {integrity: sha512-UdiSoX6ypifLmrfQ/XfiawN6hkjSBpCjhKxxZcWlUUmoXLaCKQU0bx4HF/tdDK2uzRuchf1txGvrWBzYREssoQ==} + tmp@0.2.7: + resolution: {integrity: sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==} engines: {node: '>=14.14'} tmpl@1.0.5: @@ -10318,10 +10323,10 @@ snapshots: '@shikijs/types': 3.23.0 '@shikijs/vscode-textmate': 10.0.2 - '@grafana/data@11.6.14(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + '@grafana/data@11.6.15(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@braintree/sanitize-url': 7.0.1 - '@grafana/schema': 11.6.14 + '@grafana/schema': 11.6.15 '@types/d3-interpolate': 3.0.1 '@types/string-hash': 1.1.3 d3-interpolate: 3.0.1 @@ -10347,7 +10352,7 @@ snapshots: uplot: 1.6.31 xss: 1.0.14 - '@grafana/schema@11.6.14': + '@grafana/schema@11.6.15': dependencies: tslib: 2.8.1 @@ -12886,7 +12891,7 @@ snapshots: axios@1.16.0: dependencies: follow-redirects: 1.16.0 - form-data: 4.0.4 + form-data: 4.0.6 proxy-from-env: 2.1.0 transitivePeerDependencies: - debug @@ -13833,7 +13838,7 @@ snapshots: es-errors: 1.3.0 get-intrinsic: 1.3.0 has-tostringtag: 1.0.2 - hasown: 2.0.2 + hasown: 2.0.4 es-toolkit@1.46.1: {} @@ -14031,7 +14036,7 @@ snapshots: dependencies: chardet: 0.7.0 iconv-lite: 0.4.24 - tmp: 0.2.4 + tmp: 0.2.7 fast-deep-equal@3.1.3: {} @@ -14164,12 +14169,12 @@ snapshots: cross-spawn: 7.0.5 signal-exit: 4.1.0 - form-data@4.0.4: + form-data@4.0.6: dependencies: asynckit: 0.4.0 combined-stream: 1.0.8 es-set-tostringtag: 2.1.0 - hasown: 2.0.2 + hasown: 2.0.4 mime-types: 2.1.35 format@0.2.2: {} @@ -14248,7 +14253,7 @@ snapshots: get-proto: 1.0.1 gopd: 1.2.0 has-symbols: 1.1.0 - hasown: 2.0.2 + hasown: 2.0.4 math-intrinsics: 1.1.0 get-nonce@1.0.1: {} @@ -14386,6 +14391,10 @@ snapshots: dependencies: function-bind: 1.1.2 + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + hast-util-from-parse5@8.0.1: dependencies: '@types/hast': 3.0.4 @@ -14506,10 +14515,10 @@ snapshots: transitivePeerDependencies: - supports-color - http-proxy-middleware@4.0.0: + http-proxy-middleware@4.1.1: dependencies: debug: 4.3.4(supports-color@5.5.0) - httpxy: 0.5.1 + httpxy: 0.5.3 is-glob: 4.0.3 is-plain-obj: 4.1.0 micromatch: 4.0.8 @@ -14525,7 +14534,7 @@ snapshots: transitivePeerDependencies: - supports-color - httpxy@0.5.1: {} + httpxy@0.5.3: {} human-signals@2.1.0: {} @@ -15339,7 +15348,7 @@ snapshots: js-base64@3.7.5: {} - js-cookie@2.2.1: {} + js-cookie@3.0.8: {} js-levenshtein@1.1.6: {} @@ -15367,7 +15376,7 @@ snapshots: decimal.js: 10.6.0 domexception: 4.0.0 escodegen: 2.1.0 - form-data: 4.0.4 + form-data: 4.0.6 html-encoding-sniffer: 3.0.0 http-proxy-agent: 5.0.0 https-proxy-agent: 5.0.1 @@ -17336,7 +17345,7 @@ snapshots: copy-to-clipboard: 3.3.3 fast-deep-equal: 3.1.3 fast-shallow-equal: 1.0.0 - js-cookie: 2.2.1 + js-cookie: 3.0.8 nano-css: 5.6.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) @@ -17355,7 +17364,7 @@ snapshots: copy-to-clipboard: 3.3.3 fast-deep-equal: 3.1.3 fast-shallow-equal: 1.0.0 - js-cookie: 2.2.1 + js-cookie: 3.0.8 nano-css: 5.6.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) @@ -18103,7 +18112,7 @@ snapshots: tinypool@2.1.0: {} - tmp@0.2.4: {} + tmp@0.2.7: {} tmpl@1.0.5: {} diff --git a/frontend/src/AppRoutes/Private.tsx b/frontend/src/AppRoutes/Private.tsx index 040d2f1e90..92e34beca5 100644 --- a/frontend/src/AppRoutes/Private.tsx +++ b/frontend/src/AppRoutes/Private.tsx @@ -55,7 +55,6 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element { ), [pathname], ); - const isOldRoute = oldRoutes.indexOf(pathname) > -1; const currentRoute = mapRoutes.get('current'); const { isCloudUser: isCloudUserVal } = useGetTenantLicense(); @@ -83,12 +82,36 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element { }, [usersData?.data]); // Handle old routes - redirect to new routes + const isOldRoute = oldRoutes.indexOf(pathname) > -1; if (isOldRoute) { const redirectUrl = oldNewRoutesMapping[pathname]; + // TODO(H4ad): Remove this after https://github.com/SigNoz/engineering-pod/issues/5322 + // A mapped target may itself carry a query string (e.g. `/alerts?tab=Channels`). + // react-router does not re-parse a `?` embedded in the `pathname` field, so split + // it out and merge with the incoming search params. + const [redirectPath, redirectSearch = ''] = redirectUrl.split('?'); + const mergedParams = new URLSearchParams(location.search); + new URLSearchParams(redirectSearch).forEach((value, name) => { + mergedParams.set(name, value); + }); + const search = mergedParams.toString(); return ( + ); + } + + if (pathname.startsWith('/settings/channels/edit/')) { + const channelId = pathname.replace('/settings/channels/edit/', ''); + return ( + {location.pathname}; + return ( + <> +
{location.pathname}
+
{location.search}
+
{location.hash}
+ + ); } // Helper to create mock user @@ -1475,12 +1481,10 @@ describe('PrivateRoute', () => { await assertRedirectsTo(ROUTES.UN_AUTHORIZED); }); - it('should not redirect VIEWER from /settings/channels/new due to route matching order (ALL_CHANNELS matches last)', () => { - // Note: This tests the ACTUAL behavior of Private.tsx route matching - // CHANNELS_NEW has path '/settings/channels/new' with permission ['ADMIN'] - // ALL_CHANNELS has path '/settings/channels' with permission ['ADMIN', 'EDITOR', 'VIEWER'] - // Due to non-exact matching and array order, ALL_CHANNELS matches LAST for '/settings/channels/new' - // This is a known limitation - actual permission enforcement happens in the page component + it('should redirect VIEWER from /alerts/channels/new (ADMIN only)', async () => { + // After moving channels under /alerts, CHANNELS_NEW ('/alerts/channels/new') + // is an exact, ADMIN-only route with no overlapping non-exact ALL_CHANNELS + // route to match last, so a VIEWER is now correctly redirected. renderPrivateRoute({ initialRoute: ROUTES.CHANNELS_NEW, appContext: { @@ -1489,8 +1493,7 @@ describe('PrivateRoute', () => { }, }); - assertRendersChildren(); - assertStaysOnRoute(ROUTES.CHANNELS_NEW); + await assertRedirectsTo(ROUTES.UN_AUTHORIZED); }); it('should allow EDITOR to access /get-started route', () => { @@ -1548,4 +1551,60 @@ describe('PrivateRoute', () => { await assertRedirectsTo(ROUTES.UN_AUTHORIZED); }); }); + + describe('Old channel route redirects', () => { + it.each([ + ['/settings/channels', '/alerts', 'tab=Channels'], + ['/settings/channels/new', '/alerts/channels/new', ''], + ])( + 'should redirect %s to %s', + async (oldRoute, expectedPath, expectedSearch) => { + renderPrivateRoute({ + initialRoute: oldRoute, + appContext: { isLoggedIn: true }, + }); + + await waitFor(() => { + expect(screen.getByTestId('location-display')).toHaveTextContent( + expectedPath, + ); + }); + + if (expectedSearch) { + const search = screen.getByTestId('location-search').textContent ?? ''; + const params = new URLSearchParams(search); + new URLSearchParams(expectedSearch).forEach((value, name) => { + expect(params.get(name)).toBe(value); + }); + } else { + expect(screen.getByTestId('location-search')).toHaveTextContent(''); + } + }, + ); + + it('should redirect dynamic channel edit route preserving the channel id', async () => { + renderPrivateRoute({ + initialRoute: '/settings/channels/edit/abc123', + appContext: { isLoggedIn: true }, + }); + + await assertRedirectsTo('/alerts/channels/edit/abc123'); + }); + + it('should merge incoming query params with the embedded query of the target', async () => { + renderPrivateRoute({ + initialRoute: '/settings/channels?foo=bar', + appContext: { isLoggedIn: true }, + }); + + await waitFor(() => { + expect(screen.getByTestId('location-display')).toHaveTextContent('/alerts'); + }); + + const search = screen.getByTestId('location-search').textContent ?? ''; + const params = new URLSearchParams(search); + expect(params.get('tab')).toBe('Channels'); + expect(params.get('foo')).toBe('bar'); + }); + }); }); diff --git a/frontend/src/AppRoutes/pageComponents.ts b/frontend/src/AppRoutes/pageComponents.ts index 2190eac6e9..2b17ae42d6 100644 --- a/frontend/src/AppRoutes/pageComponents.ts +++ b/frontend/src/AppRoutes/pageComponents.ts @@ -142,12 +142,12 @@ export const AlertOverview = Loadable( () => import(/* webpackChunkName: "Alert Overview" */ 'pages/AlertList'), ); -export const CreateAlertChannelAlerts = Loadable( - () => import(/* webpackChunkName: "Create Channels" */ 'pages/Settings'), +export const ChannelsNew = Loadable( + () => import(/* webpackChunkName: "Create Channels" */ 'pages/AlertList'), ); -export const AllAlertChannels = Loadable( - () => import(/* webpackChunkName: "All Channels" */ 'pages/Settings'), +export const ChannelsEdit = Loadable( + () => import(/* webpackChunkName: "Edit Channels" */ 'pages/AlertList'), ); export const AllErrors = Loadable( diff --git a/frontend/src/AppRoutes/routes.ts b/frontend/src/AppRoutes/routes.ts index 5f357f651d..d10a4d3c87 100644 --- a/frontend/src/AppRoutes/routes.ts +++ b/frontend/src/AppRoutes/routes.ts @@ -5,10 +5,10 @@ import { AIAssistantPage, AlertHistory, AlertOverview, - AllAlertChannels, AllErrors, ApiMonitoring, - CreateAlertChannelAlerts, + ChannelsEdit, + ChannelsNew, CreateNewAlerts, DashboardPage, DashboardsListPage, @@ -269,16 +269,16 @@ const routes: AppRoutes[] = [ { path: ROUTES.CHANNELS_NEW, exact: true, - component: CreateAlertChannelAlerts, + component: ChannelsNew, isPrivate: true, key: 'CHANNELS_NEW', }, { - path: ROUTES.ALL_CHANNELS, + path: ROUTES.CHANNELS_EDIT, exact: true, - component: AllAlertChannels, + component: ChannelsEdit, isPrivate: true, - key: 'ALL_CHANNELS', + key: 'CHANNELS_EDIT', }, { path: ROUTES.ALL_ERROR, @@ -534,6 +534,9 @@ export const oldNewRoutesMapping: Record = { '/messaging-queues': '/messaging-queues/overview', '/alerts/edit': '/alerts/overview', '/alerts/type-selection': '/alerts/new', + // TODO(H4ad): Update this after https://github.com/SigNoz/engineering-pod/issues/5322 + '/settings/channels': '/alerts?tab=Channels', + '/settings/channels/new': '/alerts/channels/new', }; export const oldRoutes = Object.keys(oldNewRoutesMapping); diff --git a/frontend/src/constants/routes.ts b/frontend/src/constants/routes.ts index 5dd5c61ea5..f115aa0ce5 100644 --- a/frontend/src/constants/routes.ts +++ b/frontend/src/constants/routes.ts @@ -29,9 +29,10 @@ const ROUTES = { ALERTS_NEW: '/alerts/new', ALERT_HISTORY: '/alerts/history', ALERT_OVERVIEW: '/alerts/overview', - ALL_CHANNELS: '/settings/channels', - CHANNELS_NEW: '/settings/channels/new', - CHANNELS_EDIT: '/settings/channels/edit/:channelId', + // TODO(H4ad): Add test to forbidden ? in this map after https://github.com/SigNoz/engineering-pod/issues/5322 + ALL_CHANNELS: '/alerts?tab=Channels', + CHANNELS_NEW: '/alerts/channels/new', + CHANNELS_EDIT: '/alerts/channels/edit/:channelId', ALL_ERROR: '/exceptions', ERROR_DETAIL: '/error-detail', VERSION: '/status', diff --git a/frontend/src/container/AIAssistant/components/ActionsSection/utils/__tests__/openSavedView.test.ts b/frontend/src/container/AIAssistant/components/ActionsSection/utils/__tests__/openSavedView.test.ts index 3d63e6f94d..6b0e2f76f9 100644 --- a/frontend/src/container/AIAssistant/components/ActionsSection/utils/__tests__/openSavedView.test.ts +++ b/frontend/src/container/AIAssistant/components/ActionsSection/utils/__tests__/openSavedView.test.ts @@ -94,7 +94,7 @@ describe('resourceRoute', () => { it('routes channels to the edit page', () => { expect(resourceRoute(ResourceType.channel, 'channel-uuid-1')).toBe( - '/settings/channels/edit/channel-uuid-1', + '/alerts/channels/edit/channel-uuid-1', ); }); }); diff --git a/frontend/src/container/AllAlertChannels/AllAlertChannels.styles.scss b/frontend/src/container/AllAlertChannels/AllAlertChannels.styles.scss index 7021d286f0..1cbeddb579 100644 --- a/frontend/src/container/AllAlertChannels/AllAlertChannels.styles.scss +++ b/frontend/src/container/AllAlertChannels/AllAlertChannels.styles.scss @@ -1,4 +1,4 @@ .alert-channels-container { - width: 90%; - margin: 12px auto; + width: 100%; + padding: 0 var(--spacing-8); } diff --git a/frontend/src/container/CreateAlertChannels/CreateAlertChannels.styles.scss b/frontend/src/container/CreateAlertChannels/CreateAlertChannels.styles.scss index 7fbdb86066..4fa4f30dcd 100644 --- a/frontend/src/container/CreateAlertChannels/CreateAlertChannels.styles.scss +++ b/frontend/src/container/CreateAlertChannels/CreateAlertChannels.styles.scss @@ -1,7 +1,5 @@ .create-alert-channels-container { - width: 90%; - margin: 12px auto; - + width: 100%; border: 1px solid var(--l1-border); background: var(--l2-background); border-radius: 3px; diff --git a/frontend/src/container/SideNav/SideNav.tsx b/frontend/src/container/SideNav/SideNav.tsx index 0f579ea5ce..6bd8b4ed52 100644 --- a/frontend/src/container/SideNav/SideNav.tsx +++ b/frontend/src/container/SideNav/SideNav.tsx @@ -80,7 +80,7 @@ import signozBrandLogoUrl from '@/assets/Logos/signoz-brand-logo.svg'; import { useCmdK } from '../../providers/cmdKProvider'; import { routeConfig } from './config'; -import { getQueryString } from './helper'; +import { buildNavUrl, getQueryString } from './helper'; import { defaultMoreMenuItems, getUserSettingsDropdownMenuItems, @@ -486,12 +486,13 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element { const availableParams = routeConfig[key]; const queryString = getQueryString(availableParams || [], params); + const url = buildNavUrl(key, queryString); if (pathname !== key) { if (event && isModifierKeyPressed(event)) { - openInNewTab(`${key}?${queryString.join('&')}`); + openInNewTab(url); } else { - history.push(`${key}?${queryString.join('&')}`, { + history.push(url, { from: pathname, }); } diff --git a/frontend/src/container/SideNav/helper.ts b/frontend/src/container/SideNav/helper.ts index 988f3196b6..793a96ba5f 100644 --- a/frontend/src/container/SideNav/helper.ts +++ b/frontend/src/container/SideNav/helper.ts @@ -8,3 +8,14 @@ export const getQueryString = ( } return ''; }); + +/** + * @deprecated This should be removed after https://github.com/SigNoz/engineering-pod/issues/5322 is done + */ +export const buildNavUrl = (key: string, queryString: string[]): string => { + if (key.includes('?')) { + const extra = queryString.filter(Boolean).join('&'); + return extra ? `${key}&${extra}` : key; + } + return `${key}?${queryString.join('&')}`; +}; diff --git a/frontend/src/container/SideNav/menuItems.tsx b/frontend/src/container/SideNav/menuItems.tsx index 4800ca3caa..24d8f3539b 100644 --- a/frontend/src/container/SideNav/menuItems.tsx +++ b/frontend/src/container/SideNav/menuItems.tsx @@ -337,6 +337,7 @@ export const settingsNavSections: SettingsNavSection[] = [ isEnabled: true, itemKey: 'account', }, + // TODO(@SigNoz/pulse-frontend): https://github.com/SigNoz/engineering-pod/issues/5323 { key: ROUTES.ALL_CHANNELS, label: 'Notification Channels', diff --git a/frontend/src/pages/AlertList/index.tsx b/frontend/src/pages/AlertList/index.tsx index ffb76a6a39..e52e72a698 100644 --- a/frontend/src/pages/AlertList/index.tsx +++ b/frontend/src/pages/AlertList/index.tsx @@ -4,14 +4,17 @@ import { Tabs, TabsProps } from 'antd'; import ConfigureIcon from 'assets/AlertHistory/ConfigureIcon'; import HeaderRightSection from 'components/HeaderRightSection/HeaderRightSection'; import ROUTES from 'constants/routes'; +import AllAlertChannels from 'container/AllAlertChannels'; import AllAlertRules from 'container/ListAlertRules'; import { PlannedDowntime } from 'container/PlannedDowntime/PlannedDowntime'; import RoutingPolicies from 'container/RoutingPolicies'; import TriggeredAlerts from 'container/TriggeredAlerts'; import { useSafeNavigate } from 'hooks/useSafeNavigate'; import useUrlQuery from 'hooks/useUrlQuery'; -import { GalleryVerticalEnd, Pyramid } from '@signozhq/icons'; +import { Cable, GalleryVerticalEnd, Pyramid } from '@signozhq/icons'; import AlertDetails from 'pages/AlertDetails'; +import ChannelsEdit from 'pages/ChannelsEdit'; +import ChannelsNew from 'pages/ChannelsNew'; import { AlertListSubTabs, AlertListTabs } from './types'; @@ -26,6 +29,9 @@ function AllAlertList(): JSX.Element { const subTab = urlQuery.get('subTab'); const isAlertHistory = location.pathname === ROUTES.ALERT_HISTORY; const isAlertOverview = location.pathname === ROUTES.ALERT_OVERVIEW; + const isChannelsNew = location.pathname === ROUTES.CHANNELS_NEW; + const isChannelsEdit = location.pathname.startsWith('/alerts/channels/edit/'); + const isChannelDetails = isChannelsNew || isChannelsEdit; const handleConfigurationTabChange = useCallback( (subTab: string): void => { @@ -86,6 +92,22 @@ function AllAlertList(): JSX.Element { ), }, + { + label: ( +
+ + Notification Channels +
+ ), + key: AlertListTabs.CHANNELS, + children: ( +
+ {isChannelsNew && } + {isChannelsEdit && } + {!isChannelDetails && } +
+ ), + }, { label: (
@@ -98,11 +120,21 @@ function AllAlertList(): JSX.Element { }, ]; + const getActiveKey = (): string => { + if (isAlertHistory || isAlertOverview) { + return AlertListTabs.ALERT_RULES; + } + if (isChannelDetails) { + return AlertListTabs.CHANNELS; + } + return tab || AlertListTabs.ALERT_RULES; + }; + return ( { const queryParams = new URLSearchParams(); @@ -120,7 +152,9 @@ function AllAlertList(): JSX.Element { safeNavigate(`/alerts?${queryParams.toString()}`); }} className={`alerts-container ${ - isAlertHistory || isAlertOverview ? 'alert-details-tabs' : '' + isAlertHistory || isAlertOverview || isChannelDetails + ? 'alert-details-tabs' + : '' }`} tabBarExtraContent={ - + -
+
+ +
+ ); } diff --git a/frontend/src/pages/ChannelsNew/index.tsx b/frontend/src/pages/ChannelsNew/index.tsx new file mode 100644 index 0000000000..1470f6eabb --- /dev/null +++ b/frontend/src/pages/ChannelsNew/index.tsx @@ -0,0 +1,23 @@ +import AlertBreadcrumb from 'components/AlertBreadcrumb'; +import ROUTES from 'constants/routes'; +import CreateAlertChannels from 'container/CreateAlertChannels'; +import { ChannelType } from 'container/CreateAlertChannels/config'; +import styles from './styles.module.scss'; + +function ChannelsNew(): JSX.Element { + return ( + <> + +
+ +
+ + ); +} + +export default ChannelsNew; diff --git a/frontend/src/pages/ChannelsNew/styles.module.scss b/frontend/src/pages/ChannelsNew/styles.module.scss new file mode 100644 index 0000000000..e423c91424 --- /dev/null +++ b/frontend/src/pages/ChannelsNew/styles.module.scss @@ -0,0 +1,4 @@ +.content { + padding: var(--spacing-8); + padding-top: 0px; +} diff --git a/frontend/src/pages/Settings/Settings.tsx b/frontend/src/pages/Settings/Settings.tsx index dc22f15784..e12a1ced40 100644 --- a/frontend/src/pages/Settings/Settings.tsx +++ b/frontend/src/pages/Settings/Settings.tsx @@ -6,7 +6,7 @@ import RouteTab from 'components/RouteTab'; import { FeatureKeys } from 'constants/features'; import ROUTES from 'constants/routes'; import { routeConfig } from 'container/SideNav/config'; -import { getQueryString } from 'container/SideNav/helper'; +import { buildNavUrl, getQueryString } from 'container/SideNav/helper'; import { settingsNavSections } from 'container/SideNav/menuItems'; import NavItem from 'container/SideNav/NavItem/NavItem'; import { SidebarItem } from 'container/SideNav/sideNav.types'; @@ -240,12 +240,13 @@ function SettingsPage(): JSX.Element { const availableParams = routeConfig[key]; const queryString = getQueryString(availableParams || [], params); + const url = buildNavUrl(key, queryString); if (pathname !== key) { if (event && isModifierKeyPressed(event)) { - openInNewTab(`${key}?${queryString.join('&')}`); + openInNewTab(url); } else { - history.push(`${key}?${queryString.join('&')}`, { + history.push(url, { from: pathname, }); } @@ -259,17 +260,6 @@ function SettingsPage(): JSX.Element { }; const isActiveNavItem = (key: string): boolean => { - if (pathname.startsWith(ROUTES.ALL_CHANNELS) && key === ROUTES.ALL_CHANNELS) { - return true; - } - - if ( - pathname.startsWith(ROUTES.CHANNELS_EDIT) && - key === ROUTES.ALL_CHANNELS - ) { - return true; - } - if ( pathname.startsWith(ROUTES.ROLES_SETTINGS) && key === ROUTES.ROLES_SETTINGS diff --git a/frontend/src/pages/Settings/config.tsx b/frontend/src/pages/Settings/config.tsx index bb68b83201..92e1748b8d 100644 --- a/frontend/src/pages/Settings/config.tsx +++ b/frontend/src/pages/Settings/config.tsx @@ -1,9 +1,6 @@ import { RouteTabProps } from 'components/RouteTab/types'; import ROUTES from 'constants/routes'; -import AlertChannels from 'container/AllAlertChannels'; import BillingContainer from 'container/BillingContainer/BillingContainer'; -import CreateAlertChannels from 'container/CreateAlertChannels'; -import { ChannelType } from 'container/CreateAlertChannels/config'; import GeneralSettings from 'container/GeneralSettings'; import GeneralSettingsCloud from 'container/GeneralSettingsCloud'; import IngestionSettings from 'container/IngestionSettings/IngestionSettings'; @@ -16,20 +13,16 @@ import RoleDetailsPage from 'container/RolesSettings/RoleDetails'; import { TFunction } from 'i18next'; import { Backpack, - BellDot, Bot, Building, Cpu, CreditCard, Keyboard, - Pencil, - Plus, Shield, Sparkles, User, Users, } from '@signozhq/icons'; -import ChannelsEdit from 'pages/ChannelsEdit'; import MembersSettings from 'pages/MembersSettings'; import ServiceAccountsSettings from 'pages/ServiceAccountsSettings'; import Shortcuts from 'pages/Shortcuts'; @@ -47,19 +40,6 @@ export const organizationSettings = (t: TFunction): RouteTabProps['routes'] => [ }, ]; -export const alertChannels = (t: TFunction): RouteTabProps['routes'] => [ - { - Component: AlertChannels, - name: ( -
- {t('routes:alert_channels').toString()} -
- ), - route: ROUTES.ALL_CHANNELS, - key: ROUTES.ALL_CHANNELS, - }, -]; - export const ingestionSettings = (t: TFunction): RouteTabProps['routes'] => [ { Component: IngestionSettings, @@ -219,31 +199,3 @@ export const mcpServerSettings = (t: TFunction): RouteTabProps['routes'] => [ key: ROUTES.MCP_SERVER, }, ]; - -export const createAlertChannels = (t: TFunction): RouteTabProps['routes'] => [ - { - Component: (): JSX.Element => ( - - ), - name: ( -
- {t('routes:create_alert_channels').toString()} -
- ), - route: ROUTES.CHANNELS_NEW, - key: ROUTES.CHANNELS_NEW, - }, -]; - -export const editAlertChannels = (t: TFunction): RouteTabProps['routes'] => [ - { - Component: ChannelsEdit, - name: ( -
- {t('routes:edit_alert_channels').toString()} -
- ), - route: ROUTES.CHANNELS_EDIT, - key: ROUTES.CHANNELS_EDIT, - }, -]; diff --git a/frontend/src/pages/Settings/utils.ts b/frontend/src/pages/Settings/utils.ts index c8a99a779a..7926e83ca0 100644 --- a/frontend/src/pages/Settings/utils.ts +++ b/frontend/src/pages/Settings/utils.ts @@ -3,10 +3,7 @@ import { TFunction } from 'i18next'; import { ROLES, USER_ROLES } from 'types/roles'; import { - alertChannels, billingSettings, - createAlertChannels, - editAlertChannels, generalSettings, ingestionSettings, keyboardShortcuts, @@ -60,8 +57,6 @@ export const getRoutes = ( settings.push(...ingestionSettings(t)); } - settings.push(...alertChannels(t)); - // Visible to all authenticated users settings.push( ...serviceAccountsSettings(t), @@ -80,8 +75,6 @@ export const getRoutes = ( settings.push( ...mySettings(t), - ...createAlertChannels(t), - ...editAlertChannels(t), ...keyboardShortcuts(t), ...mcpServerSettings(t), );