Compare commits

..

6 Commits

Author SHA1 Message Date
SagarRajput-7
8bbc71a370 feat: feedback fix 2026-03-10 16:39:25 +05:30
SagarRajput-7
f6cdcbed4a feat: added pagination and sorter 2026-03-10 15:43:46 +05:30
SagarRajput-7
e7c7afe7e0 feat: feedback fix 2026-03-10 15:08:52 +05:30
SagarRajput-7
0ef4106515 feat: multiple style and functionality fixes 2026-03-10 13:17:08 +05:30
SagarRajput-7
1cdd2ec001 feat: multiple style and functionality fixes 2026-03-10 02:07:53 +05:30
SagarRajput-7
0bac99742e feat: new service_account page with crud and listing 2026-03-10 02:07:53 +05:30
102 changed files with 4668 additions and 1653 deletions

View File

@@ -1,4 +1,4 @@
FROM golang:1.25-bookworm
FROM golang:1.24-bullseye
ARG OS="linux"
ARG TARGETARCH

View File

@@ -6,7 +6,7 @@ ENV NODE_OPTIONS=--max-old-space-size=8192
RUN CI=1 yarn install
RUN CI=1 yarn build
FROM golang:1.25-bookworm
FROM golang:1.24-bullseye
ARG OS="linux"
ARG TARGETARCH

View File

@@ -100,7 +100,7 @@ This command:
3. Create a `.env` file in this directory:
```env
VITE_FRONTEND_API_ENDPOINT=http://localhost:8080
FRONTEND_API_ENDPOINT=http://localhost:8080
```
4. Start the development server:

View File

@@ -273,7 +273,6 @@ Options can be simple (direct link) or nested (with another question):
- Place logo files in `public/Logos/`
- Use SVG format
- Reference as `"/Logos/your-logo.svg"`
- **Optimize new SVGs**: Run any newly downloaded SVGs through an optimizer like [SVGOMG (svgo)](https://svgomg.net/) or use `npx svgo public/Logos/your-logo.svg` to minimise their size before committing.
### 4. Links

View File

@@ -1,6 +1,6 @@
NODE_ENV="development"
BUNDLE_ANALYSER="true"
VITE_FRONTEND_API_ENDPOINT="http://localhost:8080"
VITE_FRONTEND_API_ENDPOINT="http://localhost:8080/"
VITE_PYLON_APP_ID="pylon-app-id"
VITE_APPCUES_APP_ID="appcess-app-id"
VITE_PYLON_IDENTITY_SECRET="pylon-identity-secret"

View File

@@ -39,7 +39,7 @@ const config: Config.InitialOptions = {
'^.+\\.(js|jsx)$': 'babel-jest',
},
transformIgnorePatterns: [
'node_modules/(?!(lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@signozhq/design-tokens|@signozhq/table|@signozhq/calendar|@signozhq/input|@signozhq/popover|@signozhq/button|@signozhq/sonner|@signozhq/*|date-fns|d3-interpolate|d3-color|api|@codemirror|@lezer|@marijn|@grafana|nuqs)/)',
'node_modules/(?!(lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@signozhq/design-tokens|@signozhq/table|@signozhq/calendar|@signozhq/input|@signozhq/popover|@signozhq/button|@signozhq/sonner|@signozhq/*|date-fns|d3-interpolate|d3-color|api|@codemirror|@lezer|@marijn|@grafana)/)',
],
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
testPathIgnorePatterns: ['/node_modules/', '/public/'],

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="#fff" viewBox="0 0 24 24"><title>Deno</title><path d="M1.105 18.02A11.9 11.9 0 0 1 0 12.985q0-.698.078-1.376a12 12 0 0 1 .231-1.34A12 12 0 0 1 4.025 4.02a12 12 0 0 1 5.46-2.771 12 12 0 0 1 3.428-.23c1.452.112 2.825.477 4.077 1.05a12 12 0 0 1 2.78 1.774 12.02 12.02 0 0 1 4.053 7.078A12 12 0 0 1 24 12.985q0 .454-.036.914a12 12 0 0 1-.728 3.305 12 12 0 0 1-2.38 3.875c-1.33 1.357-3.02 1.962-4.43 1.936a4.4 4.4 0 0 1-2.724-1.024c-.99-.853-1.391-1.83-1.53-2.919a5 5 0 0 1 .128-1.518c.105-.38.37-1.116.76-1.437-.455-.197-1.04-.624-1.226-.829-.045-.05-.04-.13 0-.183a.155.155 0 0 1 .177-.053c.392.134.869.267 1.372.35.66.111 1.484.25 2.317.292 2.03.1 4.153-.813 4.812-2.627s.403-3.609-1.96-4.685-3.454-2.356-5.363-3.128c-1.247-.505-2.636-.205-4.06.582-3.838 2.121-7.277 8.822-5.69 15.032a.191.191 0 0 1-.315.19 12 12 0 0 1-1.25-1.634 12 12 0 0 1-.769-1.404M11.57 6.087c.649-.051 1.214.501 1.31 1.236.13.979-.228 1.99-1.41 2.013-1.01.02-1.315-.997-1.248-1.614.066-.616.574-1.575 1.35-1.635"/></svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="#fff" viewBox="0 0 24 24"><title>Haystack</title><path d="M2.008 0C.9 0 0 .9 0 2.008v19.984C0 23.1.9 24 2.008 24h19.984C23.1 24 24 23.1 24 21.992V2.008C24 .9 23.1 0 21.992 0Zm9.963 3.84c3.43 0 6.21 2.763 6.21 6.17v6.488a.27.27 0 0 1-.27.268 2.423 2.423 0 0 1-2.43-2.414V10.01c0-1.927-1.572-3.488-3.51-3.488S8.547 8.085 8.547 10.01v1.608a.263.263 0 0 0 .259.268h1.54a.27.27 0 0 0 .275-.263V9.945c0-.74.604-1.341 1.35-1.341s1.35.6 1.35 1.341V20.03a.275.275 0 0 1-.28.268 2.41 2.41 0 0 1-2.42-2.404v-3.23a.275.275 0 0 0-.276-.269H8.811a.264.264 0 0 0-.264.263v1.08c0 1.333-1.175 2.414-2.517 2.414a.27.27 0 0 1-.27-.268v-7.872c0-3.408 2.78-6.171 6.21-6.171"/></svg>

Before

Width:  |  Height:  |  Size: 707 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 95 88" fill="none"><path fill="#ffd21e" d="M47.21 76.5a34.75 34.75 0 1 0 0-69.5 34.75 34.75 0 0 0 0 69.5"/><path fill="#ff9d0b" d="M81.96 41.75a34.75 34.75 0 1 0-69.5 0 34.75 34.75 0 0 0 69.5 0m-73.5 0a38.75 38.75 0 1 1 77.5 0 38.75 38.75 0 0 1-77.5 0"/><path fill="#3a3b45" d="M58.5 32.3c1.28.44 1.78 3.06 3.07 2.38a5 5 0 1 0-6.76-2.07c.61 1.15 2.55-.72 3.7-.32Zm-23.55 0c-1.28.44-1.79 3.06-3.07 2.38a5 5 0 1 1 6.76-2.07c-.61 1.15-2.56-.72-3.7-.32Z"/><path fill="#ff323d" d="M46.96 56.29c9.83 0 13-8.76 13-13.26 0-2.34-1.57-1.6-4.09-.36-2.33 1.15-5.46 2.74-8.9 2.74-7.19 0-13-6.88-13-2.38s3.16 13.26 13 13.26Z"/><path fill="#3a3b45" fill-rule="evenodd" d="M39.43 54a8.7 8.7 0 0 1 5.3-4.49c.4-.12.81.57 1.24 1.28.4.68.82 1.37 1.24 1.37.45 0 .9-.68 1.33-1.35.45-.7.89-1.38 1.32-1.25a8.6 8.6 0 0 1 5 4.17c3.73-2.94 5.1-7.74 5.1-10.7 0-2.34-1.57-1.6-4.09-.36l-.14.07c-2.31 1.15-5.39 2.67-8.77 2.67s-6.45-1.52-8.77-2.67c-2.6-1.29-4.23-2.1-4.23.29 0 3.05 1.46 8.06 5.47 10.97" clip-rule="evenodd"/><path fill="#ff9d0b" d="M70.71 37a3.25 3.25 0 1 0 0-6.5 3.25 3.25 0 0 0 0 6.5m-46.5 0a3.25 3.25 0 1 0 0-6.5 3.25 3.25 0 0 0 0 6.5m-6.69 11c-1.62 0-3.06.66-4.07 1.87a5.97 5.97 0 0 0-1.33 3.76 7 7 0 0 0-1.94-.3c-1.55 0-2.95.59-3.94 1.66a5.8 5.8 0 0 0-.8 7 5.3 5.3 0 0 0-1.79 2.82c-.24.9-.48 2.8.8 4.74a5.22 5.22 0 0 0-.37 5.02c1.02 2.32 3.57 4.14 8.52 6.1 3.07 1.22 5.89 2 5.91 2.01a44.3 44.3 0 0 0 10.93 1.6c5.86 0 10.05-1.8 12.46-5.34 3.88-5.69 3.33-10.9-1.7-15.92-2.77-2.78-4.62-6.87-5-7.77-.78-2.66-2.84-5.62-6.25-5.62a5.7 5.7 0 0 0-4.6 2.46c-1-1.26-1.98-2.25-2.86-2.82A7.4 7.4 0 0 0 17.52 48m0 4c.51 0 1.14.22 1.82.65 2.14 1.36 6.25 8.43 7.76 11.18.5.92 1.37 1.31 2.14 1.31 1.55 0 2.75-1.53.15-3.48-3.92-2.93-2.55-7.72-.68-8.01.08-.02.17-.02.24-.02 1.7 0 2.45 2.93 2.45 2.93s2.2 5.52 5.98 9.3c3.77 3.77 3.97 6.8 1.22 10.83-1.88 2.75-5.47 3.58-9.16 3.58-3.81 0-7.73-.9-9.92-1.46-.11-.03-13.45-3.8-11.76-7 .28-.54.75-.76 1.34-.76 2.38 0 6.7 3.54 8.57 3.54.41 0 .7-.17.83-.6.79-2.85-12.06-4.05-10.98-8.17.2-.73.71-1.02 1.44-1.02 3.14 0 10.2 5.53 11.68 5.53.11 0 .2-.03.24-.1.74-1.2.33-2.04-4.9-5.2-5.21-3.16-8.88-5.06-6.8-7.33.24-.26.58-.38 1-.38 3.17 0 10.66 6.82 10.66 6.82s2.02 2.1 3.25 2.1c.28 0 .52-.1.68-.38.86-1.46-8.06-8.22-8.56-11.01-.34-1.9.24-2.85 1.31-2.85"/><path fill="#ffd21e" d="M38.6 76.69c2.75-4.04 2.55-7.07-1.22-10.84-3.78-3.77-5.98-9.3-5.98-9.3s-.82-3.2-2.69-2.9-3.24 5.08.68 8.01c3.91 2.93-.78 4.92-2.29 2.17-1.5-2.75-5.62-9.82-7.76-11.18-2.13-1.35-3.63-.6-3.13 2.2.5 2.79 9.43 9.55 8.56 11-.87 1.47-3.93-1.71-3.93-1.71s-9.57-8.71-11.66-6.44c-2.08 2.27 1.59 4.17 6.8 7.33 5.23 3.16 5.64 4 4.9 5.2-.75 1.2-12.28-8.53-13.36-4.4-1.08 4.11 11.77 5.3 10.98 8.15-.8 2.85-9.06-5.38-10.74-2.18-1.7 3.21 11.65 6.98 11.76 7.01 4.3 1.12 15.25 3.49 19.08-2.12"/><path fill="#ff9d0b" d="M77.4 48c1.62 0 3.07.66 4.07 1.87a5.97 5.97 0 0 1 1.33 3.76 7 7 0 0 1 1.95-.3c1.55 0 2.95.59 3.94 1.66a5.8 5.8 0 0 1 .8 7 5.3 5.3 0 0 1 1.78 2.82c.24.9.48 2.8-.8 4.74a5.22 5.22 0 0 1 .37 5.02c-1.02 2.32-3.57 4.14-8.51 6.1-3.08 1.22-5.9 2-5.92 2.01a44.3 44.3 0 0 1-10.93 1.6c-5.86 0-10.05-1.8-12.46-5.34-3.88-5.69-3.33-10.9 1.7-15.92 2.78-2.78 4.63-6.87 5.01-7.77.78-2.66 2.83-5.62 6.24-5.62a5.7 5.7 0 0 1 4.6 2.46c1-1.26 1.98-2.25 2.87-2.82A7.4 7.4 0 0 1 77.4 48m0 4c-.51 0-1.13.22-1.82.65-2.13 1.36-6.25 8.43-7.76 11.18a2.43 2.43 0 0 1-2.14 1.31c-1.54 0-2.75-1.53-.14-3.48 3.91-2.93 2.54-7.72.67-8.01a1.5 1.5 0 0 0-.24-.02c-1.7 0-2.45 2.93-2.45 2.93s-2.2 5.52-5.97 9.3c-3.78 3.77-3.98 6.8-1.22 10.83 1.87 2.75 5.47 3.58 9.15 3.58 3.82 0 7.73-.9 9.93-1.46.1-.03 13.45-3.8 11.76-7-.29-.54-.75-.76-1.34-.76-2.38 0-6.71 3.54-8.57 3.54-.42 0-.71-.17-.83-.6-.8-2.85 12.05-4.05 10.97-8.17-.19-.73-.7-1.02-1.44-1.02-3.14 0-10.2 5.53-11.68 5.53-.1 0-.19-.03-.23-.1-.74-1.2-.34-2.04 4.88-5.2 5.23-3.16 8.9-5.06 6.8-7.33-.23-.26-.57-.38-.98-.38-3.18 0-10.67 6.82-10.67 6.82s-2.02 2.1-3.24 2.1a.74.74 0 0 1-.68-.38c-.87-1.46 8.05-8.22 8.55-11.01.34-1.9-.24-2.85-1.31-2.85"/><path fill="#ffd21e" d="M56.33 76.69c-2.75-4.04-2.56-7.07 1.22-10.84 3.77-3.77 5.97-9.3 5.97-9.3s.82-3.2 2.7-2.9c1.86.3 3.23 5.08-.68 8.01-3.92 2.93.78 4.92 2.28 2.17 1.51-2.75 5.63-9.82 7.76-11.18 2.13-1.35 3.64-.6 3.13 2.2-.5 2.79-9.42 9.55-8.55 11 .86 1.47 3.92-1.71 3.92-1.71s9.58-8.71 11.66-6.44-1.58 4.17-6.8 7.33c-5.23 3.16-5.63 4-4.9 5.2.75 1.2 12.28-8.53 13.36-4.4 1.08 4.11-11.76 5.3-10.97 8.15.8 2.85 9.05-5.38 10.74-2.18 1.69 3.21-11.65 6.98-11.76 7.01-4.31 1.12-15.26 3.49-19.08-2.12"/></svg>

Before

Width:  |  Height:  |  Size: 4.4 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="#76b900" viewBox="0 0 24 24"><title>NVIDIA</title><path d="M8.948 8.798v-1.43a7 7 0 0 1 .424-.018c3.922-.124 6.493 3.374 6.493 3.374s-2.774 3.851-5.75 3.851a3.7 3.7 0 0 1-1.158-.185v-4.346c1.528.185 1.837.857 2.747 2.385l2.04-1.714s-1.492-1.952-4-1.952a6 6 0 0 0-.796.035m0-4.735v2.138l.424-.027c5.45-.185 9.01 4.47 9.01 4.47s-4.08 4.964-8.33 4.964a6.5 6.5 0 0 1-1.095-.097v1.325c.3.035.61.062.91.062 3.957 0 6.82-2.023 9.593-4.408.459.371 2.34 1.263 2.73 1.652-2.633 2.208-8.772 3.984-12.253 3.984-.335 0-.653-.018-.971-.053v1.864H24V4.063zm0 10.326v1.131c-3.657-.654-4.673-4.46-4.673-4.46s1.758-1.944 4.673-2.262v1.237H8.94c-1.528-.186-2.73 1.245-2.73 1.245s.68 2.412 2.739 3.11M2.456 10.9s2.164-3.197 6.5-3.533V6.201C4.153 6.59 0 10.653 0 10.653s2.35 6.802 8.948 7.42v-1.237c-4.84-.6-6.492-5.936-6.492-5.936"/></svg>

Before

Width:  |  Height:  |  Size: 865 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="#fff" viewBox="0 0 24 24"><title>Ollama</title><path d="M16.361 10.26a.9.9 0 0 0-.558.47l-.072.148.001.207c0 .193.004.217.059.353.076.193.152.312.291.448.24.238.51.3.872.205a.86.86 0 0 0 .517-.436.75.75 0 0 0 .08-.498c-.064-.453-.33-.782-.724-.897a1.1 1.1 0 0 0-.466 0m-9.203.005c-.305.096-.533.32-.65.639a1.2 1.2 0 0 0-.06.52c.057.309.31.59.598.667.362.095.632.033.872-.205.14-.136.215-.255.291-.448.055-.136.059-.16.059-.353l.001-.207-.072-.148a.9.9 0 0 0-.565-.472 1 1 0 0 0-.474.007m4.184 2c-.131.071-.223.25-.195.383.031.143.157.288.353.407.105.063.112.072.117.136.004.038-.01.146-.029.243-.02.094-.036.194-.036.222.002.074.07.195.143.253.064.052.076.054.255.059.164.005.198.001.264-.03.169-.082.212-.234.15-.525-.052-.243-.042-.28.087-.355.137-.08.281-.219.324-.314a.365.365 0 0 0-.175-.48.4.4 0 0 0-.181-.033c-.126 0-.207.03-.355.124l-.085.053-.053-.032c-.219-.13-.259-.145-.391-.143a.4.4 0 0 0-.193.032m.39-2.195c-.373.036-.475.05-.654.086a4.5 4.5 0 0 0-.951.328c-.94.46-1.589 1.226-1.787 2.114-.04.176-.045.234-.045.53 0 .294.005.357.043.524.264 1.16 1.332 2.017 2.714 2.173.3.033 1.596.033 1.896 0 1.11-.125 2.064-.727 2.493-1.571.114-.226.169-.372.22-.602.039-.167.044-.23.044-.523 0-.297-.005-.355-.045-.531-.288-1.29-1.539-2.304-3.072-2.497a7 7 0 0 0-.855-.031zm.645.937a3.3 3.3 0 0 1 1.44.514c.223.148.537.458.671.662.166.251.26.508.303.82.02.143.01.251-.043.482-.08.345-.332.705-.672.957a3 3 0 0 1-.689.348c-.382.122-.632.144-1.525.138-.582-.006-.686-.01-.853-.042q-.856-.16-1.35-.68c-.264-.28-.385-.535-.45-.946-.03-.192.025-.509.137-.776.136-.326.488-.73.836-.963.403-.269.934-.46 1.422-.512.187-.02.586-.02.773-.002m-5.503-11a1.65 1.65 0 0 0-.683.298C5.617.74 5.173 1.666 4.985 2.819c-.07.436-.119 1.04-.119 1.503 0 .544.064 1.24.155 1.721.02.107.031.202.023.208l-.187.152a5.3 5.3 0 0 0-.949 1.02 5.5 5.5 0 0 0-.94 2.339 6.6 6.6 0 0 0-.023 1.357c.091.78.325 1.438.727 2.04l.13.195-.037.064c-.269.452-.498 1.105-.605 1.732-.084.496-.095.629-.095 1.294 0 .67.009.803.088 1.266.095.555.288 1.143.503 1.534.071.128.243.393.264.407.007.003-.014.067-.046.141a7.4 7.4 0 0 0-.548 1.873 5 5 0 0 0-.071.991c0 .56.031.832.148 1.279L3.42 24h1.478l-.05-.091c-.297-.552-.325-1.575-.068-2.597.117-.472.25-.819.498-1.296l.148-.29v-.177c0-.165-.003-.184-.057-.293a.9.9 0 0 0-.194-.25 1.7 1.7 0 0 1-.385-.543c-.424-.92-.506-2.286-.208-3.451.124-.486.329-.918.544-1.154a.8.8 0 0 0 .223-.531c0-.195-.07-.355-.224-.522a3.14 3.14 0 0 1-.817-1.729c-.14-.96.114-2.005.69-2.834.563-.814 1.353-1.336 2.237-1.475.199-.033.57-.028.776.01.226.04.367.028.512-.041.179-.085.268-.19.374-.431.093-.215.165-.333.36-.576.234-.29.46-.489.822-.729.413-.27.884-.467 1.352-.561.17-.035.25-.04.569-.04s.398.005.569.04a4.07 4.07 0 0 1 1.914.997c.117.109.398.457.488.602.034.057.095.177.132.267.105.241.195.346.374.43.14.068.286.082.503.045.343-.058.607-.053.943.016 1.144.23 2.14 1.173 2.581 2.437.385 1.108.276 2.267-.296 3.153-.097.15-.193.27-.333.419-.301.322-.301.722-.001 1.053.493.539.801 1.866.708 3.036-.062.772-.26 1.463-.533 1.854a2 2 0 0 1-.224.258.9.9 0 0 0-.194.25c-.054.109-.057.128-.057.293v.178l.148.29c.248.476.38.823.498 1.295.253 1.008.231 2.01-.059 2.581a1 1 0 0 0-.044.098c0 .006.329.009.732.009h.73l.02-.074.036-.134c.019-.076.057-.3.088-.516a9 9 0 0 0 0-1.258c-.11-.875-.295-1.57-.597-2.226-.032-.074-.053-.138-.046-.141a1.4 1.4 0 0 0 .108-.152c.376-.569.607-1.284.724-2.228.031-.26.031-1.378 0-1.628-.083-.645-.182-1.082-.348-1.525a6 6 0 0 0-.329-.7l-.038-.064.131-.194c.402-.604.636-1.262.727-2.04a6.6 6.6 0 0 0-.024-1.358 5.5 5.5 0 0 0-.939-2.339 5.3 5.3 0 0 0-.95-1.02l-.186-.152a.7.7 0 0 1 .023-.208c.208-1.087.201-2.443-.017-3.503-.19-.924-.535-1.658-.98-2.082-.354-.338-.716-.482-1.15-.455-.996.059-1.8 1.205-2.116 3.01a7 7 0 0 0-.097.726c0 .036-.007.066-.015.066a1 1 0 0 1-.149-.078A4.86 4.86 0 0 0 12 3.03c-.832 0-1.687.243-2.456.698a1 1 0 0 1-.148.078c-.008 0-.015-.03-.015-.066a7 7 0 0 0-.097-.725C8.997 1.392 8.337.319 7.46.048a2 2 0 0 0-.585-.041Zm.293 1.402c.248.197.523.759.682 1.388.03.113.06.244.069.292.007.047.026.152.041.233.067.365.098.76.102 1.24l.002.475-.12.175-.118.178h-.278c-.324 0-.646.041-.954.124l-.238.06c-.033.007-.038-.003-.057-.144a8.4 8.4 0 0 1 .016-2.323c.124-.788.413-1.501.696-1.711.067-.05.079-.049.157.013m9.825-.012c.17.126.358.46.498.888.28.854.36 2.028.212 3.145-.019.14-.024.151-.057.144l-.238-.06a3.7 3.7 0 0 0-.954-.124h-.278l-.119-.178-.119-.175.002-.474c.004-.669.066-1.19.214-1.772.157-.623.434-1.185.68-1.382.078-.062.09-.063.159-.012"/></svg>

Before

Width:  |  Height:  |  Size: 4.4 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="#fff" viewBox="0 0 24 24"><title>OpenRouter</title><path d="M16.778 1.844v1.919q-.569-.026-1.138-.032-.708-.008-1.415.037c-1.93.126-4.023.728-6.149 2.237-2.911 2.066-2.731 1.95-4.14 2.75-.396.223-1.342.574-2.185.798-.841.225-1.753.333-1.751.333v4.229s.768.108 1.61.333c.842.224 1.789.575 2.185.799 1.41.798 1.228.683 4.14 2.75 2.126 1.509 4.22 2.11 6.148 2.236.88.058 1.716.041 2.555.005v1.918l7.222-4.168-7.222-4.17v2.176c-.86.038-1.611.065-2.278.021-1.364-.09-2.417-.357-3.979-1.465-2.244-1.593-2.866-2.027-3.68-2.508.889-.518 1.449-.906 3.822-2.59 1.56-1.109 2.614-1.377 3.978-1.466.667-.044 1.418-.017 2.278.02v2.176L24 6.014Z"/></svg>

Before

Width:  |  Height:  |  Size: 685 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-3.94 -1.44 438.62 432.87"><path fill="#e75225" d="M215.926 7.068c115.684.024 210.638 93.784 210.493 207.844-.148 115.793-94.713 208.252-212.912 208.169C97.95 423 4.52 329.143 4.601 213.221 4.68 99.867 99.833 7.044 215.926 7.068m-63.947 73.001c2.652 12.978.076 25.082-3.846 36.988-2.716 8.244-6.47 16.183-8.711 24.539-3.694 13.769-7.885 27.619-9.422 41.701-2.21 20.25 5.795 38.086 19.493 55.822L86.527 225.94c.11 1.978-.007 2.727.21 3.361 5.968 17.43 16.471 32.115 28.243 45.957 1.246 1.465 4.082 2.217 6.182 2.221 62.782.115 125.565.109 188.347.028 1.948-.003 4.546-.369 5.741-1.618 13.456-14.063 23.746-30.079 30.179-50.257l-66.658 12.976c4.397-8.567 9.417-16.1 12.302-24.377 9.869-28.315 5.779-55.69-8.387-81.509-11.368-20.72-21.854-41.349-16.183-66.32-12.005 11.786-16.615 26.79-19.541 42.253-2.882 15.23-4.58 30.684-6.811 46.136-.317-.467-.728-.811-.792-1.212-.258-1.621-.499-3.255-.587-4.893-1.355-25.31-6.328-49.696-16.823-72.987-6.178-13.71-12.99-27.727-6.622-44.081-4.31 2.259-8.205 4.505-10.997 7.711-8.333 9.569-11.779 21.062-12.666 33.645-.757 10.75-1.796 21.552-3.801 32.123-2.107 11.109-5.448 21.998-12.956 32.209-3.033-21.81-3.37-43.38-22.928-57.237m161.877 216.523H116.942v34.007h196.914zm-157.871 51.575c-.163 28.317 28.851 49.414 64.709 47.883 29.716-1.269 56.016-24.51 53.755-47.883z"/></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 122.88 103.53"><path d="M5.47 0H117.4c3.01 0 5.47 2.46 5.47 5.47v92.58c0 3.01-2.46 5.47-5.47 5.47H5.47c-3.01 0-5.47-2.46-5.47-5.47V5.47C0 2.46 2.46 0 5.47 0m26.37 38.55 17.79 18.42 2.14 2.13-2.12 2.16-17.97 19.05-5.07-5 15.85-16.15L26.81 43.6zM94.1 79.41H54.69v-6.84H94.1zM38.19 9.83c3.19 0 5.78 2.59 5.78 5.78s-2.59 5.78-5.78 5.78-5.78-2.59-5.78-5.78S35 9.83 38.19 9.83m-19.24 0c3.19 0 5.78 2.59 5.78 5.78s-2.59 5.78-5.78 5.78-5.78-2.59-5.78-5.78 2.58-5.78 5.78-5.78M7.49 5.41H115.4c1.15 0 2.09.94 2.09 2.09v18.32H5.4V7.5c0-1.15.94-2.09 2.09-2.09" style="fill-rule:evenodd;clip-rule:evenodd;fill:#1668dc"/></svg>

Before

Width:  |  Height:  |  Size: 687 B

View File

@@ -15,5 +15,6 @@
"logs_to_metrics": "Logs To Metrics",
"roles": "Roles",
"role_details": "Role Details",
"members": "Members"
"members": "Members",
"service_accounts": "Service Accounts"
}

View File

@@ -50,5 +50,8 @@
"INFRASTRUCTURE_MONITORING_KUBERNETES": "SigNoz | Infra Monitoring",
"METER_EXPLORER": "SigNoz | Meter Explorer",
"METER_EXPLORER_VIEWS": "SigNoz | Meter Explorer Views",
"METER": "SigNoz | Meter"
"METER": "SigNoz | Meter",
"ROLES_SETTINGS": "SigNoz | Roles",
"MEMBERS_SETTINGS": "SigNoz | Members",
"SERVICE_ACCOUNTS_SETTINGS": "SigNoz | Service Accounts"
}

View File

@@ -15,5 +15,6 @@
"logs_to_metrics": "Logs To Metrics",
"roles": "Roles",
"role_details": "Role Details",
"members": "Members"
"members": "Members",
"service_accounts": "Service Accounts"
}

View File

@@ -75,5 +75,6 @@
"METER_EXPLORER_VIEWS": "SigNoz | Meter Explorer Views",
"METER": "SigNoz | Meter",
"ROLES_SETTINGS": "SigNoz | Roles",
"MEMBERS_SETTINGS": "SigNoz | Members"
"MEMBERS_SETTINGS": "SigNoz | Members",
"SERVICE_ACCOUNTS_SETTINGS": "SigNoz | Service Accounts"
}

View File

@@ -0,0 +1,142 @@
.create-sa-modal {
max-width: 530px;
background: var(--popover);
border: 1px solid var(--secondary);
border-radius: 4px;
box-shadow: 0 4px 9px 0 rgba(0, 0, 0, 0.04);
[data-slot='dialog-header'] {
padding: var(--padding-4);
border-bottom: 1px solid var(--secondary);
flex-shrink: 0;
background: transparent;
margin: 0;
}
[data-slot='dialog-title'] {
font-family: Inter, sans-serif;
font-size: var(--label-base-400-font-size);
font-weight: var(--label-base-400-font-weight);
line-height: var(--label-base-400-line-height);
letter-spacing: -0.065px;
color: var(--bg-base-white);
margin: 0;
}
[data-slot='dialog-description'] {
padding: 0;
.create-sa-modal__content {
padding: var(--padding-4);
}
}
}
.create-sa-form {
display: flex;
flex-direction: column;
gap: var(--spacing-2);
.ant-form-item {
margin-bottom: var(--spacing-4);
}
.ant-form-item-label > label {
font-size: var(--paragraph-base-400-font-size);
font-weight: var(--paragraph-base-400-font-weight);
color: var(--foreground);
letter-spacing: -0.07px;
}
&__input {
height: 32px;
color: var(--l1-foreground);
background-color: var(--l2-background);
border-color: var(--border);
font-size: var(--paragraph-base-400-font-size);
border-radius: 2px;
width: 100%;
&::placeholder {
color: var(--l3-foreground);
}
&:focus {
border-color: var(--primary);
box-shadow: none;
}
}
&__select {
width: 100%;
.ant-select-selector {
min-height: 32px;
border-radius: 2px;
background-color: var(--l2-background) !important;
border: 1px solid var(--border) !important;
padding: 0 var(--padding-2) !important;
.ant-select-selection-placeholder {
color: var(--l3-foreground);
opacity: 0.4;
font-size: var(--paragraph-base-400-font-size);
letter-spacing: -0.07px;
line-height: 32px;
}
.ant-select-selection-item {
font-size: var(--paragraph-base-400-font-size);
letter-spacing: -0.07px;
color: var(--bg-base-white);
}
}
.ant-select-arrow {
color: var(--foreground);
}
&.ant-select-focused .ant-select-selector,
&:not(.ant-select-disabled):hover .ant-select-selector {
border-color: var(--primary);
}
}
&__helper {
font-size: var(--paragraph-small-400-font-size);
color: var(--l3-foreground);
margin: calc(var(--spacing-2) * -1) 0 var(--spacing-4) 0;
line-height: var(--paragraph-small-400-line-height);
}
}
.create-sa-modal__footer {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-end;
padding: 0 var(--padding-4);
height: 56px;
min-height: 56px;
border-top: 1px solid var(--secondary);
gap: var(--spacing-4);
flex-shrink: 0;
}
.lightMode {
.create-sa-modal {
[data-slot='dialog-title'] {
color: var(--bg-base-black);
}
}
.create-sa-form {
&__select {
.ant-select-selector {
.ant-select-selection-item {
color: var(--bg-base-black);
}
}
}
}
}

View File

@@ -0,0 +1,185 @@
import { useCallback, useEffect, useState } from 'react';
import { Button } from '@signozhq/button';
import { DialogFooter, DialogWrapper } from '@signozhq/dialog';
import { X } from '@signozhq/icons';
import { toast } from '@signozhq/sonner';
import { Form, Input } from 'antd';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { useCreateServiceAccount } from 'api/generated/services/serviceaccount';
import type { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import RolesSelect, { useRoles } from 'components/RolesSelect';
import './CreateServiceAccountModal.styles.scss';
interface CreateServiceAccountModalProps {
open: boolean;
onClose: () => void;
onSuccess: () => void;
}
interface FormValues {
name: string;
email: string;
roles: string[];
}
function CreateServiceAccountModal({
open,
onClose,
onSuccess,
}: CreateServiceAccountModalProps): JSX.Element {
const [form] = Form.useForm<FormValues>();
const [isSubmitting, setIsSubmitting] = useState(false);
const [submittable, setSubmittable] = useState(false);
const values = Form.useWatch([], form);
useEffect(() => {
form
.validateFields({ validateOnly: true })
.then(() => setSubmittable(true))
.catch(() => setSubmittable(false));
}, [values, form]);
const { mutateAsync: createServiceAccount } = useCreateServiceAccount();
const {
roles,
isLoading: rolesLoading,
isError: rolesError,
error: rolesErrorObj,
refetch: refetchRoles,
} = useRoles();
const handleClose = useCallback((): void => {
form.resetFields();
onClose();
}, [form, onClose]);
const handleSubmit = useCallback(async (): Promise<void> => {
try {
const values = await form.validateFields();
setIsSubmitting(true);
await createServiceAccount({
data: {
name: values.name.trim(),
email: values.email.trim(),
roles: values.roles,
},
});
toast.success('Service account created successfully', { richColors: true });
form.resetFields();
onSuccess();
onClose();
} catch (err: unknown) {
if (err && typeof err === 'object' && 'errorFields' in err) {
return;
}
const errMessage =
convertToApiError(
err as AxiosError<RenderErrorResponseDTO, unknown> | null,
)?.getErrorMessage() || 'An error occurred';
toast.error(`Failed to create service account: ${errMessage}`, {
richColors: true,
});
} finally {
setIsSubmitting(false);
}
}, [form, createServiceAccount, onSuccess, onClose]);
return (
<DialogWrapper
title="New Service Account"
open={open}
onOpenChange={(isOpen): void => {
if (!isOpen) {
handleClose();
}
}}
showCloseButton
width="narrow"
className="create-sa-modal"
disableOutsideClick={false}
>
<div className="create-sa-modal__content">
<Form form={form} layout="vertical" className="create-sa-form">
<Form.Item
name="name"
label="Name"
rules={[{ required: true, message: 'Name is required' }]}
className="create-sa-form__item"
>
<Input placeholder="Enter a name" className="create-sa-form__input" />
</Form.Item>
<Form.Item
name="email"
label="Email Address"
rules={[
{ required: true, message: 'Email Address is required' },
{ type: 'email', message: 'Please enter a valid email address' },
]}
className="create-sa-form__item"
>
<Input
type="email"
placeholder="email@example.com"
className="create-sa-form__input"
/>
</Form.Item>
<p className="create-sa-form__helper">
Used only for notifications about this service account. It is not used for
authentication.
</p>
<Form.Item
name="roles"
label="Roles"
rules={[{ required: true, message: 'At least one role is required' }]}
className="create-sa-form__item"
>
<RolesSelect
mode="multiple"
roles={roles}
loading={rolesLoading}
isError={rolesError}
error={rolesErrorObj}
onRefetch={refetchRoles}
placeholder="Select roles"
className="create-sa-form__select"
getPopupContainer={(triggerNode): HTMLElement =>
(triggerNode?.closest('.create-sa-modal') as HTMLElement) ||
document.body
}
/>
</Form.Item>
</Form>
</div>
<DialogFooter className="create-sa-modal__footer">
<Button
type="button"
variant="solid"
color="secondary"
size="sm"
onClick={handleClose}
>
<X size={12} />
Cancel
</Button>
<Button
variant="solid"
color="primary"
size="sm"
onClick={handleSubmit}
disabled={isSubmitting || !submittable}
>
{isSubmitting ? 'Creating...' : 'Create Service Account'}
</Button>
</DialogFooter>
</DialogWrapper>
);
}
export default CreateServiceAccountModal;

View File

@@ -243,56 +243,60 @@ function InviteMembersModal({
<div className="table-header-cell role-header">Roles</div>
<div className="table-header-cell action-header" />
</div>
<div className="invite-members-modal__container">
{rows.map(
(row): JSX.Element => (
<div key={row.id} className="team-member-row">
<div className="team-member-cell email-cell">
<Input
type="email"
placeholder="john@signoz.io"
value={row.email}
onChange={(e): void => updateEmail(row.id, e.target.value)}
className="team-member-email-input"
/>
{emailValidity[row.id] === false && row.email.trim() !== '' && (
<span className="email-error-message">Invalid email address</span>
)}
</div>
<div className="team-member-cell role-cell">
<Select
value={row.role || undefined}
onChange={(role): void => updateRole(row.id, role as ROLES)}
className="team-member-role-select"
placeholder="Select roles"
suffixIcon={<ChevronDown size={14} />}
getPopupContainer={(triggerNode): HTMLElement =>
(triggerNode?.closest('.invite-members-modal') as HTMLElement) ||
document.body
}
>
<Select.Option value="VIEWER">Viewer</Select.Option>
<Select.Option value="EDITOR">Editor</Select.Option>
<Select.Option value="ADMIN">Admin</Select.Option>
</Select>
</div>
<div className="team-member-cell action-cell">
{rows.length > 1 && (
<Button
variant="ghost"
color="destructive"
className="remove-team-member-button"
onClick={(): void => removeRow(row.id)}
aria-label="Remove row"
<form onSubmit={(e): void => e.preventDefault()}>
<div className="invite-members-modal__container">
{rows.map(
(row): JSX.Element => (
<div key={row.id} className="team-member-row">
<div className="team-member-cell email-cell">
<Input
type="email"
placeholder="john@signoz.io"
value={row.email}
onChange={(e): void => updateEmail(row.id, e.target.value)}
className="team-member-email-input"
name={`invite-email-${row.id}`}
autoComplete="email"
/>
{emailValidity[row.id] === false && row.email.trim() !== '' && (
<span className="email-error-message">Invalid email address</span>
)}
</div>
<div className="team-member-cell role-cell">
<Select
value={row.role || undefined}
onChange={(role): void => updateRole(row.id, role as ROLES)}
className="team-member-role-select"
placeholder="Select roles"
suffixIcon={<ChevronDown size={14} />}
getPopupContainer={(triggerNode): HTMLElement =>
(triggerNode?.closest('.invite-members-modal') as HTMLElement) ||
document.body
}
>
<Trash2 size={12} />
</Button>
)}
<Select.Option value="VIEWER">Viewer</Select.Option>
<Select.Option value="EDITOR">Editor</Select.Option>
<Select.Option value="ADMIN">Admin</Select.Option>
</Select>
</div>
<div className="team-member-cell action-cell">
{rows.length > 1 && (
<Button
variant="ghost"
color="destructive"
className="remove-team-member-button"
onClick={(): void => removeRow(row.id)}
aria-label="Remove row"
>
<Trash2 size={12} />
</Button>
)}
</div>
</div>
</div>
),
)}
</div>
),
)}
</div>
</form>
</div>
{(hasInvalidEmails || hasInvalidRoles) && (

View File

@@ -361,58 +361,6 @@ describe('CheckboxFilter - User Flows', () => {
expect(filtersForServiceName).toHaveLength(0);
});
it('should match filter when query uses resource. prefix (resource.service.name matches service.name)', async () => {
// Filter config uses unprefixed key (service.name)
// Query has filter with resource. prefix (resource.service.name)
// Checkbox should recognize the match and show checked state
mockUseQueryBuilder.mockReturnValue({
lastUsedQuery: 0,
currentQuery: {
builder: {
queryData: [
{
filters: {
items: [
{
key: {
key: 'resource.service.name',
dataType: DataTypes.String,
type: 'resource',
},
op: 'in',
value: [OTEL_DEMO],
},
],
op: 'AND',
},
},
],
},
},
redirectWithQueryBuilderData: jest.fn(),
} as any);
const mockFilter = createMockFilter({ defaultOpen: false });
render(
<CheckboxFilter
filter={mockFilter}
source={QuickFiltersSource.LOGS_EXPLORER}
/>,
);
// Filter should auto-open because it has active filters (key match via prefix stripping)
await waitFor(() => {
expect(screen.getByPlaceholderText('Filter values')).toBeInTheDocument();
});
// otel-demo should be checked (filter uses resource.service.name IN [otel-demo])
// Checked items are sorted to the top, so otel-demo is first
const checkboxes = screen.getAllByRole('checkbox');
expect(checkboxes[0]).toBeChecked();
expect(screen.getByText(OTEL_DEMO)).toBeInTheDocument();
});
it('should extend an existing IN filter when checking an additional value', async () => {
const redirectWithQueryBuilderData = jest.fn();

View File

@@ -18,7 +18,7 @@ import { useGetAggregateValues } from 'hooks/queryBuilder/useGetAggregateValues'
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useGetQueryKeyValueSuggestions } from 'hooks/querySuggestions/useGetQueryKeyValueSuggestions';
import useDebouncedFn from 'hooks/useDebouncedFunction';
import { cloneDeep, isArray, isFunction } from 'lodash-es';
import { cloneDeep, isArray, isEqual, isFunction } from 'lodash-es';
import { ChevronDown, ChevronRight } from 'lucide-react';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
@@ -26,7 +26,6 @@ import { DataSource } from 'types/common/queryBuilder';
import { v4 as uuid } from 'uuid';
import LogsQuickFilterEmptyState from './LogsQuickFilterEmptyState';
import { isKeyMatch } from './utils';
import './Checkbox.styles.scss';
@@ -85,7 +84,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
currentQuery.builder.queryData?.[
activeQueryIndex
]?.filters?.items?.some((item) =>
isKeyMatch(item.key?.key, filter.attributeKey.key),
isEqual(item.key?.key, filter.attributeKey.key),
),
[currentQuery.builder.queryData, activeQueryIndex, filter.attributeKey.key],
);
@@ -190,7 +189,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
const filterSync = currentQuery?.builder.queryData?.[
activeQueryIndex
]?.filters?.items.find((item) =>
isKeyMatch(item.key?.key, filter.attributeKey.key),
isEqual(item.key?.key, filter.attributeKey.key),
);
if (filterSync) {
@@ -237,7 +236,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
(currentQuery?.builder?.queryData?.[
activeQueryIndex
]?.filters?.items?.filter((item) =>
isKeyMatch(item.key?.key, filter.attributeKey.key),
isEqual(item.key?.key, filter.attributeKey.key),
)?.length || 0) > 1,
[currentQuery?.builder?.queryData, activeQueryIndex, filter.attributeKey],
@@ -281,7 +280,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
items:
idx === activeQueryIndex
? item.filters?.items?.filter(
(fil) => !isKeyMatch(fil.key?.key, filter.attributeKey.key),
(fil) => !isEqual(fil.key?.key, filter.attributeKey.key),
) || []
: [...(item.filters?.items || [])],
op: item.filters?.op || 'AND',
@@ -314,7 +313,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
: 'Only'
: 'Only';
query.filters.items = query.filters.items.filter(
(q) => !isKeyMatch(q.key?.key, filter.attributeKey.key),
(q) => !isEqual(q.key?.key, filter.attributeKey.key),
);
if (query.filter?.expression) {
@@ -336,13 +335,13 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
} else if (query?.filters?.items) {
if (
query.filters?.items?.some((item) =>
isKeyMatch(item.key?.key, filter.attributeKey.key),
isEqual(item.key?.key, filter.attributeKey.key),
)
) {
// if there is already a running filter for the current attribute key then
// we split the cases by which particular operator is present right now!
const currentFilter = query.filters?.items?.find((q) =>
isKeyMatch(q.key?.key, filter.attributeKey.key),
isEqual(q.key?.key, filter.attributeKey.key),
);
if (currentFilter) {
const runningOperator = currentFilter?.op;
@@ -357,7 +356,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
value: [...currentFilter.value, value],
};
query.filters.items = query.filters.items.map((item) => {
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
if (isEqual(item.key?.key, filter.attributeKey.key)) {
return newFilter;
}
return item;
@@ -369,7 +368,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
value: [currentFilter.value as string, value],
};
query.filters.items = query.filters.items.map((item) => {
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
if (isEqual(item.key?.key, filter.attributeKey.key)) {
return newFilter;
}
return item;
@@ -386,11 +385,11 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
if (newFilter.value.length === 0) {
query.filters.items = query.filters.items.filter(
(item) => !isKeyMatch(item.key?.key, filter.attributeKey.key),
(item) => !isEqual(item.key?.key, filter.attributeKey.key),
);
} else {
query.filters.items = query.filters.items.map((item) => {
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
if (isEqual(item.key?.key, filter.attributeKey.key)) {
return newFilter;
}
return item;
@@ -399,7 +398,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
} else {
// if not an array remove the whole thing altogether!
query.filters.items = query.filters.items.filter(
(item) => !isKeyMatch(item.key?.key, filter.attributeKey.key),
(item) => !isEqual(item.key?.key, filter.attributeKey.key),
);
}
}
@@ -415,7 +414,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
value: [...currentFilter.value, value],
};
query.filters.items = query.filters.items.map((item) => {
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
if (isEqual(item.key?.key, filter.attributeKey.key)) {
return newFilter;
}
return item;
@@ -427,7 +426,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
value: [currentFilter.value as string, value],
};
query.filters.items = query.filters.items.map((item) => {
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
if (isEqual(item.key?.key, filter.attributeKey.key)) {
return newFilter;
}
return item;
@@ -442,7 +441,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
};
if (newFilter.value.length === 0) {
query.filters.items = query.filters.items.filter(
(item) => !isKeyMatch(item.key?.key, filter.attributeKey.key),
(item) => !isEqual(item.key?.key, filter.attributeKey.key),
);
if (query.filter?.expression) {
query.filter.expression = removeKeysFromExpression(
@@ -452,7 +451,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
}
} else {
query.filters.items = query.filters.items.map((item) => {
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
if (isEqual(item.key?.key, filter.attributeKey.key)) {
return newFilter;
}
return item;
@@ -470,7 +469,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
);
}
query.filters.items = query.filters.items.filter(
(item) => !isKeyMatch(item.key?.key, filter.attributeKey.key),
(item) => !isEqual(item.key?.key, filter.attributeKey.key),
);
}
}
@@ -483,14 +482,14 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
value: [currentFilter.value as string, value],
};
query.filters.items = query.filters.items.map((item) => {
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
if (isEqual(item.key?.key, filter.attributeKey.key)) {
return newFilter;
}
return item;
});
} else if (!checked) {
query.filters.items = query.filters.items.filter(
(item) => !isKeyMatch(item.key?.key, filter.attributeKey.key),
(item) => !isEqual(item.key?.key, filter.attributeKey.key),
);
}
break;
@@ -502,14 +501,14 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
value: [currentFilter.value as string, value],
};
query.filters.items = query.filters.items.map((item) => {
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
if (isEqual(item.key?.key, filter.attributeKey.key)) {
return newFilter;
}
return item;
});
} else if (checked) {
query.filters.items = query.filters.items.filter(
(item) => !isKeyMatch(item.key?.key, filter.attributeKey.key),
(item) => !isEqual(item.key?.key, filter.attributeKey.key),
);
}
break;

View File

@@ -1,41 +0,0 @@
/**
* These prefixes are added to attribute keys based on their context.
*/
export const FIELD_CONTEXT_PREFIXES = [
'metric',
'log',
'span',
'trace',
'resource',
'scope',
'attribute',
'event',
'body',
];
/**
* Removes the field context prefix from a key to get the base key name.
* Example: 'resource.service.name' -> 'service.name'
* Example: 'attribute.http.method' -> 'http.method'
*/
export function getKeyWithoutPrefix(key: string | undefined): string {
if (!key) {
return '';
}
const prefixPattern = new RegExp(
`^(${FIELD_CONTEXT_PREFIXES.join('|')})\\.`,
'i',
);
return key.replace(prefixPattern, '').trim();
}
/**
* Compares two keys by their base name (without prefix).
* This ensures that 'service.name' matches 'resource.service.name'
*/
export function isKeyMatch(
itemKey: string | undefined,
filterKey: string | undefined,
): boolean {
return getKeyWithoutPrefix(itemKey) === getKeyWithoutPrefix(filterKey);
}

View File

@@ -0,0 +1,25 @@
.roles-select-error {
display: flex;
align-items: center;
justify-content: space-between;
gap: 6px;
padding: 4px 8px;
color: var(--bg-cherry-500);
font-size: 12px;
&__msg {
display: flex;
align-items: center;
gap: 6px;
}
&__retry-btn {
display: flex;
align-items: center;
background: none;
border: none;
cursor: pointer;
padding: 2px;
color: var(--bg-cherry-500);
}
}

View File

@@ -0,0 +1,171 @@
import { CircleAlert, RefreshCw } from '@signozhq/icons';
import { Checkbox, Select } from 'antd';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { useListRoles } from 'api/generated/services/role';
import type { RoletypesRoleDTO } from 'api/generated/services/sigNoz.schemas';
import APIError from 'types/api/error';
import './RolesSelect.styles.scss';
export interface RoleOption {
label: string;
value: string;
}
export function useRoles(): {
roles: RoletypesRoleDTO[];
isLoading: boolean;
isError: boolean;
error: APIError | undefined;
refetch: () => void;
} {
const { data, isLoading, isError, error, refetch } = useListRoles();
return {
roles: data?.data ?? [],
isLoading,
isError,
error: convertToApiError(error),
refetch,
};
}
export function getRoleOptions(roles: RoletypesRoleDTO[]): RoleOption[] {
return roles.map((role) => ({
label: role.name ?? '',
value: role.name ?? '',
}));
}
function ErrorContent({
error,
onRefetch,
}: {
error?: APIError;
onRefetch?: () => void;
}): JSX.Element {
const errorMessage = error?.message || 'Failed to load roles';
return (
<div className="roles-select-error">
<span className="roles-select-error__msg">
<CircleAlert size={12} />
{errorMessage}
</span>
{onRefetch && (
<button
type="button"
onClick={(e): void => {
e.stopPropagation();
onRefetch();
}}
className="roles-select-error__retry-btn"
title="Retry"
>
<RefreshCw size={12} />
</button>
)}
</div>
);
}
interface BaseProps {
id?: string;
placeholder?: string;
className?: string;
getPopupContainer?: (trigger: HTMLElement) => HTMLElement;
roles?: RoletypesRoleDTO[];
loading?: boolean;
isError?: boolean;
error?: APIError;
onRefetch?: () => void;
}
interface SingleProps extends BaseProps {
mode?: 'single';
value?: string;
onChange?: (role: string) => void;
}
interface MultipleProps extends BaseProps {
mode: 'multiple';
value?: string[];
onChange?: (roles: string[]) => void;
}
export type RolesSelectProps = SingleProps | MultipleProps;
function RolesSelect(props: RolesSelectProps): JSX.Element {
const externalRoles = props.roles;
const {
data,
isLoading: internalLoading,
isError: internalError,
error: internalErrorObj,
refetch: internalRefetch,
} = useListRoles({
query: { enabled: externalRoles === undefined },
});
const roles = externalRoles ?? data?.data ?? [];
const options = getRoleOptions(roles);
const {
mode,
id,
placeholder = 'Select role',
className,
getPopupContainer,
loading = internalLoading,
isError = internalError,
error = convertToApiError(internalErrorObj),
onRefetch = externalRoles === undefined ? internalRefetch : undefined,
} = props;
const notFoundContent = isError ? (
<ErrorContent error={error} onRefetch={onRefetch} />
) : undefined;
if (mode === 'multiple') {
const { value = [], onChange } = props as MultipleProps;
return (
<Select
id={id}
mode="multiple"
value={value}
onChange={onChange}
placeholder={placeholder}
className={className}
loading={loading}
notFoundContent={notFoundContent}
options={options}
optionRender={(option): JSX.Element => (
<Checkbox
checked={value.includes(option.value as string)}
style={{ pointerEvents: 'none' }}
>
{option.label}
</Checkbox>
)}
getPopupContainer={getPopupContainer}
/>
);
}
const { value, onChange } = props as SingleProps;
return (
<Select
id={id}
value={value}
onChange={onChange}
placeholder={placeholder}
className={className}
loading={loading}
notFoundContent={notFoundContent}
options={options}
getPopupContainer={getPopupContainer}
/>
);
}
export default RolesSelect;

View File

@@ -0,0 +1,2 @@
export type { RoleOption, RolesSelectProps } from './RolesSelect';
export { default, getRoleOptions, useRoles } from './RolesSelect';

View File

@@ -0,0 +1,179 @@
.add-key-modal {
[data-slot='dialog-description'] {
padding: 0;
}
&__form {
display: flex;
flex-direction: column;
gap: var(--spacing-8);
padding: var(--padding-4);
}
&__field {
display: flex;
flex-direction: column;
gap: var(--spacing-4);
}
&__label {
font-size: var(--font-size-sm);
font-weight: var(--font-weight-normal);
color: var(--foreground);
line-height: var(--line-height-20);
letter-spacing: -0.07px;
}
&__input {
height: 32px;
background: var(--l2-background);
border-color: var(--border);
color: var(--l1-foreground);
box-shadow: none;
&::placeholder {
color: var(--l3-foreground);
}
}
&__expiry-toggle {
width: 60%;
display: flex;
border: 1px solid var(--border);
border-radius: 2px;
overflow: hidden;
padding: 0;
gap: 0;
[data-slot='toggle-group'] {
width: 100%;
display: flex;
}
&-btn {
flex: 1;
height: 32px;
border-radius: 0;
font-size: var(--label-small-400-font-size);
font-weight: var(--label-small-400-font-weight);
line-height: var(--label-small-400-line-height);
justify-content: center;
background: transparent;
border: none;
border-right: 1px solid var(--border);
color: var(--foreground);
&:last-child {
border-right: none;
}
&[data-state='on'] {
background: var(--l2-background);
color: var(--l1-foreground);
}
}
}
&__datepicker {
width: 100%;
height: 32px;
.ant-picker {
background: var(--l2-background);
border-color: var(--border);
border-radius: 2px;
width: 100%;
height: 32px;
input {
color: var(--l1-foreground);
font-size: var(--font-size-sm);
}
.ant-picker-suffix {
color: var(--foreground);
}
}
.add-key-modal-datepicker-popup {
border-radius: 4px;
border: 1px solid var(--secondary);
background: var(--popover);
box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2);
}
}
&__key-display {
display: flex;
align-items: center;
height: 32px;
background: var(--l2-background);
border: 1px solid var(--border);
border-radius: 2px;
overflow: hidden;
}
&__key-text {
flex: 1;
min-width: 0;
padding: 0 var(--padding-2);
font-size: var(--font-size-sm);
color: var(--l1-foreground);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-family: monospace;
}
&__copy-btn {
flex-shrink: 0;
height: 32px;
border-radius: 0 2px 2px 0;
border-top: none;
border-right: none;
border-bottom: none;
border-left: 1px solid var(--border);
min-width: 40px;
}
&__expiry-meta {
display: flex;
flex-direction: column;
gap: var(--spacing-2);
}
&__expiry-label {
font-size: var(--font-size-xs);
font-weight: var(--font-weight-medium);
color: var(--foreground);
letter-spacing: 0.48px;
text-transform: uppercase;
}
&__footer {
display: flex;
align-items: center;
justify-content: flex-end;
padding: var(--padding-4);
border-top: 1px solid var(--secondary);
}
&__footer-right {
display: flex;
align-items: center;
gap: var(--spacing-4);
}
&__learn-more {
display: inline-flex;
align-items: center;
gap: var(--spacing-1);
color: var(--primary);
font-size: var(--font-size-sm);
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
}

View File

@@ -0,0 +1,267 @@
import { useCallback, useEffect, useState } from 'react';
import { Badge } from '@signozhq/badge';
import { Button } from '@signozhq/button';
import { Callout } from '@signozhq/callout';
import { DialogWrapper } from '@signozhq/dialog';
import { Check, Copy } from '@signozhq/icons';
import { Input } from '@signozhq/input';
import { toast } from '@signozhq/sonner';
import { ToggleGroup, ToggleGroupItem } from '@signozhq/toggle-group';
import { DatePicker } from 'antd';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { useCreateServiceAccountKey } from 'api/generated/services/serviceaccount';
import type {
RenderErrorResponseDTO,
ServiceaccounttypesGettableFactorAPIKeyWithKeyDTO,
} from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import type { Dayjs } from 'dayjs';
import { popupContainer } from 'utils/selectPopupContainer';
import { disabledDate } from './utils';
import './AddKeyModal.styles.scss';
interface AddKeyModalProps {
open: boolean;
accountId: string;
onClose: () => void;
onSuccess: () => void;
}
type Phase = 'form' | 'created';
type ExpiryMode = 'none' | 'date';
function AddKeyModal({
open,
accountId,
onClose,
onSuccess,
}: AddKeyModalProps): JSX.Element {
const [phase, setPhase] = useState<Phase>('form');
const [keyName, setKeyName] = useState('');
const [expiryMode, setExpiryMode] = useState<ExpiryMode>('none');
const [expiryDate, setExpiryDate] = useState<Dayjs | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const [
createdKey,
setCreatedKey,
] = useState<ServiceaccounttypesGettableFactorAPIKeyWithKeyDTO | null>(null);
const [hasCopied, setHasCopied] = useState(false);
useEffect(() => {
if (open) {
setPhase('form');
setKeyName('');
setExpiryMode('none');
setExpiryDate(null);
setIsSubmitting(false);
setCreatedKey(null);
setHasCopied(false);
}
}, [open]);
const { mutateAsync: createKey } = useCreateServiceAccountKey();
const handleCreate = useCallback(async (): Promise<void> => {
if (!keyName.trim()) {
return;
}
setIsSubmitting(true);
try {
const expiresAt =
expiryMode === 'date' && expiryDate ? expiryDate.unix() : 0;
const response = await createKey({
pathParams: { id: accountId },
data: { name: keyName.trim(), expires_at: expiresAt },
});
const keyData = response?.data;
if (keyData) {
setCreatedKey(keyData);
setPhase('created');
}
} catch (error: unknown) {
const errMessage =
convertToApiError(
error as AxiosError<RenderErrorResponseDTO, unknown> | null,
)?.getErrorMessage() || 'Failed to create key';
toast.error(errMessage, { richColors: true });
} finally {
setIsSubmitting(false);
}
}, [keyName, expiryMode, expiryDate, accountId, createKey]);
const handleCopy = useCallback(async (): Promise<void> => {
if (!createdKey?.key) {
return;
}
try {
await navigator.clipboard.writeText(createdKey.key);
setHasCopied(true);
setTimeout(() => setHasCopied(false), 2000);
toast.success('Key copied to clipboard', { richColors: true });
} catch {
toast.error('Failed to copy key', { richColors: true });
}
}, [createdKey]);
const handleClose = useCallback((): void => {
if (phase === 'created') {
onSuccess();
}
onClose();
}, [phase, onSuccess, onClose]);
const expiryLabel = (): string => {
if (expiryMode === 'none' || !expiryDate) {
return 'Never';
}
try {
return expiryDate.format('MMM D, YYYY');
} catch {
return 'Never';
}
};
const title = phase === 'form' ? 'Add a New Key' : 'Key Created Successfully';
return (
<DialogWrapper
open={open}
onOpenChange={(isOpen): void => {
if (!isOpen) {
handleClose();
}
}}
title={title}
width="base"
className="add-key-modal"
showCloseButton
disableOutsideClick={false}
>
{phase === 'form' && (
<>
<div className="add-key-modal__form">
<div className="add-key-modal__field">
<label className="add-key-modal__label" htmlFor="key-name">
Name <span style={{ color: 'var(--destructive)' }}>*</span>
</label>
<Input
id="key-name"
value={keyName}
onChange={(e): void => setKeyName(e.target.value)}
placeholder="Enter key name e.g.: Service Owner"
className="add-key-modal__input"
/>
</div>
<div className="add-key-modal__field">
<span className="add-key-modal__label">Expiration</span>
<ToggleGroup
type="single"
value={expiryMode}
onValueChange={(val): void => {
if (val) {
setExpiryMode(val as ExpiryMode);
if (val === 'none') {
setExpiryDate(null);
}
}
}}
className="add-key-modal__expiry-toggle"
>
<ToggleGroupItem
value="none"
className="add-key-modal__expiry-toggle-btn"
>
No Expiration
</ToggleGroupItem>
<ToggleGroupItem
value="date"
className="add-key-modal__expiry-toggle-btn"
>
Set Expiration Date
</ToggleGroupItem>
</ToggleGroup>
</div>
{expiryMode === 'date' && (
<div className="add-key-modal__field">
<label className="add-key-modal__label" htmlFor="expiry-date">
Expiration Date
</label>
<div className="add-key-modal__datepicker">
<DatePicker
id="expiry-date"
value={expiryDate}
onChange={(date): void => setExpiryDate(date)}
popupClassName="add-key-modal-datepicker-popup"
getPopupContainer={popupContainer}
disabledDate={disabledDate}
/>
</div>
</div>
)}
</div>
<div className="add-key-modal__footer">
<div className="add-key-modal__footer-right">
<Button
variant="solid"
color="secondary"
size="sm"
onClick={handleClose}
>
Cancel
</Button>
<Button
variant="solid"
color="primary"
size="sm"
disabled={!keyName.trim() || isSubmitting}
onClick={handleCreate}
>
{isSubmitting ? 'Creating...' : 'Create Key'}
</Button>
</div>
</div>
</>
)}
{phase === 'created' && createdKey && (
<>
<div className="add-key-modal__form">
<div className="add-key-modal__field">
<span className="add-key-modal__label">Key</span>
<div className="add-key-modal__key-display">
<span className="add-key-modal__key-text">{createdKey.key}</span>
<Button
variant="outlined"
color="secondary"
size="sm"
onClick={handleCopy}
className="add-key-modal__copy-btn"
>
{hasCopied ? <Check size={12} /> : <Copy size={12} />}
</Button>
</div>
</div>
<div className="add-key-modal__expiry-meta">
<span className="add-key-modal__expiry-label">Expiration</span>
<Badge color="vanilla">{expiryLabel()}</Badge>
</div>
<Callout
type="info"
showIcon
message="Store the key securely. This is the only time it will be displayed."
/>
</div>
</>
)}
</DialogWrapper>
);
}
export default AddKeyModal;

View File

@@ -0,0 +1,188 @@
.edit-key-modal {
[data-slot='dialog-description'] {
padding: 0;
}
&__form {
display: flex;
flex-direction: column;
gap: var(--spacing-8);
padding: var(--padding-4);
}
&__field {
display: flex;
flex-direction: column;
gap: var(--spacing-4);
}
&__label {
font-size: 13px;
font-weight: var(--font-weight-normal);
color: var(--foreground);
line-height: var(--line-height-20);
letter-spacing: -0.07px;
}
&__input {
height: 32px;
background: var(--l2-background);
border-color: var(--border);
color: var(--l1-foreground);
box-shadow: none;
&::placeholder {
color: var(--l3-foreground);
}
}
&__key-display {
display: flex;
align-items: center;
justify-content: space-between;
height: 32px;
padding: 0 var(--padding-2);
border-radius: 2px;
background: var(--l2-background);
border: 1px solid var(--border);
cursor: not-allowed;
opacity: 0.8;
}
&__key-text {
font-size: 13px;
font-family: monospace;
color: var(--foreground);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
letter-spacing: 2px;
}
&__lock-icon {
color: var(--foreground);
flex-shrink: 0;
margin-left: 6px;
opacity: 0.6;
}
&__expiry-toggle {
width: 60%;
display: flex;
border: 1px solid var(--border);
border-radius: 2px;
overflow: hidden;
padding: 0;
gap: 0;
[data-slot='toggle-group'] {
width: 100%;
display: flex;
}
&-btn {
flex: 1;
height: 32px;
border-radius: 0;
font-size: var(--label-small-400-font-size);
font-weight: var(--label-small-400-font-weight);
line-height: var(--label-small-400-line-height);
justify-content: center;
background: transparent;
border: none;
border-right: 1px solid var(--border);
color: var(--foreground);
white-space: nowrap;
&:last-child {
border-right: none;
}
&[data-state='on'] {
background: var(--l2-background);
color: var(--l1-foreground);
}
}
}
&__datepicker {
width: 100%;
height: 32px;
.ant-picker {
background: var(--l2-background);
border-color: var(--border);
border-radius: 2px;
width: 100%;
height: 32px;
input {
color: var(--l1-foreground);
font-size: 13px;
}
.ant-picker-suffix {
color: var(--foreground);
}
}
.edit-key-modal-datepicker-popup {
border-radius: 4px;
border: 1px solid var(--secondary);
background: var(--popover);
box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2);
}
}
&__meta {
display: flex;
flex-direction: column;
gap: var(--spacing-2);
}
&__meta-label {
font-size: var(--font-size-xs);
font-weight: var(--font-weight-medium);
color: var(--foreground);
letter-spacing: 0.48px;
text-transform: uppercase;
}
&__footer {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--padding-4);
border-top: 1px solid var(--secondary);
}
&__footer-right {
display: flex;
align-items: center;
gap: var(--spacing-4);
}
&__footer-danger {
display: inline-flex;
align-items: center;
gap: var(--spacing-2);
padding: 0;
background: transparent;
border: none;
cursor: pointer;
color: var(--destructive);
font-size: var(--label-small-400-font-size);
font-weight: var(--label-small-400-font-weight);
transition: opacity 0.15s ease;
&:hover {
opacity: 0.8;
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
}

View File

@@ -0,0 +1,301 @@
import { useCallback, useEffect, useState } from 'react';
import { Badge } from '@signozhq/badge';
import { Button } from '@signozhq/button';
import { DialogFooter, DialogWrapper } from '@signozhq/dialog';
import { LockKeyhole, Trash2, X } from '@signozhq/icons';
import { Input } from '@signozhq/input';
import { toast } from '@signozhq/sonner';
import { ToggleGroup, ToggleGroupItem } from '@signozhq/toggle-group';
import { DatePicker } from 'antd';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import {
useRevokeServiceAccountKey,
useUpdateServiceAccountKey,
} from 'api/generated/services/serviceaccount';
import type {
RenderErrorResponseDTO,
ServiceaccounttypesFactorAPIKeyDTO,
} from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import type { Dayjs } from 'dayjs';
import dayjs from 'dayjs';
import { useTimezone } from 'providers/Timezone';
import { popupContainer } from 'utils/selectPopupContainer';
import { disabledDate, formatLastUsed } from './utils';
import './EditKeyModal.styles.scss';
interface EditKeyModalProps {
open: boolean;
accountId: string;
keyItem: ServiceaccounttypesFactorAPIKeyDTO | null;
onClose: () => void;
onSuccess: () => void;
}
type ExpiryMode = 'none' | 'date';
// eslint-disable-next-line sonarjs/cognitive-complexity
function EditKeyModal({
open,
accountId,
keyItem,
onClose,
onSuccess,
}: EditKeyModalProps): JSX.Element {
const { formatTimezoneAdjustedTimestamp } = useTimezone();
const [localName, setLocalName] = useState('');
const [expiryMode, setExpiryMode] = useState<ExpiryMode>('none');
const [localDate, setLocalDate] = useState<Dayjs | null>(null);
const [isRevokeConfirmOpen, setIsRevokeConfirmOpen] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [isRevoking, setIsRevoking] = useState(false);
useEffect(() => {
if (keyItem) {
setLocalName(keyItem.name ?? '');
if (keyItem.expires_at === 0) {
setExpiryMode('none');
setLocalDate(null);
} else {
setExpiryMode('date');
setLocalDate(dayjs.unix(keyItem.expires_at));
}
}
}, [keyItem]);
const originalExpiresAt = keyItem?.expires_at ?? 0;
const currentExpiresAt =
expiryMode === 'none' || !localDate ? 0 : localDate.unix();
const isDirty =
keyItem !== null &&
(localName !== (keyItem.name ?? '') ||
currentExpiresAt !== originalExpiresAt);
const { mutateAsync: updateKey } = useUpdateServiceAccountKey();
const { mutateAsync: revokeKey } = useRevokeServiceAccountKey();
const handleSave = useCallback(async (): Promise<void> => {
if (!keyItem || !isDirty) {
return;
}
setIsSaving(true);
try {
await updateKey({
pathParams: { id: accountId, fid: keyItem.id },
data: { name: localName, expires_at: currentExpiresAt },
});
toast.success('Key updated successfully', { richColors: true });
onSuccess();
} catch (error: unknown) {
const errMessage =
convertToApiError(
error as AxiosError<RenderErrorResponseDTO, unknown> | null,
)?.getErrorMessage() || 'Failed to update key';
toast.error(errMessage, { richColors: true });
} finally {
setIsSaving(false);
}
}, [
keyItem,
isDirty,
localName,
currentExpiresAt,
accountId,
updateKey,
onSuccess,
]);
const handleRevoke = useCallback(async (): Promise<void> => {
if (!keyItem) {
return;
}
setIsRevoking(true);
try {
await revokeKey({
pathParams: { id: accountId, fid: keyItem.id },
});
toast.success('Key revoked successfully', { richColors: true });
setIsRevokeConfirmOpen(false);
onSuccess();
} catch (error: unknown) {
const errMessage =
convertToApiError(
error as AxiosError<RenderErrorResponseDTO, unknown> | null,
)?.getErrorMessage() || 'Failed to revoke key';
toast.error(errMessage, { richColors: true });
} finally {
setIsRevoking(false);
}
}, [keyItem, accountId, revokeKey, onSuccess]);
return (
<DialogWrapper
open={open}
onOpenChange={(isOpen): void => {
if (!isOpen) {
if (isRevokeConfirmOpen) {
setIsRevokeConfirmOpen(false);
} else {
onClose();
}
}
}}
title={
isRevokeConfirmOpen
? `Revoke ${keyItem?.name ?? 'key'}?`
: 'Edit Key Details'
}
width={isRevokeConfirmOpen ? 'narrow' : 'base'}
className={
isRevokeConfirmOpen ? 'alert-dialog delete-dialog' : 'edit-key-modal'
}
showCloseButton={!isRevokeConfirmOpen}
disableOutsideClick={false}
>
{isRevokeConfirmOpen ? (
<>
<p className="delete-dialog__body">
Revoking this key will permanently invalidate it. Any systems using this
key will lose access immediately.
</p>
<DialogFooter className="delete-dialog__footer">
<Button
variant="solid"
color="secondary"
size="sm"
onClick={(): void => setIsRevokeConfirmOpen(false)}
>
<X size={12} />
Cancel
</Button>
<Button
variant="solid"
color="destructive"
size="sm"
disabled={isRevoking}
onClick={handleRevoke}
>
<Trash2 size={12} />
{isRevoking ? 'Revoking...' : 'Revoke Key'}
</Button>
</DialogFooter>
</>
) : (
<>
<div className="edit-key-modal__form">
<div className="edit-key-modal__field">
<label className="edit-key-modal__label" htmlFor="edit-key-name">
Name
</label>
<Input
id="edit-key-name"
value={localName}
onChange={(e): void => setLocalName(e.target.value)}
className="edit-key-modal__input"
placeholder="Enter key name"
/>
</div>
<div className="edit-key-modal__field">
<label className="edit-key-modal__label" htmlFor="edit-key-display">
Key
</label>
<div id="edit-key-display" className="edit-key-modal__key-display">
<span className="edit-key-modal__key-text">********************</span>
<LockKeyhole size={12} className="edit-key-modal__lock-icon" />
</div>
</div>
<div className="edit-key-modal__field">
<span className="edit-key-modal__label">Expiration</span>
<ToggleGroup
type="single"
value={expiryMode}
onValueChange={(val): void => {
if (val) {
setExpiryMode(val as ExpiryMode);
if (val === 'none') {
setLocalDate(null);
}
}
}}
className="edit-key-modal__expiry-toggle"
>
<ToggleGroupItem
value="none"
className="edit-key-modal__expiry-toggle-btn"
>
No Expiration
</ToggleGroupItem>
<ToggleGroupItem
value="date"
className="edit-key-modal__expiry-toggle-btn"
>
Set Expiration Date
</ToggleGroupItem>
</ToggleGroup>
</div>
{expiryMode === 'date' && (
<div className="edit-key-modal__field">
<label className="edit-key-modal__label" htmlFor="edit-key-datepicker">
Expiration Date
</label>
<div className="edit-key-modal__datepicker">
<DatePicker
value={localDate}
id="edit-key-datepicker"
onChange={(date): void => setLocalDate(date)}
popupClassName="edit-key-modal-datepicker-popup"
getPopupContainer={popupContainer}
disabledDate={disabledDate}
/>
</div>
</div>
)}
<div className="edit-key-modal__meta">
<span className="edit-key-modal__meta-label">Last Used</span>
<Badge color="vanilla">
{formatLastUsed(
keyItem?.last_used ?? null,
formatTimezoneAdjustedTimestamp,
)}
</Badge>
</div>
</div>
<div className="edit-key-modal__footer">
<Button
type="button"
className="edit-key-modal__footer-danger"
onClick={(): void => setIsRevokeConfirmOpen(true)}
>
<Trash2 size={12} />
Revoke Key
</Button>
<div className="edit-key-modal__footer-right">
<Button variant="solid" color="secondary" size="sm" onClick={onClose}>
<X size={12} />
Cancel
</Button>
<Button
variant="solid"
color="primary"
size="sm"
disabled={!isDirty || isSaving}
onClick={handleSave}
>
{isSaving ? 'Saving...' : 'Save Changes'}
</Button>
</div>
</div>
</>
)}
</DialogWrapper>
);
}
export default EditKeyModal;

View File

@@ -0,0 +1,243 @@
import { useCallback, useMemo, useState } from 'react';
import { Button } from '@signozhq/button';
import { DialogFooter, DialogWrapper } from '@signozhq/dialog';
import { Trash2, X } from '@signozhq/icons';
import { toast } from '@signozhq/sonner';
import { Skeleton, Tooltip } from 'antd';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { useRevokeServiceAccountKey } from 'api/generated/services/serviceaccount';
import type {
RenderErrorResponseDTO,
ServiceaccounttypesFactorAPIKeyDTO,
} from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import dayjs from 'dayjs';
import { useTimezone } from 'providers/Timezone';
import EditKeyModal from './EditKeyModal';
import { formatLastUsed } from './utils';
interface KeysTabProps {
accountId: string;
keys: ServiceaccounttypesFactorAPIKeyDTO[];
isLoading: boolean;
isDisabled?: boolean;
currentPage: number;
pageSize: number;
onRefetch: () => void;
onAddKeyClick: () => void;
}
function formatExpiry(expiresAt: number): JSX.Element {
if (expiresAt === 0) {
return <span className="keys-tab__expiry--never">Never</span>;
}
const expiryDate = dayjs.unix(expiresAt);
if (expiryDate.isBefore(dayjs())) {
return <span className="keys-tab__expiry--expired">Expired</span>;
}
return <span>{expiryDate.format('MMM D, YYYY')}</span>;
}
function KeysTab({
accountId,
keys,
isLoading,
isDisabled = false,
currentPage,
pageSize,
onRefetch,
onAddKeyClick,
}: KeysTabProps): JSX.Element {
const { formatTimezoneAdjustedTimestamp } = useTimezone();
const [
editKey,
setEditKey,
] = useState<ServiceaccounttypesFactorAPIKeyDTO | null>(null);
const [
revokeTarget,
setRevokeTarget,
] = useState<ServiceaccounttypesFactorAPIKeyDTO | null>(null);
const [isRevoking, setIsRevoking] = useState(false);
const { mutateAsync: revokeKey } = useRevokeServiceAccountKey();
const handleRevoke = useCallback(async (): Promise<void> => {
if (!revokeTarget) {
return;
}
setIsRevoking(true);
try {
await revokeKey({
pathParams: { id: accountId, fid: revokeTarget.id },
});
toast.success('Key revoked successfully', { richColors: true });
setRevokeTarget(null);
onRefetch();
} catch (error: unknown) {
const errMessage =
convertToApiError(
error as AxiosError<RenderErrorResponseDTO, unknown> | null,
)?.getErrorMessage() || 'Failed to revoke key';
toast.error(errMessage, { richColors: true });
} finally {
setIsRevoking(false);
}
}, [revokeTarget, revokeKey, accountId, onRefetch]);
const handleKeySuccess = useCallback((): void => {
setEditKey(null);
onRefetch();
}, [onRefetch]);
const handleFormatLastUsed = useCallback(
(lastUsed: Date | null | undefined): string =>
formatLastUsed(lastUsed, formatTimezoneAdjustedTimestamp),
[formatTimezoneAdjustedTimestamp],
);
const paginatedKeys = useMemo(() => {
const start = (currentPage - 1) * pageSize;
return keys.slice(start, start + pageSize);
}, [keys, currentPage, pageSize]);
if (isLoading) {
return (
<div className="keys-tab__loading">
<Skeleton active paragraph={{ rows: 4 }} />
</div>
);
}
if (keys.length === 0) {
return (
<div className="keys-tab__empty">
<span className="keys-tab__empty-emoji" role="img" aria-label="searching">
🧐
</span>
<p className="keys-tab__empty-text">No keys. Start by creating one.</p>
<Button
type="button"
className="keys-tab__learn-more"
onClick={onAddKeyClick}
disabled={isDisabled}
>
+ Add your first key
</Button>
</div>
);
}
return (
<>
<div className="keys-tab__table-wrap">
<div className="keys-tab__table-header">
<span className="keys-tab__col-name">Name</span>
<span className="keys-tab__col-expiry">Expiry</span>
<span className="keys-tab__col-last-used">Last Used</span>
<span className="keys-tab__col-action" />
</div>
<div className="keys-tab__scroll">
{paginatedKeys.map((keyItem, idx) => (
<div
key={keyItem.id}
className={`keys-tab__table-row${
idx % 2 === 0 ? ' keys-tab__table-row--alt' : ''
}${isDisabled ? ' keys-tab__table-row--disabled' : ''}`}
onClick={(): void => {
if (!isDisabled) {
setEditKey(keyItem);
}
}}
role="button"
tabIndex={0}
onKeyDown={(e): void => {
if (e.key === 'Enter' && !isDisabled) {
setEditKey(keyItem);
}
}}
>
<span className="keys-tab__col-name keys-tab__name-text">
{keyItem.name ?? '—'}
</span>
<span className="keys-tab__col-expiry">
{formatExpiry(keyItem.expires_at)}
</span>
<span className="keys-tab__col-last-used">
{handleFormatLastUsed(keyItem?.last_used ?? null)}
</span>
<span className="keys-tab__col-action">
<Tooltip title={isDisabled ? 'Service account disabled' : 'Revoke Key'}>
<Button
variant="ghost"
size="xs"
color="destructive"
disabled={isDisabled}
onClick={(e): void => {
e.stopPropagation();
setRevokeTarget(keyItem);
}}
className="keys-tab__revoke-btn"
>
<X size={12} />
</Button>
</Tooltip>
</span>
</div>
))}
</div>
</div>
<DialogWrapper
open={revokeTarget !== null}
onOpenChange={(isOpen): void => {
if (!isOpen) {
setRevokeTarget(null);
}
}}
title={`Revoke ${revokeTarget?.name ?? 'key'}?`}
width="narrow"
className="alert-dialog delete-dialog"
showCloseButton={false}
disableOutsideClick={false}
>
<p className="delete-dialog__body">
Revoking this key will permanently invalidate it. Any systems using this
key will lose access immediately.
</p>
<DialogFooter className="delete-dialog__footer">
<Button
variant="solid"
color="secondary"
size="sm"
onClick={(): void => setRevokeTarget(null)}
>
<X size={12} />
Cancel
</Button>
<Button
variant="solid"
color="destructive"
size="sm"
disabled={isRevoking}
onClick={handleRevoke}
>
<Trash2 size={12} />
{isRevoking ? 'Revoking...' : 'Revoke Key'}
</Button>
</DialogFooter>
</DialogWrapper>
<EditKeyModal
open={editKey !== null}
accountId={accountId}
keyItem={editKey}
onClose={(): void => setEditKey(null)}
onSuccess={handleKeySuccess}
/>
</>
);
}
export default KeysTab;

View File

@@ -0,0 +1,154 @@
import { useCallback } from 'react';
import { Badge } from '@signozhq/badge';
import { LockKeyhole } from '@signozhq/icons';
import { Input } from '@signozhq/input';
import type { RoletypesRoleDTO } from 'api/generated/services/sigNoz.schemas';
import RolesSelect from 'components/RolesSelect';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { ServiceAccountRow } from 'container/ServiceAccountsSettings/utils';
import { useTimezone } from 'providers/Timezone';
import APIError from 'types/api/error';
interface OverviewTabProps {
account: ServiceAccountRow;
localName: string;
onNameChange: (v: string) => void;
localRoles: string[];
onRolesChange: (v: string[]) => void;
isDisabled: boolean;
availableRoles: RoletypesRoleDTO[];
rolesLoading?: boolean;
rolesError?: boolean;
rolesErrorObj?: APIError | undefined;
onRefetchRoles?: () => void;
}
function OverviewTab({
account,
localName,
onNameChange,
localRoles,
onRolesChange,
isDisabled,
availableRoles,
rolesLoading,
rolesError,
rolesErrorObj,
onRefetchRoles,
}: OverviewTabProps): JSX.Element {
const { formatTimezoneAdjustedTimestamp } = useTimezone();
const formatTimestamp = useCallback(
(ts: string | null | undefined): string => {
if (!ts) {
return '—';
}
const d = new Date(ts);
if (Number.isNaN(d.getTime())) {
return '—';
}
return formatTimezoneAdjustedTimestamp(ts, DATE_TIME_FORMATS.DASH_DATETIME);
},
[formatTimezoneAdjustedTimestamp],
);
return (
<>
<div className="sa-drawer__field">
<label className="sa-drawer__label" htmlFor="sa-name">
Name
</label>
{isDisabled ? (
<div className="sa-drawer__input-wrapper sa-drawer__input-wrapper--disabled">
<span className="sa-drawer__input-text">{localName || '—'}</span>
<LockKeyhole size={14} className="sa-drawer__lock-icon" />
</div>
) : (
<Input
id="sa-name"
value={localName}
onChange={(e): void => onNameChange(e.target.value)}
className="sa-drawer__input"
placeholder="Enter name"
/>
)}
</div>
<div className="sa-drawer__field">
<label className="sa-drawer__label" htmlFor="sa-email">
Email Address
</label>
<div className="sa-drawer__input-wrapper sa-drawer__input-wrapper--disabled">
<span className="sa-drawer__input-text">{account.email || '—'}</span>
<LockKeyhole size={14} className="sa-drawer__lock-icon" />
</div>
</div>
<div className="sa-drawer__field">
<label className="sa-drawer__label" htmlFor="sa-roles">
Roles
</label>
{isDisabled ? (
<div className="sa-drawer__input-wrapper sa-drawer__input-wrapper--disabled">
<div className="sa-drawer__disabled-roles">
{localRoles.length > 0 ? (
localRoles.map((r) => (
<Badge key={r} color="vanilla">
{r}
</Badge>
))
) : (
<span className="sa-drawer__input-text"></span>
)}
</div>
<LockKeyhole size={14} className="sa-drawer__lock-icon" />
</div>
) : (
<RolesSelect
id="sa-roles"
mode="multiple"
roles={availableRoles}
loading={rolesLoading}
isError={rolesError}
error={rolesErrorObj}
onRefetch={onRefetchRoles}
value={localRoles}
onChange={onRolesChange}
placeholder="Select roles"
className="sa-drawer__role-select"
getPopupContainer={(triggerNode): HTMLElement =>
(triggerNode?.closest('.sa-drawer') as HTMLElement) || document.body
}
/>
)}
</div>
<div className="sa-drawer__meta">
<div className="sa-drawer__meta-item">
<span className="sa-drawer__meta-label">Status</span>
{account.status?.toUpperCase() === 'ACTIVE' ? (
<Badge color="forest" variant="outline">
ACTIVE
</Badge>
) : (
<Badge color="vanilla" variant="outline" className="sa-status-badge">
DISABLED
</Badge>
)}
</div>
<div className="sa-drawer__meta-item">
<span className="sa-drawer__meta-label">Created At</span>
<Badge color="vanilla">{formatTimestamp(account.createdAt)}</Badge>
</div>
<div className="sa-drawer__meta-item">
<span className="sa-drawer__meta-label">Updated At</span>
<Badge color="vanilla">{formatTimestamp(account.updatedAt)}</Badge>
</div>
</div>
</>
);
}
export default OverviewTab;

View File

@@ -0,0 +1,548 @@
.sa-drawer {
[data-slot='drawer-close'] + div {
border-left: 1px solid var(--l1-border);
padding-left: var(--padding-4);
margin-left: var(--margin-2);
}
&__layout {
display: flex;
flex-direction: column;
height: calc(100vh - 48px);
}
&__tabs {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--padding-3) var(--padding-4) var(--padding-2) var(--padding-4);
flex-shrink: 0;
}
&__tab-group {
[data-slot='toggle-group'] {
height: 32px;
border-radius: 2px;
border: 1px solid var(--l1-border);
background: var(--l2-background);
gap: 0;
}
[data-slot='toggle-group-item'] {
height: 32px;
border-radius: 0;
border-left: 1px solid var(--l1-border);
background: transparent;
color: var(--l2-foreground);
font-size: 12px;
font-weight: 400;
font-family: Inter, sans-serif;
padding: 0 var(--padding-7);
gap: var(--spacing-3);
box-shadow: none;
&:first-child {
border-left: none;
border-radius: 2px 0 0 2px;
}
&:last-child {
border-radius: 0 2px 2px 0;
}
&:hover {
background: rgba(171, 189, 255, 0.04);
color: var(--l1-foreground);
}
&[data-state='on'] {
background: var(--l1-border);
color: var(--l1-foreground);
}
}
}
&__tab {
display: inline-flex;
align-items: center;
gap: var(--spacing-2);
font-size: var(--font-size-xs);
font-weight: var(--font-weight-normal);
}
&__tab-count {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 20px;
padding: 0 6px;
border-radius: 50px;
background: var(--secondary);
font-size: 11px;
font-weight: var(--font-weight-normal);
line-height: 20px;
color: var(--foreground);
letter-spacing: -0.06px;
}
&__body {
flex: 1;
overflow-y: auto;
padding: var(--padding-5) var(--padding-4);
display: flex;
flex-direction: column;
gap: var(--spacing-8);
}
&__footer {
height: 56px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 var(--padding-4);
border-top: 1px solid var(--secondary);
background: var(--card);
}
&__keys-pagination {
display: flex;
align-items: center;
justify-content: flex-end;
width: 100%;
padding: var(--padding-2) 0;
.ant-pagination-total-text {
margin-right: auto;
}
}
&__pagination-range {
font-size: var(--font-size-xs);
color: var(--foreground);
font-weight: var(--font-weight-normal);
}
&__pagination-total {
font-size: var(--font-size-xs);
color: var(--foreground);
opacity: 0.5;
}
&__footer-btn {
padding-left: 0;
padding-right: 0;
}
&__footer-right {
display: flex;
align-items: center;
gap: var(--spacing-6);
}
&__field {
display: flex;
flex-direction: column;
gap: var(--spacing-4);
}
&__label {
font-size: var(--font-size-sm);
font-weight: var(--font-weight-normal);
color: var(--foreground);
line-height: var(--line-height-20);
letter-spacing: -0.07px;
cursor: default;
}
&__input {
height: 32px;
background: var(--l2-background);
border-color: var(--border);
color: var(--l1-foreground);
box-shadow: none;
&::placeholder {
color: var(--l3-foreground);
}
}
&__input-wrapper {
display: flex;
align-items: center;
justify-content: space-between;
height: 32px;
padding: 0 var(--padding-2);
border-radius: 2px;
background: var(--l2-background);
border: 1px solid var(--border);
&--disabled {
cursor: not-allowed;
opacity: 0.8;
}
}
&__input-text {
font-size: var(--font-size-sm);
font-weight: var(--font-weight-normal);
color: var(--foreground);
line-height: var(--line-height-18);
letter-spacing: -0.07px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
}
&__lock-icon {
color: var(--foreground);
flex-shrink: 0;
margin-left: 6px;
opacity: 0.6;
}
&__role-select {
width: 100%;
min-height: 32px;
.ant-select-selector {
background-color: var(--l2-background) !important;
border-color: var(--border) !important;
border-radius: 2px;
padding: 2px var(--padding-2) !important;
display: flex;
align-items: center;
flex-wrap: wrap;
min-height: 32px;
}
.ant-select-selection-overflow {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 2px;
padding: 2px 0;
}
.ant-select-selection-overflow-item {
display: flex;
align-items: center;
}
.ant-select-selection-item {
display: flex;
align-items: center;
height: 22px;
font-size: var(--font-size-sm);
color: var(--l1-foreground);
background: var(--l3-background);
border: 1px solid var(--border);
border-radius: 2px;
padding: 0 4px 0 6px;
line-height: 20px;
letter-spacing: -0.07px;
margin: 0;
}
.ant-select-selection-item-remove {
display: flex;
align-items: center;
color: var(--foreground);
margin-left: 2px;
}
.ant-select-selection-placeholder {
font-size: var(--font-size-sm);
color: var(--l3-foreground);
}
.ant-select-arrow {
color: var(--foreground);
}
&:not(.ant-select-disabled):hover .ant-select-selector {
border-color: var(--foreground);
}
}
&__disabled-roles {
display: flex;
flex-wrap: wrap;
gap: var(--spacing-2);
}
&__meta {
display: flex;
flex-direction: column;
gap: var(--spacing-8);
margin-top: var(--margin-1);
}
&__meta-item {
display: flex;
flex-direction: column;
gap: var(--spacing-2);
[data-slot='badge'] {
padding: var(--padding-1) var(--padding-2);
align-items: center;
font-size: var(--uppercase-small-500-font-size);
font-weight: var(--uppercase-small-500-font-weight);
line-height: 100%;
letter-spacing: 0.44px;
text-transform: uppercase;
}
}
&__meta-label {
font-size: var(--font-size-xs);
font-weight: var(--font-weight-medium);
color: var(--foreground);
line-height: var(--line-height-20);
letter-spacing: 0.48px;
text-transform: uppercase;
}
}
.keys-tab {
&__loading {
display: flex;
align-items: center;
justify-content: center;
padding: var(--padding-8) var(--padding-4);
}
&__empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: var(--padding-8) var(--padding-4);
gap: var(--spacing-4);
text-align: center;
height: 80%;
}
&__empty-emoji {
font-size: 32px;
}
&__empty-text {
font-size: var(--paragraph-base-400-font-size);
font-weight: var(--paragraph-base-400-font-weight);
color: var(--foreground);
margin: 0;
}
&__learn-more {
background: transparent;
border: none;
color: var(--primary);
font-size: var(--font-size-sm);
cursor: pointer;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
&__table-wrap {
border: 1px solid var(--l1-border);
border-radius: 4px;
overflow: hidden;
}
&__table-header {
display: flex;
align-items: center;
gap: var(--spacing-2);
height: 38px;
padding: 0 var(--padding-4);
border-bottom: 1px solid var(--l1-border);
background: transparent;
}
&__col-name {
flex: 1;
min-width: 0;
font-size: 11px;
font-weight: var(--font-weight-medium);
color: var(--l2-foreground);
letter-spacing: 0.44px;
text-transform: uppercase;
}
&__col-expiry {
width: 100px;
text-align: right;
font-size: 11px;
font-weight: var(--font-weight-medium);
color: var(--l2-foreground);
letter-spacing: 0.44px;
text-transform: uppercase;
flex-shrink: 0;
}
&__col-last-used {
width: 180px;
text-align: right;
font-size: 11px;
font-weight: var(--font-weight-medium);
color: var(--l2-foreground);
letter-spacing: 0.44px;
text-transform: uppercase;
flex-shrink: 0;
}
&__col-action {
width: 40px;
display: flex;
justify-content: flex-end;
flex-shrink: 0;
}
&__table-row {
display: flex;
align-items: center;
gap: var(--spacing-2);
height: 38px;
padding: 0 var(--padding-4);
border-bottom: 1px solid var(--l1-border);
cursor: pointer;
&:last-of-type {
border-bottom: none;
}
&:not(.keys-tab__table-row--disabled):hover {
background: rgba(171, 189, 255, 0.06);
}
&--alt {
background: rgba(171, 189, 255, 0.02);
&:not(.keys-tab__table-row--disabled):hover {
background: rgba(171, 189, 255, 0.06);
}
}
&--disabled {
cursor: not-allowed;
opacity: 0.6;
&:hover {
background: transparent;
}
}
.keys-tab__col-expiry,
.keys-tab__col-last-used {
font-size: var(--font-size-sm);
font-weight: var(--font-weight-normal);
color: var(--l2-foreground);
letter-spacing: -0.07px;
text-transform: none;
}
}
&__name-text {
font-size: var(--font-size-sm);
font-weight: var(--font-weight-normal);
color: var(--l2-foreground);
letter-spacing: -0.07px;
text-transform: none;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
&__expiry--never {
color: var(--l2-foreground);
}
&__expiry--expired {
color: var(--l3-foreground);
}
&__revoke-btn {
width: 32px;
height: 32px;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
}
&__scroll {
overflow-y: auto;
max-height: calc(100% - 20px);
scrollbar-width: thin;
scrollbar-color: var(--l2-border) transparent;
&::-webkit-scrollbar {
width: 4px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: var(--l1-border);
border-radius: 4px;
}
}
&__pagination {
display: flex;
align-items: center;
justify-content: flex-end;
padding: var(--padding-2) var(--padding-4);
border-top: 1px solid var(--l1-border);
}
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
// Activate/disable confirm dialogs
.sa-activate-dialog,
.sa-disable-dialog {
background: var(--l2-background);
border: 1px solid var(--l2-border);
[data-slot='dialog-title'] {
color: var(--l1-foreground);
}
&__body {
font-size: var(--paragraph-base-400-font-size);
font-weight: var(--paragraph-base-400-font-weight);
color: var(--l2-foreground);
line-height: var(--paragraph-base-400-line-height);
letter-spacing: -0.065px;
margin: 0;
strong {
font-weight: var(--font-weight-medium);
color: var(--l1-foreground);
}
}
&__footer {
display: flex;
justify-content: flex-end;
gap: var(--spacing-4);
margin-top: var(--margin-6);
}
}

View File

@@ -0,0 +1,447 @@
import { useCallback, useEffect, useState } from 'react';
import { Button } from '@signozhq/button';
import { DialogFooter, DialogWrapper } from '@signozhq/dialog';
import { DrawerWrapper } from '@signozhq/drawer';
import {
Check,
Key,
LayoutGrid,
Plus,
PowerOff,
Trash2,
X,
} from '@signozhq/icons';
import { toast } from '@signozhq/sonner';
import { ToggleGroup, ToggleGroupItem } from '@signozhq/toggle-group';
import { Pagination } from 'antd';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import {
useListServiceAccountKeys,
useUpdateServiceAccount,
useUpdateServiceAccountStatus,
} from 'api/generated/services/serviceaccount';
import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import { useRoles } from 'components/RolesSelect';
import { ServiceAccountRow } from 'container/ServiceAccountsSettings/utils';
import AddKeyModal from './AddKeyModal';
import KeysTab from './KeysTab';
import OverviewTab from './OverviewTab';
import './ServiceAccountDrawer.styles.scss';
export interface ServiceAccountDrawerProps {
account: ServiceAccountRow | null;
open: boolean;
onClose: () => void;
onSuccess: () => void;
}
const PAGE_SIZE = 15;
// eslint-disable-next-line sonarjs/cognitive-complexity
function ServiceAccountDrawer({
account,
open,
onClose,
onSuccess,
}: ServiceAccountDrawerProps): JSX.Element {
const [activeTab, setActiveTab] = useState<'overview' | 'keys'>('overview');
const [isActivateConfirmOpen, setIsActivateConfirmOpen] = useState(false);
const [isDisableConfirmOpen, setIsDisableConfirmOpen] = useState(false);
const [localName, setLocalName] = useState('');
const [localRoles, setLocalRoles] = useState<string[]>([]);
const [isSaving, setIsSaving] = useState(false);
const [isDisabling, setIsDisabling] = useState(false);
const [isActivating, setIsActivating] = useState(false);
const [isAddKeyOpen, setIsAddKeyOpen] = useState(false);
const [keysPage, setKeysPage] = useState(1);
useEffect(() => {
if (account) {
setLocalName(account.name ?? '');
setLocalRoles(account.roles ?? []);
setActiveTab('overview');
setKeysPage(1);
}
}, [account]);
const isDisabled = account?.status?.toUpperCase() !== 'ACTIVE';
const isDirty =
account !== null &&
(localName !== (account.name ?? '') ||
JSON.stringify(localRoles) !== JSON.stringify(account.roles ?? []));
const {
roles: availableRoles,
isLoading: rolesLoading,
isError: rolesError,
error: rolesErrorObj,
refetch: refetchRoles,
} = useRoles();
const {
data: keysData,
isLoading: keysLoading,
refetch: refetchKeys,
} = useListServiceAccountKeys(
{ id: account?.id ?? '' },
{ query: { enabled: !!account?.id } },
);
const keys = keysData?.data ?? [];
useEffect(() => {
if (keysLoading) {
return;
}
const maxPage = Math.max(1, Math.ceil(keys.length / PAGE_SIZE));
if (keysPage > maxPage) {
setKeysPage(maxPage);
}
}, [keysLoading, keys.length, keysPage]);
const { mutateAsync: updateAccount } = useUpdateServiceAccount();
const { mutateAsync: updateStatus } = useUpdateServiceAccountStatus();
const handleSave = useCallback(async (): Promise<void> => {
if (!account || !isDirty) {
return;
}
setIsSaving(true);
try {
await updateAccount({
pathParams: { id: account.id },
data: { name: localName, email: account.email, roles: localRoles },
});
toast.success('Service account updated successfully', { richColors: true });
onSuccess();
} catch (error: unknown) {
const errMessage =
convertToApiError(
error as AxiosError<RenderErrorResponseDTO, unknown> | null,
)?.getErrorMessage() || 'Failed to update service account';
toast.error(errMessage, { richColors: true });
} finally {
setIsSaving(false);
}
}, [account, isDirty, localName, localRoles, updateAccount, onSuccess]);
const handleDisable = useCallback(async (): Promise<void> => {
if (!account) {
return;
}
setIsDisabling(true);
try {
await updateStatus({
pathParams: { id: account.id },
data: { status: 'DISABLED' },
});
toast.success('Service account disabled', { richColors: true });
setIsDisableConfirmOpen(false);
onSuccess();
} catch (error: unknown) {
const errMessage =
convertToApiError(
error as AxiosError<RenderErrorResponseDTO, unknown> | null,
)?.getErrorMessage() || 'Failed to disable service account';
toast.error(errMessage, { richColors: true });
} finally {
setIsDisabling(false);
}
}, [account, updateStatus, onSuccess]);
const handleActivate = useCallback(async (): Promise<void> => {
if (!account) {
return;
}
setIsActivating(true);
try {
await updateStatus({
pathParams: { id: account.id },
data: { status: 'ACTIVE' },
});
toast.success('Service account activated', { richColors: true });
setIsActivateConfirmOpen(false);
onSuccess();
} catch (error: unknown) {
const errMessage =
convertToApiError(
error as AxiosError<RenderErrorResponseDTO, unknown> | null,
)?.getErrorMessage() || 'Failed to activate service account';
toast.error(errMessage, { richColors: true });
} finally {
setIsActivating(false);
}
}, [account, updateStatus, onSuccess]);
const handleClose = useCallback((): void => {
setIsActivateConfirmOpen(false);
setIsDisableConfirmOpen(false);
setIsAddKeyOpen(false);
onClose();
}, [onClose]);
const handleKeySuccess = useCallback((): void => {
setIsAddKeyOpen(false);
refetchKeys();
}, [refetchKeys]);
const drawerContent = (
<div className="sa-drawer__layout">
<div className="sa-drawer__tabs">
<ToggleGroup
type="single"
value={activeTab}
onValueChange={(val): void => {
if (val) {
setActiveTab(val as 'overview' | 'keys');
}
}}
className="sa-drawer__tab-group"
>
<ToggleGroupItem value="overview" className="sa-drawer__tab">
<LayoutGrid size={14} />
Overview
</ToggleGroupItem>
<ToggleGroupItem value="keys" className="sa-drawer__tab">
<Key size={14} />
Keys
{keys.length > 0 && (
<span className="sa-drawer__tab-count">{keys.length}</span>
)}
</ToggleGroupItem>
</ToggleGroup>
{activeTab === 'keys' && (
<Button
variant="outlined"
size="sm"
color="secondary"
disabled={isDisabled}
onClick={(): void => setIsAddKeyOpen(true)}
>
<Plus size={12} />
Add Key
</Button>
)}
</div>
<div
className={`sa-drawer__body${
activeTab === 'keys' ? ' sa-drawer__body--keys' : ''
}`}
>
{activeTab === 'overview' && account && (
<OverviewTab
account={account}
localName={localName}
onNameChange={setLocalName}
localRoles={localRoles}
onRolesChange={setLocalRoles}
isDisabled={isDisabled}
availableRoles={availableRoles}
rolesLoading={rolesLoading}
rolesError={rolesError}
rolesErrorObj={rolesErrorObj}
onRefetchRoles={refetchRoles}
/>
)}
{activeTab === 'keys' && account && (
<KeysTab
accountId={account.id}
keys={keys}
isLoading={keysLoading}
isDisabled={isDisabled}
currentPage={keysPage}
pageSize={PAGE_SIZE}
onRefetch={refetchKeys}
onAddKeyClick={(): void => setIsAddKeyOpen(true)}
/>
)}
</div>
<div className="sa-drawer__footer">
{activeTab === 'keys' ? (
<Pagination
current={keysPage}
pageSize={PAGE_SIZE}
total={keys.length}
showTotal={(total: number, range: number[]): JSX.Element => (
<>
<span className="sa-drawer__pagination-range">
{range[0]} &#8212; {range[1]}
</span>
<span className="sa-drawer__pagination-total"> of {total}</span>
</>
)}
showSizeChanger={false}
hideOnSinglePage
onChange={(page): void => setKeysPage(page)}
className="sa-drawer__keys-pagination"
/>
) : (
<>
{isDisabled ? (
<Button
variant="ghost"
color="primary"
className="sa-drawer__footer-btn"
onClick={(): void => setIsActivateConfirmOpen(true)}
>
<Check size={12} />
Activate Service Account
</Button>
) : (
<Button
variant="ghost"
color="destructive"
className="sa-drawer__footer-btn"
onClick={(): void => setIsDisableConfirmOpen(true)}
>
<PowerOff size={12} />
Disable Service Account
</Button>
)}
{!isDisabled && (
<div className="sa-drawer__footer-right">
<Button
variant="solid"
color="secondary"
size="sm"
onClick={handleClose}
>
<X size={14} />
Cancel
</Button>
<Button
variant="solid"
color="primary"
size="sm"
disabled={!isDirty || isSaving}
onClick={handleSave}
>
{isSaving ? 'Saving...' : 'Save Changes'}
</Button>
</div>
)}
</>
)}
</div>
</div>
);
return (
<>
<DrawerWrapper
open={open}
onOpenChange={(isOpen): void => {
if (!isOpen) {
handleClose();
}
}}
direction="right"
type="panel"
showCloseButton
showOverlay={false}
allowOutsideClick
header={{ title: 'Service Account Details' }}
content={drawerContent}
className="sa-drawer"
/>
{/* Activate confirm dialog */}
<DialogWrapper
open={isActivateConfirmOpen}
onOpenChange={(isOpen): void => {
if (!isOpen) {
setIsActivateConfirmOpen(false);
}
}}
title={`Activate service account ${account?.name ?? ''}?`}
width="narrow"
className="alert-dialog sa-activate-dialog"
showCloseButton={false}
disableOutsideClick={false}
>
<p className="sa-activate-dialog__body">
Reactivating this service account will restore access for all its keys and
any systems using them.
</p>
<DialogFooter className="sa-activate-dialog__footer">
<Button
variant="solid"
color="secondary"
size="sm"
onClick={(): void => setIsActivateConfirmOpen(false)}
>
<X size={12} />
Cancel
</Button>
<Button
variant="solid"
color="primary"
size="sm"
disabled={isActivating}
onClick={handleActivate}
>
<Check size={12} />
{isActivating ? 'Activating...' : 'Activate'}
</Button>
</DialogFooter>
</DialogWrapper>
{/* Disable confirm dialog */}
<DialogWrapper
open={isDisableConfirmOpen}
onOpenChange={(isOpen): void => {
if (!isOpen) {
setIsDisableConfirmOpen(false);
}
}}
title={`Disable service account ${account?.name ?? ''}?`}
width="narrow"
className="alert-dialog sa-disable-dialog"
showCloseButton={false}
disableOutsideClick={false}
>
<p className="sa-disable-dialog__body">
Disabling this service account will revoke access for all its keys. Any
systems using this account will lose access immediately.
</p>
<DialogFooter className="sa-disable-dialog__footer">
<Button
variant="solid"
color="secondary"
size="sm"
onClick={(): void => setIsDisableConfirmOpen(false)}
>
<X size={12} />
Cancel
</Button>
<Button
variant="solid"
color="destructive"
size="sm"
disabled={isDisabling}
onClick={handleDisable}
>
<Trash2 size={12} />
{isDisabling ? 'Disabling...' : 'Disable'}
</Button>
</DialogFooter>
</DialogWrapper>
{account && (
<AddKeyModal
open={isAddKeyOpen}
accountId={account.id}
onClose={(): void => setIsAddKeyOpen(false)}
onSuccess={handleKeySuccess}
/>
)}
</>
);
}
export default ServiceAccountDrawer;

View File

@@ -0,0 +1,25 @@
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import type { Dayjs } from 'dayjs';
import dayjs from 'dayjs';
export function formatLastUsed(
lastUsed: string | Date | null | undefined,
formatTimezoneAdjustedTimestamp: (ts: string, format: string) => string,
): string {
if (!lastUsed) {
return '—';
}
const str = typeof lastUsed === 'string' ? lastUsed : lastUsed.toISOString();
// Go zero time means the key has never been used
if (str.startsWith('0001-01-01')) {
return '—';
}
const d = new Date(str);
if (Number.isNaN(d.getTime())) {
return '—';
}
return formatTimezoneAdjustedTimestamp(str, DATE_TIME_FORMATS.DASH_DATETIME);
}
export const disabledDate = (current: Dayjs): boolean =>
!!current && current < dayjs().endOf('day');

View File

@@ -0,0 +1,218 @@
.sa-table-wrapper {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
overflow: hidden;
border-radius: 4px;
}
.sa-table {
.ant-table {
background: transparent;
font-size: 13px;
}
.ant-table-container {
border-radius: 0 !important;
border: none !important;
}
.ant-table-thead {
> tr > th,
> tr > td {
background: var(--background);
font-size: var(--paragraph-small-600-font-size);
font-weight: var(--paragraph-small-600-font-weight);
line-height: var(--paragraph-small-600-line-height);
letter-spacing: 0.44px;
text-transform: uppercase;
color: var(--foreground);
padding: var(--padding-2) var(--padding-4);
border-bottom: none !important;
border-top: none !important;
&::before {
display: none !important;
}
}
}
.ant-table-tbody {
> tr > td {
border-bottom: none !important;
padding: var(--padding-2) var(--padding-4);
background: transparent;
transition: none;
}
> tr.sa-table-row--tinted > td {
background: rgba(171, 189, 255, 0.02);
}
> tr:hover > td {
background: rgba(171, 189, 255, 0.04) !important;
}
}
.ant-table-wrapper,
.ant-table-container,
.ant-spin-nested-loading,
.ant-spin-container {
border: none !important;
box-shadow: none !important;
}
.sa-name-column {
.ant-table-column-sorters {
justify-content: flex-start;
gap: var(--spacing-2);
}
.ant-table-column-title {
flex: none;
}
}
.sa-status-cell {
[data-slot='badge'] {
padding: var(--padding-1) var(--padding-2);
align-items: center;
font-size: var(--uppercase-small-500-font-size);
font-weight: var(--uppercase-small-500-font-weight);
line-height: 100%;
letter-spacing: 0.44px;
text-transform: uppercase;
}
}
}
.sa-name-email-cell {
display: flex;
align-items: center;
gap: var(--spacing-2);
height: 22px;
overflow: hidden;
.sa-name {
font-size: var(--paragraph-base-500-font-size);
font-weight: var(--paragraph-base-500-font-weight);
color: var(--foreground);
line-height: var(--paragraph-base-500-line-height);
letter-spacing: -0.07px;
white-space: nowrap;
flex-shrink: 0;
}
.sa-email {
font-size: var(--paragraph-base-400-font-size);
font-weight: var(--paragraph-base-400-font-weight);
color: var(--l3-foreground-hover);
line-height: var(--paragraph-base-400-line-height);
letter-spacing: -0.07px;
flex: 1 0 0;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.sa-roles-cell {
display: flex;
align-items: center;
gap: var(--spacing-2);
}
.sa-dash {
font-size: var(--paragraph-base-400-font-size);
color: var(--l3-foreground-hover);
}
.sa-empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: var(--padding-12) var(--padding-4);
gap: var(--spacing-4);
color: var(--foreground);
&__emoji {
font-size: var(--font-size-2xl);
line-height: 1;
}
&__text {
font-size: var(--paragraph-base-400-font-size);
font-weight: var(--paragraph-base-400-font-weight);
color: var(--foreground);
margin: 0;
line-height: var(--paragraph-base-400-font-height);
strong {
font-weight: var(--font-weight-medium);
color: var(--bg-base-white);
}
}
}
.sa-table-pagination {
display: flex;
align-items: center;
justify-content: flex-end;
padding: var(--padding-2) var(--padding-4);
.ant-pagination-total-text {
margin-right: auto;
}
.sa-pagination-range {
font-size: var(--font-size-xs);
color: var(--foreground);
}
.sa-pagination-total {
font-size: var(--font-size-xs);
color: var(--foreground);
opacity: 0.5;
}
}
.sa-tooltip {
.ant-tooltip-inner {
background-color: var(--bg-slate-500);
color: var(--foreground);
font-size: var(--font-size-xs);
line-height: normal;
padding: var(--padding-2) var(--padding-3);
border-radius: 4px;
text-align: left;
}
.ant-tooltip-arrow-content {
background-color: var(--bg-slate-500);
}
}
.lightMode {
.sa-table {
.ant-table-tbody {
> tr.sa-table-row--tinted > td {
background: rgba(0, 0, 0, 0.015);
}
> tr:hover > td {
background: rgba(0, 0, 0, 0.03) !important;
}
}
}
.sa-empty-state {
&__text {
strong {
color: var(--bg-base-black);
}
}
}
}

View File

@@ -0,0 +1,192 @@
import React from 'react';
import { Badge } from '@signozhq/badge';
import { Pagination, Table, Tooltip } from 'antd';
import type { ColumnsType } from 'antd/es/table/interface';
import { ServiceAccountRow } from 'container/ServiceAccountsSettings/utils';
import './ServiceAccountsTable.styles.scss';
interface ServiceAccountsTableProps {
data: ServiceAccountRow[];
loading: boolean;
total: number;
currentPage: number;
pageSize: number;
searchQuery: string;
onPageChange: (page: number) => void;
onRowClick?: (row: ServiceAccountRow) => void;
}
function NameEmailCell({
name,
email,
}: {
name: string;
email: string;
}): JSX.Element {
return (
<div className="sa-name-email-cell">
{name && (
<span className="sa-name" title={name}>
{name}
</span>
)}
<Tooltip title={email} overlayClassName="sa-tooltip">
<span className="sa-email">{email}</span>
</Tooltip>
</div>
);
}
function RolesCell({ roles }: { roles: string[] }): JSX.Element {
if (!roles || roles.length === 0) {
return <span className="sa-dash"></span>;
}
const first = roles[0];
const overflow = roles.length - 1;
const tooltipContent = roles.slice(1).join(', ');
return (
<div className="sa-roles-cell">
<Badge color="vanilla">{first}</Badge>
{overflow > 0 && (
<Tooltip
title={tooltipContent}
overlayClassName="sa-tooltip"
overlayStyle={{ maxWidth: '600px' }}
>
<Badge color="vanilla" variant="outline" className="sa-status-badge">
+{overflow}
</Badge>
</Tooltip>
)}
</div>
);
}
function StatusBadge({ status }: { status: string }): JSX.Element {
if (status?.toUpperCase() === 'ACTIVE') {
return (
<Badge color="forest" variant="outline">
ACTIVE
</Badge>
);
}
return (
<Badge color="vanilla" variant="outline" className="sa-status-badge">
DISABLED
</Badge>
);
}
function ServiceAccountsEmptyState({
searchQuery,
}: {
searchQuery: string;
}): JSX.Element {
return (
<div className="sa-empty-state">
<span className="sa-empty-state__emoji" role="img" aria-label="monocle face">
🧐
</span>
{searchQuery ? (
<p className="sa-empty-state__text">
No results for <strong>{searchQuery}</strong>
</p>
) : (
<p className="sa-empty-state__text">
No service accounts. Start by creating one to manage keys.
</p>
)}
</div>
);
}
function ServiceAccountsTable({
data,
loading,
total,
currentPage,
pageSize,
searchQuery,
onPageChange,
onRowClick,
}: ServiceAccountsTableProps): JSX.Element {
const columns: ColumnsType<ServiceAccountRow> = [
{
title: 'Name / Email',
dataIndex: 'name',
key: 'name',
className: 'sa-name-column',
sorter: (a, b): number => a.email.localeCompare(b.email),
render: (_, record): JSX.Element => (
<NameEmailCell name={record.name} email={record.email} />
),
},
{
title: 'Roles',
dataIndex: 'roles',
key: 'roles',
width: 420,
render: (roles: string[]): JSX.Element => <RolesCell roles={roles} />,
},
{
title: 'Status',
dataIndex: 'status',
key: 'status',
width: 120,
align: 'right' as const,
className: 'sa-status-cell',
sorter: (a, b): number =>
(a.status?.toUpperCase() === 'ACTIVE' ? 0 : 1) -
(b.status?.toUpperCase() === 'ACTIVE' ? 0 : 1),
render: (status: string): JSX.Element => <StatusBadge status={status} />,
},
];
const showPaginationTotal = (_total: number, range: number[]): JSX.Element => (
<>
<span className="sa-pagination-range">
{range[0]} &#8212; {range[1]}
</span>
<span className="sa-pagination-total"> of {_total}</span>
</>
);
return (
<div className="sa-table-wrapper">
<Table<ServiceAccountRow>
columns={columns}
dataSource={data}
rowKey="id"
loading={loading}
pagination={false}
rowClassName={(_, index): string =>
index % 2 === 0 ? 'sa-table-row--tinted' : ''
}
showSorterTooltip={false}
locale={{
emptyText: <ServiceAccountsEmptyState searchQuery={searchQuery} />,
}}
className="sa-table"
onRow={(record): { onClick: () => void; style?: React.CSSProperties } => ({
onClick: (): void => onRowClick?.(record),
style: onRowClick ? { cursor: 'pointer' } : undefined,
})}
/>
{total > pageSize && (
<Pagination
current={currentPage}
pageSize={pageSize}
total={total}
showTotal={showPaginationTotal}
showSizeChanger={false}
onChange={onPageChange}
className="sa-table-pagination"
/>
)}
</div>
);
}
export default ServiceAccountsTable;

View File

@@ -55,5 +55,4 @@ export enum QueryParams {
source = 'source',
showClassicCreateAlertsPage = 'showClassicCreateAlertsPage',
isTestAlert = 'isTestAlert',
yAxisUnit = 'yAxisUnit',
}

View File

@@ -86,6 +86,7 @@ const ROUTES = {
METER_EXPLORER_VIEWS: '/meter/explorer/views',
HOME_PAGE: '/',
PUBLIC_DASHBOARD: '/public/dashboard/:dashboardId',
SERVICE_ACCOUNTS_SETTINGS: '/settings/service-accounts',
} as const;
export default ROUTES;

View File

@@ -16,8 +16,8 @@ export const PagerInitialConfig: Partial<PagerChannel> = {
client: 'SigNoz Alert Manager',
client_url: 'https://enter-signoz-host-n-port-here/alerts',
details: JSON.stringify({
firing: `{{ .Alerts.Firing | toJson }}`,
resolved: `{{ .Alerts.Resolved | toJson }}`,
firing: `{{ template "pagerduty.default.instances" .Alerts.Firing }}`,
resolved: `{{ template "pagerduty.default.instances" .Alerts.Resolved }}`,
num_firing: '{{ .Alerts.Firing | len }}',
num_resolved: '{{ .Alerts.Resolved | len }}',
}),

View File

@@ -193,6 +193,7 @@ describe('Dashboard landing page actions header tests', () => {
handleDashboardLockToggle: jest.fn(),
dashboardResponse: {} as IDashboardContext['dashboardResponse'],
selectedDashboard: (getDashboardById.data as unknown) as Dashboard,
dashboardId: '4',
layouts: [],
panelMap: {},
setPanelMap: jest.fn(),
@@ -204,6 +205,8 @@ describe('Dashboard landing page actions header tests', () => {
updateLocalStorageDashboardVariables: jest.fn(),
dashboardQueryRangeCalled: false,
setDashboardQueryRangeCalled: jest.fn(),
selectedRowWidgetId: null,
setSelectedRowWidgetId: jest.fn(),
isDashboardFetching: false,
columnWidths: {},
setColumnWidths: jest.fn(),

View File

@@ -78,6 +78,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
isDashboardLocked,
setSelectedDashboard,
handleToggleDashboardSlider,
setSelectedRowWidgetId,
handleDashboardLockToggle,
} = useDashboard();
@@ -145,6 +146,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
const [addPanelPermission] = useComponentPermission(permissions, userRole);
const onEmptyWidgetHandler = useCallback(() => {
setSelectedRowWidgetId(null);
handleToggleDashboardSlider(true);
logEvent('Dashboard Detail: Add new panel clicked', {
dashboardId: selectedDashboard?.id,

View File

@@ -67,18 +67,17 @@ export const useDashboardVariableUpdate = (): UseDashboardVariableUpdateReturn =
const oldVariables = prev?.data.variables;
// this is added to handle case where we have two different
// schemas for variable response
const updatedVariables = { ...oldVariables };
if (updatedVariables?.[id]) {
updatedVariables[id] = {
...updatedVariables[id],
if (oldVariables?.[id]) {
oldVariables[id] = {
...oldVariables[id],
selectedValue: value,
allSelected,
haveCustomValuesSelected,
};
}
if (updatedVariables?.[name]) {
updatedVariables[name] = {
...updatedVariables[name],
if (oldVariables?.[name]) {
oldVariables[name] = {
...oldVariables[name],
selectedValue: value,
allSelected,
haveCustomValuesSelected,
@@ -88,7 +87,9 @@ export const useDashboardVariableUpdate = (): UseDashboardVariableUpdateReturn =
...prev,
data: {
...prev?.data,
variables: updatedVariables,
variables: {
...oldVariables,
},
},
};
}

View File

@@ -6,6 +6,7 @@ import { useResizeObserver } from 'hooks/useDimensions';
import { LegendPosition } from 'lib/uPlotV2/components/types';
import ContextMenu from 'periscope/components/ContextMenu';
import { useTimezone } from 'providers/Timezone';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import uPlot from 'uplot';
import { getTimeRange } from 'utils/getTimeRange';
@@ -61,7 +62,7 @@ function BarPanel(props: PanelWrapperProps): JSX.Element {
currentQuery: widget.query,
onClick: clickHandlerWithContextMenu,
onDragSelect,
apiResponse: queryResponse?.data?.payload,
apiResponse: queryResponse?.data?.payload as MetricRangePayloadProps,
timezone,
panelMode,
minTimeScale: minTimeScale,

View File

@@ -11,6 +11,7 @@ import { get } from 'lodash-es';
import { Widgets } from 'types/api/dashboard/getAll';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { QueryData } from 'types/api/widgets/getQuery';
import { AlignedData } from 'uplot';
import { PanelMode } from '../types';
@@ -43,7 +44,7 @@ export function prepareBarPanelConfig({
currentQuery: Query;
onClick: OnClickPluginOpts['onClick'];
onDragSelect: (startTime: number, endTime: number) => void;
apiResponse?: MetricRangePayloadProps;
apiResponse: MetricRangePayloadProps;
timezone: Timezone;
panelMode: PanelMode;
minTimeScale?: number;
@@ -75,17 +76,13 @@ export function prepareBarPanelConfig({
stepInterval: minStepInterval,
});
if (!(apiResponse && apiResponse?.data?.result)) {
// if no data, return the builder without adding any series
return builder;
}
if (widget.stackedBarChart) {
const seriesCount = (apiResponse.data.result.length ?? 0) + 1; // +1 for 1-based uPlot series indices
const seriesCount = (apiResponse?.data?.result?.length ?? 0) + 1; // +1 for 1-based uPlot series indices
builder.setBands(getInitialStackedBands(seriesCount));
}
apiResponse.data.result.forEach((series) => {
const seriesList: QueryData[] = apiResponse?.data?.result || [];
seriesList.forEach((series) => {
const baseLabelName = getLabelName(
series.metric,
series.queryName || '', // query

View File

@@ -6,6 +6,7 @@ import { useResizeObserver } from 'hooks/useDimensions';
import { LegendPosition } from 'lib/uPlotV2/components/types';
import { DashboardCursorSync } from 'lib/uPlotV2/plugins/TooltipPlugin/types';
import { useTimezone } from 'providers/Timezone';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import uPlot from 'uplot';
import Histogram from '../../charts/Histogram/Histogram';
@@ -38,7 +39,7 @@ function HistogramPanel(props: PanelWrapperProps): JSX.Element {
return prepareHistogramPanelConfig({
widget,
isDarkMode,
apiResponse: queryResponse?.data?.payload,
apiResponse: queryResponse?.data?.payload as MetricRangePayloadProps,
panelMode,
});
}, [widget, isDarkMode, queryResponse?.data?.payload, panelMode]);
@@ -48,7 +49,7 @@ function HistogramPanel(props: PanelWrapperProps): JSX.Element {
return [];
}
return prepareHistogramPanelData({
apiResponse: queryResponse?.data?.payload,
apiResponse: queryResponse?.data?.payload as MetricRangePayloadProps,
bucketWidth: widget?.bucketWidth,
bucketCount: widget?.bucketCount,
mergeAllActiveQueries: widget?.mergeAllActiveQueries,

View File

@@ -149,7 +149,7 @@ export function prepareHistogramPanelConfig({
isDarkMode,
}: {
widget: Widgets;
apiResponse?: MetricRangePayloadProps;
apiResponse: MetricRangePayloadProps;
panelMode: PanelMode;
isDarkMode: boolean;
}): UPlotConfigBuilder {
@@ -204,7 +204,7 @@ export function prepareHistogramPanelConfig({
fillColor: '#4E74F8',
isDarkMode,
});
} else if (apiResponse && apiResponse?.data?.result) {
} else {
apiResponse.data.result.forEach((series) => {
const baseLabelName = getLabelName(
series.metric,

View File

@@ -9,6 +9,7 @@ import { useResizeObserver } from 'hooks/useDimensions';
import { LegendPosition } from 'lib/uPlotV2/components/types';
import { ContextMenu } from 'periscope/components/ContextMenu';
import { useTimezone } from 'providers/Timezone';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import uPlot from 'uplot';
import { getTimeRange } from 'utils/getTimeRange';
@@ -67,7 +68,7 @@ function TimeSeriesPanel(props: PanelWrapperProps): JSX.Element {
currentQuery: widget.query,
onClick: clickHandlerWithContextMenu,
onDragSelect,
apiResponse: queryResponse?.data?.payload,
apiResponse: queryResponse?.data?.payload as MetricRangePayloadProps,
timezone,
panelMode,
minTimeScale: minTimeScale,

View File

@@ -68,12 +68,11 @@ export const prepareUPlotConfig = ({
currentQuery: Query;
onClick?: OnClickPluginOpts['onClick'];
onDragSelect: (startTime: number, endTime: number) => void;
apiResponse?: MetricRangePayloadProps;
apiResponse: MetricRangePayloadProps;
timezone: Timezone;
panelMode: PanelMode;
minTimeScale?: number;
maxTimeScale?: number;
// eslint-disable-next-line sonarjs/cognitive-complexity
}): UPlotConfigBuilder => {
const stepIntervals: ExecStats['stepIntervals'] = get(
apiResponse,
@@ -101,12 +100,7 @@ export const prepareUPlotConfig = ({
stepInterval: minStepInterval,
});
if (!(apiResponse && apiResponse.data.result)) {
// if no data, return the builder without adding any series
return builder;
}
apiResponse.data.result.forEach((series) => {
apiResponse.data?.result?.forEach((series) => {
const hasSingleValidPoint = hasSingleVisiblePointForSeries(series);
const baseLabelName = getLabelName(
series.metric,

View File

@@ -18,7 +18,7 @@ import { PanelMode } from '../types';
export interface BaseConfigBuilderProps {
id: string;
thresholds?: ThresholdProps[];
apiResponse?: MetricRangePayloadProps;
apiResponse: MetricRangePayloadProps;
isDarkMode: boolean;
onClick?: OnClickPluginOpts['onClick'];
onDragSelect?: (startTime: number, endTime: number) => void;

View File

@@ -19,6 +19,7 @@ export default function DashboardEmptyState(): JSX.Element {
selectedDashboard,
isDashboardLocked,
handleToggleDashboardSlider,
setSelectedRowWidgetId,
} = useDashboard();
const variablesSettingsTabHandle = useRef<VariablesSettingsTab>(null);
@@ -41,6 +42,7 @@ export default function DashboardEmptyState(): JSX.Element {
const [addPanelPermission] = useComponentPermission(permissions, userRole);
const onEmptyWidgetHandler = useCallback(() => {
setSelectedRowWidgetId(null);
handleToggleDashboardSlider(true);
logEvent('Dashboard Detail: Add new panel clicked', {
dashboardId: selectedDashboard?.id,

View File

@@ -71,6 +71,7 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
isDashboardLocked,
dashboardQueryRangeCalled,
setDashboardQueryRangeCalled,
setSelectedRowWidgetId,
isDashboardFetching,
columnWidths,
} = useDashboard();
@@ -194,6 +195,7 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
updateDashboardMutation.mutate(updatedDashboard, {
onSuccess: (updatedDashboard) => {
setSelectedRowWidgetId(null);
if (updatedDashboard.data) {
if (updatedDashboard.data.data.layout) {
setLayouts(sortLayout(updatedDashboard.data.data.layout));

View File

@@ -5,7 +5,6 @@ import useComponentPermission from 'hooks/useComponentPermission';
import { EllipsisIcon, PenLine, Plus, X } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { setSelectedRowWidgetId } from 'providers/Dashboard/helpers/selectedRowWidgetIdHelper';
import { ROLES, USER_ROLES } from 'types/roles';
import { ComponentTypes } from 'utils/permission';
@@ -38,6 +37,7 @@ export function WidgetRowHeader(props: WidgetRowHeaderProps): JSX.Element {
handleToggleDashboardSlider,
selectedDashboard,
isDashboardLocked,
setSelectedRowWidgetId,
} = useDashboard();
const permissions: ComponentTypes[] = ['add_panel'];
@@ -81,12 +81,7 @@ export function WidgetRowHeader(props: WidgetRowHeaderProps): JSX.Element {
disabled={!editWidget && addPanelPermission && !isDashboardLocked}
icon={<Plus size={14} />}
onClick={(): void => {
// TODO: @AshwinBhatkal Simplify this check in cleanup of https://github.com/SigNoz/engineering-pod/issues/3953
if (!selectedDashboard?.id) {
return;
}
setSelectedRowWidgetId(selectedDashboard.id, id);
setSelectedRowWidgetId(id);
handleToggleDashboardSlider(true);
}}
>

View File

@@ -39,7 +39,6 @@ import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQuery
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQueryData from 'hooks/useUrlQueryData';
import useUrlYAxisUnit from 'hooks/useUrlYAxisUnit';
import { isEmpty, isUndefined } from 'lodash-es';
import LiveLogs from 'pages/LiveLogs';
import { UpdateTimeInterval } from 'store/actions';
@@ -60,7 +59,6 @@ import LogsActionsContainer from './LogsActionsContainer';
import './LogsExplorerViews.styles.scss';
// eslint-disable-next-line sonarjs/cognitive-complexity
function LogsExplorerViewsContainer({
setIsLoadingQueries,
listQueryKeyRef,
@@ -111,7 +109,7 @@ function LogsExplorerViewsContainer({
const [orderBy, setOrderBy] = useState<string>('timestamp:desc');
const { yAxisUnit, onUnitChange } = useUrlYAxisUnit('');
const [yAxisUnit, setYAxisUnit] = useState<string>('');
const listQuery = useMemo(() => getListQuery(stagedQuery) || null, [
stagedQuery,
@@ -369,6 +367,10 @@ function LogsExplorerViewsContainer({
orderBy,
]);
const onUnitChangeHandler = useCallback((value: string): void => {
setYAxisUnit(value);
}, []);
const chartData = useMemo(() => {
if (!stagedQuery) {
return [];
@@ -486,7 +488,10 @@ function LogsExplorerViewsContainer({
{selectedPanelType === PANEL_TYPES.TIME_SERIES && !showLiveLogs && (
<div className="time-series-view-container">
<div className="time-series-view-container-header">
<BuilderUnitsFilter onChange={onUnitChange} yAxisUnit={yAxisUnit} />
<BuilderUnitsFilter
onChange={onUnitChangeHandler}
yAxisUnit={yAxisUnit}
/>
</div>
<TimeSeriesView
isLoading={isLoading || isFetching}

View File

@@ -119,7 +119,7 @@ function MembersSettings(): JSX.Element {
return;
}
const maxPage = Math.ceil(filteredMembers.length / PAGE_SIZE);
if (currentPage > maxPage) {
if (currentPage > maxPage || currentPage < 1) {
setPage(maxPage);
}
}, [filteredMembers.length, currentPage, setPage]);
@@ -209,6 +209,7 @@ function MembersSettings(): JSX.Element {
<div className="members-settings__search">
<Input
type="search"
placeholder="Search by name, email, or role..."
value={searchQuery}
onChange={(e): void => {
@@ -217,6 +218,7 @@ function MembersSettings(): JSX.Element {
}}
className="members-search-input"
color="secondary"
name="members-search"
/>
</div>

View File

@@ -1,4 +1,4 @@
import { useMemo } from 'react';
import { useMemo, useState } from 'react';
import { useQueries } from 'react-query';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
@@ -11,7 +11,6 @@ import { BuilderUnitsFilter } from 'container/QueryBuilder/filters/BuilderUnitsF
import TimeSeriesView from 'container/TimeSeriesView/TimeSeriesView';
import { convertDataValueToMs } from 'container/TimeSeriesView/utils';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import useUrlYAxisUnit from 'hooks/useUrlYAxisUnit';
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
import { useErrorModal } from 'providers/ErrorModalProvider';
import { AppState } from 'store/reducers';
@@ -23,13 +22,14 @@ import { GlobalReducer } from 'types/reducer/globalTime';
function TimeSeries(): JSX.Element {
const { stagedQuery, currentQuery } = useQueryBuilder();
const { yAxisUnit, onUnitChange } = useUrlYAxisUnit('');
const { selectedTime: globalSelectedTime, maxTime, minTime } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
const [yAxisUnit, setYAxisUnit] = useState<string>('');
const isValidToConvertToMs = useMemo(() => {
const isValid: boolean[] = [];
@@ -112,6 +112,10 @@ function TimeSeries(): JSX.Element {
[data, isValidToConvertToMs],
);
const onUnitChangeHandler = (value: string): void => {
setYAxisUnit(value);
};
const hasMetricSelected = useMemo(
() => currentQuery.builder.queryData.some((q) => q.aggregateAttribute?.key),
[currentQuery],
@@ -119,7 +123,7 @@ function TimeSeries(): JSX.Element {
return (
<div className="meter-time-series-container">
<BuilderUnitsFilter onChange={onUnitChange} yAxisUnit={yAxisUnit} />
<BuilderUnitsFilter onChange={onUnitChangeHandler} yAxisUnit={yAxisUnit} />
<div className="time-series-container">
{!hasMetricSelected && <EmptyMetricsSearch />}
{hasMetricSelected &&

View File

@@ -34,10 +34,6 @@ import { cloneDeep, defaultTo, isEmpty, isUndefined } from 'lodash-es';
import { Check, X } from 'lucide-react';
import { DashboardWidgetPageParams } from 'pages/DashboardWidget';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import {
clearSelectedRowWidgetId,
getSelectedRowWidgetId,
} from 'providers/Dashboard/helpers/selectedRowWidgetIdHelper';
import {
getNextWidgets,
getPreviousWidgets,
@@ -90,6 +86,8 @@ function NewWidget({
selectedDashboard,
setSelectedDashboard,
setToScrollWidgetId,
selectedRowWidgetId,
setSelectedRowWidgetId,
columnWidths,
} = useDashboard();
@@ -452,8 +450,6 @@ function NewWidget({
const widgetId = query.get('widgetId') || '';
let updatedLayout = selectedDashboard.data.layout || [];
const selectedRowWidgetId = getSelectedRowWidgetId(dashboardId);
if (isNewDashboard && isEmpty(selectedRowWidgetId)) {
const newLayoutItem = placeWidgetAtBottom(widgetId, updatedLayout);
updatedLayout = [...updatedLayout, newLayoutItem];
@@ -558,6 +554,7 @@ function NewWidget({
updateDashboardMutation.mutateAsync(dashboard, {
onSuccess: (updatedDashboard) => {
setSelectedRowWidgetId(null);
setSelectedDashboard(updatedDashboard.data);
setToScrollWidgetId(selectedWidget?.id || '');
safeNavigate({
@@ -569,6 +566,7 @@ function NewWidget({
selectedDashboard,
query,
isNewDashboard,
selectedRowWidgetId,
afterWidgets,
selectedWidget,
selectedTime.enum,
@@ -579,6 +577,7 @@ function NewWidget({
widgets,
setSelectedDashboard,
setToScrollWidgetId,
setSelectedRowWidgetId,
safeNavigate,
dashboardId,
]);
@@ -682,10 +681,6 @@ function NewWidget({
* on mount here with the currentQuery in the begining itself
*/
setSupersetQuery(currentQuery);
return (): void => {
clearSelectedRowWidgetId(dashboardId);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

View File

@@ -619,11 +619,6 @@ function OnboardingAddDataSource(): JSX.Element {
);
};
const progressText = `Get Started (${Math.min(
currentStep + 1,
setupStepItems.length,
)}/${setupStepItems.length})`;
return (
<div className="onboarding-v2">
<Layout>
@@ -644,7 +639,7 @@ function OnboardingAddDataSource(): JSX.Element {
history.push(ROUTES.HOME);
}}
/>
<Typography.Text>{progressText}</Typography.Text>
<Typography.Text>Get Started (2/4)</Typography.Text>
</div>
<div className="header-right-section">

View File

@@ -611,34 +611,6 @@
]
}
},
{
"dataSource": "deno",
"label": "Deno",
"imgUrl": "/Logos/deno.svg",
"tags": [
"apm/traces",
"logs",
"metrics"
],
"module": "apm",
"relatedSearchKeywords": [
"deno",
"deno monitoring",
"deno observability",
"javascript",
"js",
"ts",
"typescript",
"opentelemetry deno",
"observability",
"metrics",
"logs",
"traces",
"tracing"
],
"id": "deno",
"link": "/docs/instrumentation/opentelemetry-deno/"
},
{
"dataSource": "javascript",
"label": "JavaScript",
@@ -1856,33 +1828,15 @@
],
"module": "logs",
"relatedSearchKeywords": [
"apache",
"application log file collection",
"c#",
"c++",
"collect",
"collect logs from file",
"collect logs from log file",
"cpp",
"csharp",
"custom log file monitoring",
"deno",
"django",
"dotnet",
"elixir",
"express",
"file",
"file based logging",
"filebeat alternative",
"flask",
"from-log-file",
"go",
"golang",
"java",
"javascript",
"js",
"kotlin",
"linux",
"log",
"log file ingestion",
"log file to signoz",
@@ -1890,29 +1844,7 @@
"log tailing with otel",
"logging",
"logs",
"macos",
"mongodb",
"mysql",
"nestjs",
"nextjs",
"nginx",
"otel file logs",
"php",
"postgres",
"postgresql",
"python",
"rails",
"react",
"redis",
"ruby",
"rust",
"scala",
"spring",
"swift",
"ts",
"typescript",
"vue",
"windows"
"otel file logs"
],
"link": "/docs/userguide/collect_logs_from_file/"
},
@@ -5276,7 +5208,7 @@
{
"dataSource": "prometheus-metrics",
"label": "Prometheus Metrics",
"imgUrl": "/Logos/prometheus.svg",
"imgUrl": "/Logos/other-metrics.svg",
"tags": [
"metrics",
"infrastructure monitoring"
@@ -5877,250 +5809,5 @@
}
]
}
},
{
"dataSource": "rust-metrics",
"label": "Rust Metrics",
"imgUrl": "/Logos/rust.svg",
"tags": [
"metrics"
],
"module": "metrics",
"relatedSearchKeywords": [
"metrics",
"opentelemetry rust",
"otel rust",
"rust",
"rust metrics",
"rust monitoring",
"rust observability"
],
"id": "rust-metrics",
"link": "/docs/metrics-management/send-metrics/applications/opentelemetry-rust/",
"question": {
"desc": "What is your Environment?",
"type": "select",
"entityID": "environment",
"options": [
{
"key": "vm",
"label": "VM",
"imgUrl": "/Logos/vm.svg",
"link": "/docs/metrics-management/send-metrics/applications/opentelemetry-rust/"
},
{
"key": "k8s",
"label": "Kubernetes",
"imgUrl": "/Logos/kubernetes.svg",
"link": "/docs/metrics-management/send-metrics/applications/opentelemetry-rust/"
},
{
"key": "windows",
"label": "Windows",
"imgUrl": "/Logos/windows.svg",
"link": "/docs/metrics-management/send-metrics/applications/opentelemetry-rust/"
},
{
"key": "docker",
"label": "Docker",
"imgUrl": "/Logos/docker.svg",
"link": "/docs/metrics-management/send-metrics/applications/opentelemetry-rust/"
}
]
}
},
{
"dataSource": "ruby-metrics",
"label": "Ruby Metrics",
"imgUrl": "/Logos/ruby-on-rails.svg",
"tags": [
"metrics"
],
"module": "metrics",
"relatedSearchKeywords": [
"metrics",
"opentelemetry ruby",
"otel ruby",
"ruby",
"ruby metrics",
"ruby monitoring",
"ruby observability",
"ruby on rails metrics"
],
"id": "ruby-metrics",
"link": "/docs/metrics-management/send-metrics/applications/opentelemetry-ruby/",
"question": {
"desc": "What is your Environment?",
"type": "select",
"entityID": "environment",
"options": [
{
"key": "vm",
"label": "VM",
"imgUrl": "/Logos/vm.svg",
"link": "/docs/metrics-management/send-metrics/applications/opentelemetry-ruby/"
},
{
"key": "k8s",
"label": "Kubernetes",
"imgUrl": "/Logos/kubernetes.svg",
"link": "/docs/metrics-management/send-metrics/applications/opentelemetry-ruby/"
},
{
"key": "windows",
"label": "Windows",
"imgUrl": "/Logos/windows.svg",
"link": "/docs/metrics-management/send-metrics/applications/opentelemetry-ruby/"
},
{
"key": "docker",
"label": "Docker",
"imgUrl": "/Logos/docker.svg",
"link": "/docs/metrics-management/send-metrics/applications/opentelemetry-ruby/"
}
]
}
},
{
"dataSource": "statsd",
"label": "StatsD",
"imgUrl": "/Logos/statsd.svg",
"tags": [
"metrics"
],
"module": "metrics",
"relatedSearchKeywords": [
"metrics",
"statsd",
"statsd metrics",
"statsd monitoring",
"statsd observability"
],
"id": "statsd",
"link": "/docs/userguide/opentelemetry-statsd/"
},
{
"dataSource": "nvidia-dcgm",
"label": "Nvidia DCGM",
"imgUrl": "/Logos/nvidia-dcgm.svg",
"tags": [
"infrastructure monitoring"
],
"module": "infra-monitoring-hosts",
"relatedSearchKeywords": [
"dcgm",
"gpu monitoring",
"infrastructure monitoring",
"nvidia",
"nvidia dcgm",
"nvidia metrics"
],
"id": "nvidia-dcgm",
"link": "/docs/metrics-management/nvidia-dcgm-metrics/"
},
{
"dataSource": "slurm",
"label": "Slurm",
"imgUrl": "/Logos/slurm.svg",
"tags": [
"infrastructure monitoring"
],
"module": "infra-monitoring-hosts",
"relatedSearchKeywords": [
"infrastructure monitoring",
"slurm",
"slurm cluster monitoring",
"slurm metrics"
],
"id": "slurm",
"link": "/docs/metrics-management/slurm-metrics/"
},
{
"dataSource": "ollama-monitoring",
"label": "Ollama",
"imgUrl": "/Logos/ollama.svg",
"tags": [
"LLM Monitoring"
],
"module": "apm",
"relatedSearchKeywords": [
"llm",
"llm monitoring",
"monitoring",
"observability",
"ollama",
"ollama monitoring",
"otel ollama",
"traces",
"tracing"
],
"id": "ollama-monitoring",
"link": "/docs/ollama-monitoring/"
},
{
"dataSource": "haystack-monitoring",
"label": "Haystack",
"imgUrl": "/Logos/haystack.svg",
"tags": [
"LLM Monitoring"
],
"module": "apm",
"relatedSearchKeywords": [
"haystack",
"haystack monitoring",
"llm",
"llm monitoring",
"monitoring",
"observability",
"otel haystack",
"traces",
"tracing"
],
"id": "haystack-monitoring",
"link": "/docs/haystack-monitoring/"
},
{
"dataSource": "openrouter-observability",
"label": "OpenRouter",
"imgUrl": "/Logos/openrouter.svg",
"tags": [
"LLM Monitoring"
],
"module": "apm",
"relatedSearchKeywords": [
"llm",
"llm monitoring",
"monitoring",
"observability",
"openrouter",
"openrouter monitoring",
"otel openrouter",
"traces",
"tracing"
],
"id": "openrouter-observability",
"link": "/docs/openrouter-observability/"
},
{
"dataSource": "huggingface-observability",
"label": "Hugging Face",
"imgUrl": "/Logos/huggingface.svg",
"tags": [
"LLM Monitoring"
],
"module": "apm",
"relatedSearchKeywords": [
"hugging face monitoring",
"huggingface",
"llm",
"llm monitoring",
"monitoring",
"observability",
"otel huggingface",
"traces",
"tracing"
],
"id": "huggingface-observability",
"link": "/docs/huggingface-observability/"
}
]

View File

@@ -0,0 +1,134 @@
.sa-settings {
display: flex;
flex-direction: column;
gap: var(--spacing-8);
padding: var(--padding-4) var(--padding-2) var(--padding-6) var(--padding-4);
height: 100%;
&__header {
display: flex;
flex-direction: column;
gap: var(--spacing-2);
}
&__title {
font-size: var(--label-large-500-font-size);
font-weight: var(--label-large-500-font-weight);
color: var(--text-base-white);
letter-spacing: -0.09px;
line-height: var(--line-height-normal);
margin: 0;
}
&__subtitle {
font-size: var(--paragraph-base-400-font-size);
font-weight: var(--paragraph-base-400-font-weight);
color: var(--foreground);
letter-spacing: -0.07px;
line-height: var(--paragraph-base-400-line-height);
margin: 0;
}
&__learn-more {
color: var(--primary);
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
&__controls {
display: flex;
align-items: center;
gap: var(--spacing-4);
}
&__search {
flex: 1;
min-width: 0;
}
}
.sa-status-badge {
color: var(--l3-foreground);
border-color: var(--border);
}
.sa-settings-filter-trigger {
display: flex;
align-items: center;
gap: var(--spacing-2);
border: 1px solid var(--border);
border-radius: 2px;
background-color: var(--l2-background);
> span {
color: var(--foreground);
}
&__chevron {
flex-shrink: 0;
color: var(--foreground);
}
}
.sa-settings-filter-dropdown {
.ant-dropdown-menu {
padding: var(--padding-3) 14px;
border-radius: 4px;
border: 1px solid var(--border);
background: var(--l2-background);
backdrop-filter: blur(20px);
}
.ant-dropdown-menu-item {
background: transparent !important;
padding: var(--padding-1) 0 !important;
&:hover {
background: transparent !important;
}
}
}
.sa-settings-filter-option {
display: flex;
align-items: center;
justify-content: space-between;
font-size: var(--paragraph-base-400-font-size);
font-weight: var(--paragraph-base-400-font-weight);
color: var(--foreground);
letter-spacing: 0.14px;
min-width: 170px;
&:hover {
color: var(--card-foreground);
background: transparent;
}
}
.sa-settings-search-input {
height: 32px;
color: var(--l1-foreground);
background-color: var(--l2-background);
border-color: var(--border);
&::placeholder {
color: var(--l3-foreground);
}
}
.lightMode {
.sa-settings {
&__title {
color: var(--text-base-black);
}
}
.sa-settings-filter-option {
&:hover {
color: var(--bg-neutral-light-100);
}
}
}

View File

@@ -0,0 +1,277 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useHistory } from 'react-router-dom';
import { Button } from '@signozhq/button';
import { Check, ChevronDown, Plus } from '@signozhq/icons';
import { Input } from '@signozhq/input';
import type { MenuProps } from 'antd';
import { Dropdown } from 'antd';
import { useListServiceAccounts } from 'api/generated/services/serviceaccount';
import CreateServiceAccountModal from 'components/CreateServiceAccountModal/CreateServiceAccountModal';
import ServiceAccountDrawer from 'components/ServiceAccountDrawer/ServiceAccountDrawer';
import ServiceAccountsTable from 'components/ServiceAccountsTable/ServiceAccountsTable';
import useUrlQuery from 'hooks/useUrlQuery';
import { FilterMode, ServiceAccountRow, ServiceAccountStatus } from './utils';
import './ServiceAccountsSettings.styles.scss';
const PAGE_SIZE = 20;
function ServiceAccountsSettings(): JSX.Element {
const history = useHistory();
const urlQuery = useUrlQuery();
const pageParam = parseInt(urlQuery.get('page') ?? '1', 10);
const currentPage = Number.isNaN(pageParam) || pageParam < 1 ? 1 : pageParam;
const [searchQuery, setSearchQuery] = useState('');
const [filterMode, setFilterMode] = useState<FilterMode>(FilterMode.All);
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const [
selectedAccount,
setSelectedAccount,
] = useState<ServiceAccountRow | null>(null);
const {
data: serviceAccountsData,
isLoading,
refetch,
} = useListServiceAccounts();
const allAccounts = useMemo(
(): ServiceAccountRow[] =>
(serviceAccountsData?.data ?? []).map((sa) => ({
id: sa.id,
name: sa.name,
email: sa.email,
roles: sa.roles,
status: sa.status,
createdAt: sa.createdAt ? String(sa.createdAt) : null,
updatedAt: sa.updatedAt ? String(sa.updatedAt) : null,
})),
[serviceAccountsData],
);
const activeCount = useMemo(
() =>
allAccounts.filter(
(a) => a.status?.toUpperCase() === ServiceAccountStatus.Active,
).length,
[allAccounts],
);
const disabledCount = useMemo(
() =>
allAccounts.filter(
(a) => a.status?.toUpperCase() !== ServiceAccountStatus.Active,
).length,
[allAccounts],
);
const filteredAccounts = useMemo((): ServiceAccountRow[] => {
let result = allAccounts;
if (filterMode === FilterMode.Active) {
result = result.filter(
(a) => a.status?.toUpperCase() === ServiceAccountStatus.Active,
);
} else if (filterMode === FilterMode.Disabled) {
result = result.filter(
(a) => a.status?.toUpperCase() !== ServiceAccountStatus.Active,
);
}
if (searchQuery.trim()) {
const q = searchQuery.toLowerCase();
result = result.filter(
(a) =>
a.name?.toLowerCase().includes(q) ||
a.email?.toLowerCase().includes(q) ||
a.roles?.some((role: string) => role.toLowerCase().includes(q)),
);
}
return result;
}, [allAccounts, filterMode, searchQuery]);
const paginatedAccounts = useMemo((): ServiceAccountRow[] => {
const start = (currentPage - 1) * PAGE_SIZE;
return filteredAccounts.slice(start, start + PAGE_SIZE);
}, [filteredAccounts, currentPage]);
const setPage = useCallback(
(page: number): void => {
urlQuery.set('page', String(page));
history.replace({ search: urlQuery.toString() });
},
[history, urlQuery],
);
useEffect(() => {
if (filteredAccounts.length === 0) {
return;
}
const maxPage = Math.max(1, Math.ceil(filteredAccounts.length / PAGE_SIZE));
if (currentPage > maxPage || currentPage < 1) {
setPage(maxPage);
}
}, [filteredAccounts.length, currentPage, setPage]);
const totalCount = allAccounts.length;
const filterMenuItems: MenuProps['items'] = [
{
key: FilterMode.All,
label: (
<div className="sa-settings-filter-option">
<span>All accounts {totalCount}</span>
{filterMode === FilterMode.All && <Check size={14} />}
</div>
),
onClick: (): void => {
setFilterMode(FilterMode.All);
setPage(1);
},
},
{
key: FilterMode.Active,
label: (
<div className="sa-settings-filter-option">
<span>Active {activeCount}</span>
{filterMode === FilterMode.Active && <Check size={14} />}
</div>
),
onClick: (): void => {
setFilterMode(FilterMode.Active);
setPage(1);
},
},
{
key: FilterMode.Disabled,
label: (
<div className="sa-settings-filter-option">
<span>Disabled {disabledCount}</span>
{filterMode === FilterMode.Disabled && <Check size={14} />}
</div>
),
onClick: (): void => {
setFilterMode(FilterMode.Disabled);
setPage(1);
},
},
];
const filterLabel =
filterMode === FilterMode.Active
? `Active ⎯ ${activeCount}`
: filterMode === FilterMode.Disabled
? `Disabled ⎯ ${disabledCount}`
: `All accounts ⎯ ${totalCount}`;
const handleRowClick = useCallback((row: ServiceAccountRow): void => {
setSelectedAccount(row);
}, []);
const handleDrawerClose = useCallback((): void => {
setSelectedAccount(null);
}, []);
const handleDrawerSuccess = useCallback((): void => {
refetch();
setSelectedAccount(null);
}, [refetch]);
const handleCreateSuccess = useCallback((): void => {
refetch();
}, [refetch]);
return (
<>
<div className="sa-settings">
<div className="sa-settings__header">
<h1 className="sa-settings__title">Service Accounts</h1>
<p className="sa-settings__subtitle">
Service accounts are used for machine-to-machine authentication via API
keys. {/* Todo: to add doc links */}
{/* <a
href="https://signoz.io/docs/service-accounts"
target="_blank"
rel="noopener noreferrer"
className="sa-settings__learn-more"
>
Learn more
</a> */}
</p>
</div>
<div className="sa-settings__controls">
<Dropdown
menu={{ items: filterMenuItems }}
trigger={['click']}
overlayClassName="sa-settings-filter-dropdown"
>
<Button
variant="solid"
size="sm"
color="secondary"
className="sa-settings-filter-trigger"
>
<span>{filterLabel}</span>
<ChevronDown size={12} className="sa-settings-filter-trigger__chevron" />
</Button>
</Dropdown>
<div className="sa-settings__search">
<Input
placeholder="Search by name or email..."
value={searchQuery}
onChange={(e): void => {
setSearchQuery(e.target.value);
setPage(1);
}}
className="sa-settings-search-input"
color="secondary"
/>
</div>
<Button
variant="solid"
size="sm"
color="primary"
onClick={(): void => setIsCreateModalOpen(true)}
>
<Plus size={12} />
New Service Account
</Button>
</div>
</div>
<ServiceAccountsTable
data={paginatedAccounts}
loading={isLoading}
total={filteredAccounts.length}
currentPage={currentPage}
pageSize={PAGE_SIZE}
searchQuery={searchQuery}
onPageChange={setPage}
onRowClick={handleRowClick}
/>
<CreateServiceAccountModal
open={isCreateModalOpen}
onClose={(): void => setIsCreateModalOpen(false)}
onSuccess={handleCreateSuccess}
/>
<ServiceAccountDrawer
account={selectedAccount}
open={selectedAccount !== null}
onClose={handleDrawerClose}
onSuccess={handleDrawerSuccess}
/>
</>
);
}
export default ServiceAccountsSettings;

View File

@@ -0,0 +1,20 @@
export enum FilterMode {
All = 'all',
Active = 'active',
Disabled = 'disabled',
}
export enum ServiceAccountStatus {
Active = 'ACTIVE',
Disabled = 'DISABLED',
}
export interface ServiceAccountRow {
id: string;
name: string;
email: string;
roles: string[];
status: ServiceAccountStatus | string;
createdAt: string | null;
updatedAt: string | null;
}

View File

@@ -8,6 +8,7 @@ import {
BellDot,
Binoculars,
Book,
Bot,
Boxes,
BugIcon,
Building2,
@@ -358,6 +359,13 @@ export const settingsNavSections: SettingsNavSection[] = [
isEnabled: false,
itemKey: 'members',
},
{
key: ROUTES.SERVICE_ACCOUNTS_SETTINGS,
label: 'Service Accounts',
icon: <Bot size={16} />,
isEnabled: false,
itemKey: 'service-accounts',
},
{
key: ROUTES.API_KEYS,
label: 'API Keys',

View File

@@ -154,6 +154,7 @@ export const routesToSkip = [
ROUTES.ALL_DASHBOARD,
ROUTES.ORG_SETTINGS,
ROUTES.MEMBERS_SETTINGS,
ROUTES.SERVICE_ACCOUNTS_SETTINGS,
ROUTES.INGESTION_SETTINGS,
ROUTES.API_KEYS,
ROUTES.ERROR_DETAIL,

View File

@@ -1,129 +0,0 @@
import { renderHook } from '@testing-library/react';
import {
IBuilderFormula,
IClickHouseQuery,
IPromQLQuery,
Query,
} from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { useGetQueryLabels } from './useGetQueryLabels';
jest.mock('components/QueryBuilderV2/utils', () => ({
getQueryLabelWithAggregation: jest.fn(() => []),
}));
function buildQuery(overrides: Partial<Query> = {}): Query {
return {
id: 'test-id',
queryType: EQueryType.QUERY_BUILDER,
builder: {
queryData: [],
queryFormulas: [],
queryTraceOperator: [],
},
promql: [],
clickhouse_sql: [],
...overrides,
};
}
describe('useGetQueryLabels', () => {
describe('QUERY_BUILDER type', () => {
it('returns empty array when queryFormulas is undefined', () => {
const query = buildQuery({
queryType: EQueryType.QUERY_BUILDER,
builder: {
queryData: [],
queryFormulas: (undefined as unknown) as IBuilderFormula[],
queryTraceOperator: [],
},
});
const { result } = renderHook(() => useGetQueryLabels(query));
expect(result.current).toEqual([]);
});
it('returns formula labels when queryFormulas is populated', () => {
const query = buildQuery({
queryType: EQueryType.QUERY_BUILDER,
builder: {
queryData: [],
queryFormulas: [
({ queryName: 'F1' } as unknown) as IBuilderFormula,
({ queryName: 'F2' } as unknown) as IBuilderFormula,
],
queryTraceOperator: [],
},
});
const { result } = renderHook(() => useGetQueryLabels(query));
expect(result.current).toEqual([
{ label: 'F1', value: 'F1' },
{ label: 'F2', value: 'F2' },
]);
});
});
describe('CLICKHOUSE type', () => {
it('returns empty array when clickhouse_sql is undefined', () => {
const query = buildQuery({
queryType: EQueryType.CLICKHOUSE,
clickhouse_sql: (undefined as unknown) as IClickHouseQuery[],
});
const { result } = renderHook(() => useGetQueryLabels(query));
expect(result.current).toEqual([]);
});
it('returns labels from clickhouse_sql when populated', () => {
const query = buildQuery({
queryType: EQueryType.CLICKHOUSE,
clickhouse_sql: [
({ name: 'query_a' } as unknown) as IClickHouseQuery,
({ name: 'query_b' } as unknown) as IClickHouseQuery,
],
});
const { result } = renderHook(() => useGetQueryLabels(query));
expect(result.current).toEqual([
{ label: 'query_a', value: 'query_a' },
{ label: 'query_b', value: 'query_b' },
]);
});
});
describe('PROM type (default)', () => {
it('returns empty array when promql is undefined', () => {
const query = buildQuery({
queryType: EQueryType.PROM,
promql: (undefined as unknown) as IPromQLQuery[],
});
const { result } = renderHook(() => useGetQueryLabels(query));
expect(result.current).toEqual([]);
});
it('returns labels from promql when populated', () => {
const query = buildQuery({
queryType: EQueryType.PROM,
promql: [
({ name: 'prom_1' } as unknown) as IPromQLQuery,
({ name: 'prom_2' } as unknown) as IPromQLQuery,
],
});
const { result } = renderHook(() => useGetQueryLabels(query));
expect(result.current).toEqual([
{ label: 'prom_1', value: 'prom_1' },
{ label: 'prom_2', value: 'prom_2' },
]);
});
});
});

View File

@@ -11,7 +11,7 @@ export const useGetQueryLabels = (
const queryLabels = getQueryLabelWithAggregation(
currentQuery?.builder?.queryData || [],
);
const formulaLabels = (currentQuery?.builder?.queryFormulas ?? []).map(
const formulaLabels = currentQuery?.builder?.queryFormulas?.map(
(formula) => ({
label: formula.queryName,
value: formula.queryName,
@@ -20,13 +20,10 @@ export const useGetQueryLabels = (
return [...queryLabels, ...formulaLabels];
}
if (currentQuery?.queryType === EQueryType.CLICKHOUSE) {
return (currentQuery?.clickhouse_sql ?? []).map((q) => ({
return currentQuery?.clickhouse_sql?.map((q) => ({
label: q.name,
value: q.name,
}));
}
return (currentQuery?.promql ?? []).map((q) => ({
label: q.name,
value: q.name,
}));
return currentQuery?.promql?.map((q) => ({ label: q.name, value: q.name }));
}, [currentQuery]);

View File

@@ -1,33 +0,0 @@
import { useCallback } from 'react';
import { QueryParams } from 'constants/query';
import { parseAsString, useQueryState } from 'nuqs';
interface UseUrlYAxisUnitResult {
yAxisUnit: string;
onUnitChange: (value: string) => void;
}
/**
* Hook to manage y-axis unit synchronized with the URL query param.
* It:
* - Initializes from `QueryParams.yAxisUnit` or a provided default
* - Keeps local state in sync when the URL changes (e.g. back/forward, shared links)
* - Writes updates back to the URL while preserving existing query params
*/
function useUrlYAxisUnit(defaultUnit = ''): UseUrlYAxisUnitResult {
const [yAxisUnit, setYAxisUnitInUrl] = useQueryState(
QueryParams.yAxisUnit,
parseAsString.withDefault(defaultUnit),
);
const onUnitChange = useCallback(
(value: string): void => {
setYAxisUnitInUrl(value || null);
},
[setYAxisUnitInUrl],
);
return { yAxisUnit, onUnitChange };
}
export default useUrlYAxisUnit;

View File

@@ -6,7 +6,6 @@ import { Provider } from 'react-redux';
import AppRoutes from 'AppRoutes';
import { AxiosError } from 'axios';
import { ThemeProvider } from 'hooks/useDarkMode';
import { NuqsAdapter } from 'nuqs/adapters/react';
import { AppProvider } from 'providers/App/App';
import TimezoneProvider from 'providers/Timezone';
import store from 'store';
@@ -46,19 +45,17 @@ if (container) {
root.render(
<HelmetProvider>
<NuqsAdapter>
<ThemeProvider>
<TimezoneProvider>
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<AppProvider>
<AppRoutes />
</AppProvider>
</Provider>
</QueryClientProvider>
</TimezoneProvider>
</ThemeProvider>
</NuqsAdapter>
<ThemeProvider>
<TimezoneProvider>
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<AppProvider>
<AppRoutes />
</AppProvider>
</Provider>
</QueryClientProvider>
</TimezoneProvider>
</ThemeProvider>
</HelmetProvider>,
);
}

View File

@@ -32,8 +32,8 @@ export const editSlackDescriptionDefaultValue = `{{ range .Alerts -}} *Alert:* {
export const pagerDutyDescriptionDefaultVaule = `{{ if gt (len .Alerts.Firing) 0 -}} Alerts Firing: {{ range .Alerts.Firing }} - Message: {{ .Annotations.description }} Labels: {{ range .Labels.SortedPairs }} - {{ .Name }} = {{ .Value }} {{ end }} Annotations: {{ range .Annotations.SortedPairs }} - {{ .Name }} = {{ .Value }} {{ end }} Source: {{ .GeneratorURL }} {{ end }} {{- end }} {{ if gt (len .Alerts.Resolved) 0 -}} Alerts Resolved: {{ range .Alerts.Resolved }} - Message: {{ .Annotations.description }} Labels: {{ range .Labels.SortedPairs }} - {{ .Name }} = {{ .Value }} {{ end }} Annotations: {{ range .Annotations.SortedPairs }} - {{ .Name }} = {{ .Value }} {{ end }} Source: {{ .GeneratorURL }} {{ end }} {{- end }}`;
export const pagerDutyAdditionalDetailsDefaultValue = JSON.stringify({
firing: `{{ .Alerts.Firing | toJson }}`,
resolved: `{{ .Alerts.Resolved | toJson }}`,
firing: `{{ template "pagerduty.default.instances" .Alerts.Firing }}`,
resolved: `{{ template "pagerduty.default.instances" .Alerts.Resolved }}`,
num_firing: '{{ .Alerts.Firing | len }}',
num_resolved: '{{ .Alerts.Resolved | len }}',
});

View File

@@ -0,0 +1 @@
export { default } from 'container/ServiceAccountsSettings/ServiceAccountsSettings';

View File

@@ -84,6 +84,7 @@ function SettingsPage(): JSX.Element {
item.key === ROUTES.INGESTION_SETTINGS ||
item.key === ROUTES.ORG_SETTINGS ||
item.key === ROUTES.MEMBERS_SETTINGS ||
item.key === ROUTES.SERVICE_ACCOUNTS_SETTINGS ||
item.key === ROUTES.SHORTCUTS
? true
: item.isEnabled,
@@ -115,6 +116,7 @@ function SettingsPage(): JSX.Element {
item.key === ROUTES.API_KEYS ||
item.key === ROUTES.ORG_SETTINGS ||
item.key === ROUTES.MEMBERS_SETTINGS ||
item.key === ROUTES.SERVICE_ACCOUNTS_SETTINGS ||
item.key === ROUTES.INGESTION_SETTINGS
? true
: item.isEnabled,
@@ -140,7 +142,8 @@ function SettingsPage(): JSX.Element {
isEnabled:
item.key === ROUTES.API_KEYS ||
item.key === ROUTES.ORG_SETTINGS ||
item.key === ROUTES.MEMBERS_SETTINGS
item.key === ROUTES.MEMBERS_SETTINGS ||
item.key === ROUTES.SERVICE_ACCOUNTS_SETTINGS
? true
: item.isEnabled,
}));

View File

@@ -17,6 +17,7 @@ import { TFunction } from 'i18next';
import {
Backpack,
BellDot,
Bot,
Building,
Cpu,
CreditCard,
@@ -30,6 +31,7 @@ import {
} from 'lucide-react';
import ChannelsEdit from 'pages/ChannelsEdit';
import MembersSettings from 'pages/MembersSettings';
import ServiceAccountsSettings from 'pages/ServiceAccountsSettings';
import Shortcuts from 'pages/Shortcuts';
export const organizationSettings = (t: TFunction): RouteTabProps['routes'] => [
@@ -203,6 +205,21 @@ export const mySettings = (t: TFunction): RouteTabProps['routes'] => [
},
];
export const serviceAccountsSettings = (
t: TFunction,
): RouteTabProps['routes'] => [
{
Component: ServiceAccountsSettings,
name: (
<div className="periscope-tab">
<Bot size={16} /> {t('routes:service_accounts').toString()}
</div>
),
route: ROUTES.SERVICE_ACCOUNTS_SETTINGS,
key: ROUTES.SERVICE_ACCOUNTS_SETTINGS,
},
];
export const createAlertChannels = (t: TFunction): RouteTabProps['routes'] => [
{
Component: (): JSX.Element => (

View File

@@ -17,6 +17,7 @@ import {
organizationSettings,
roleDetails,
rolesSettings,
serviceAccountsSettings,
} from './config';
export const getRoutes = (
@@ -61,7 +62,11 @@ export const getRoutes = (
settings.push(...alertChannels(t));
if (isAdmin) {
settings.push(...apiKeys(t), ...membersSettings(t));
settings.push(
...apiKeys(t),
...membersSettings(t),
...serviceAccountsSettings(t),
);
}
// todo: Sagar - check the condition for role list and details page, to whom we want to serve

View File

@@ -4,6 +4,7 @@ import {
SetStateAction,
useEffect,
useMemo,
useState,
} from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
@@ -15,7 +16,6 @@ import TimeSeriesView from 'container/TimeSeriesView/TimeSeriesView';
import { convertDataValueToMs } from 'container/TimeSeriesView/utils';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import useUrlYAxisUnit from 'hooks/useUrlYAxisUnit';
import { AppState } from 'store/reducers';
import { Warning } from 'types/api';
import APIError from 'types/api/error';
@@ -52,8 +52,13 @@ function TimeSeriesViewContainer({
return isValid.every(Boolean);
}, [currentQuery]);
const defaultUnit = isValidToConvertToMs ? 'ms' : 'short';
const { yAxisUnit, onUnitChange } = useUrlYAxisUnit(defaultUnit);
const [yAxisUnit, setYAxisUnit] = useState<string>(
isValidToConvertToMs ? 'ms' : 'short',
);
const onUnitChangeHandler = (value: string): void => {
setYAxisUnit(value);
};
const { selectedTime: globalSelectedTime, maxTime, minTime } = useSelector<
AppState,
@@ -116,7 +121,7 @@ function TimeSeriesViewContainer({
return (
<div className="trace-explorer-time-series-view-container">
<div className="trace-explorer-time-series-view-container-header">
<BuilderUnitsFilter onChange={onUnitChange} yAxisUnit={yAxisUnit} />
<BuilderUnitsFilter onChange={onUnitChangeHandler} yAxisUnit={yAxisUnit} />
</div>
<TimeSeriesView
isFilterApplied={isFilterApplied}

View File

@@ -68,6 +68,7 @@ export const DashboardContext = createContext<IDashboardContext>({
APIError
>,
selectedDashboard: {} as Dashboard,
dashboardId: '',
layouts: [],
panelMap: {},
setPanelMap: () => {},
@@ -80,6 +81,8 @@ export const DashboardContext = createContext<IDashboardContext>({
updateLocalStorageDashboardVariables: () => {},
dashboardQueryRangeCalled: false,
setDashboardQueryRangeCalled: () => {},
selectedRowWidgetId: '',
setSelectedRowWidgetId: () => {},
isDashboardFetching: false,
columnWidths: {},
setColumnWidths: () => {},
@@ -99,6 +102,10 @@ export function DashboardProvider({
const [isDashboardLocked, setIsDashboardLocked] = useState<boolean>(false);
const [selectedRowWidgetId, setSelectedRowWidgetId] = useState<string | null>(
null,
);
const [
dashboardQueryRangeCalled,
setDashboardQueryRangeCalled,
@@ -461,6 +468,8 @@ export function DashboardProvider({
updateLocalStorageDashboardVariables,
dashboardQueryRangeCalled,
setDashboardQueryRangeCalled,
selectedRowWidgetId,
setSelectedRowWidgetId,
isDashboardFetching,
columnWidths,
setColumnWidths,
@@ -479,6 +488,8 @@ export function DashboardProvider({
currentDashboard,
dashboardQueryRangeCalled,
setDashboardQueryRangeCalled,
selectedRowWidgetId,
setSelectedRowWidgetId,
isDashboardFetching,
columnWidths,
setColumnWidths,

View File

@@ -58,9 +58,8 @@ jest.mock('react-redux', () => ({
jest.mock('uuid', () => ({ v4: jest.fn(() => 'mock-uuid') }));
function TestComponent(): JSX.Element {
const { dashboardResponse, selectedDashboard } = useDashboard();
const { dashboardResponse, dashboardId, selectedDashboard } = useDashboard();
const { dashboardVariables } = useDashboardVariables();
const dashboardId = selectedDashboard?.id;
return (
<div>

View File

@@ -1,28 +0,0 @@
const PREFIX = 'dashboard_row_widget_';
function getKey(dashboardId: string): string {
return `${PREFIX}${dashboardId}`;
}
export function setSelectedRowWidgetId(
dashboardId: string,
widgetId: string,
): void {
const key = getKey(dashboardId);
// remove all other selected widget ids for the dashboard before setting the new one
// to ensure only one widget is selected at a time. Helps out in weird navigate and refresh scenarios
Object.keys(sessionStorage)
.filter((k) => k.startsWith(PREFIX) && k !== key)
.forEach((k) => sessionStorage.removeItem(k));
sessionStorage.setItem(key, widgetId);
}
export function getSelectedRowWidgetId(dashboardId: string): string | null {
return sessionStorage.getItem(getKey(dashboardId));
}
export function clearSelectedRowWidgetId(dashboardId: string): void {
sessionStorage.removeItem(getKey(dashboardId));
}

View File

@@ -15,6 +15,7 @@ export interface IDashboardContext {
handleDashboardLockToggle: (value: boolean) => void;
dashboardResponse: UseQueryResult<SuccessResponseV2<Dashboard>, unknown>;
selectedDashboard: Dashboard | undefined;
dashboardId: string;
layouts: Layout[];
panelMap: Record<string, { widgets: Layout[]; collapsed: boolean }>;
setPanelMap: React.Dispatch<React.SetStateAction<Record<string, any>>>;
@@ -39,6 +40,8 @@ export interface IDashboardContext {
) => void;
dashboardQueryRangeCalled: boolean;
setDashboardQueryRangeCalled: (value: boolean) => void;
selectedRowWidgetId: string | null;
setSelectedRowWidgetId: React.Dispatch<React.SetStateAction<string | null>>;
isDashboardFetching: boolean;
columnWidths: WidgetColumnWidths;
setColumnWidths: React.Dispatch<React.SetStateAction<WidgetColumnWidths>>;

View File

@@ -7,7 +7,6 @@ import { render, RenderOptions, RenderResult } from '@testing-library/react';
import { FeatureKeys } from 'constants/features';
import { ORG_PREFERENCES } from 'constants/orgPreferences';
import { ResourceProvider } from 'hooks/useResourceAttribute';
import { NuqsAdapter } from 'nuqs/adapters/react';
import { AppContext } from 'providers/App/App';
import { IAppContext } from 'providers/App/types';
import { ErrorModalProvider } from 'providers/ErrorModalProvider';
@@ -281,25 +280,23 @@ export function AllTheProviders({
return (
<MemoryRouter initialEntries={[initialRouteValue]}>
<NuqsAdapter>
<QueryClientProvider client={queryClient}>
<Provider store={mockStored(roleValue)}>
<AppContext.Provider
value={getAppContextMock(roleValue, appContextOverridesValue)}
>
<ResourceProvider>
<ErrorModalProvider>
<TimezoneProvider>
<PreferenceContextProvider>
{queryBuilderContent}
</PreferenceContextProvider>
</TimezoneProvider>
</ErrorModalProvider>
</ResourceProvider>
</AppContext.Provider>
</Provider>
</QueryClientProvider>
</NuqsAdapter>
<QueryClientProvider client={queryClient}>
<Provider store={mockStored(roleValue)}>
<AppContext.Provider
value={getAppContextMock(roleValue, appContextOverridesValue)}
>
<ResourceProvider>
<ErrorModalProvider>
<TimezoneProvider>
<PreferenceContextProvider>
{queryBuilderContent}
</PreferenceContextProvider>
</TimezoneProvider>
</ErrorModalProvider>
</ResourceProvider>
</AppContext.Provider>
</Provider>
</QueryClientProvider>
</MemoryRouter>
);
}

View File

@@ -100,6 +100,7 @@ export const routePermission: Record<keyof typeof ROUTES, ROLES[]> = {
ROLES_SETTINGS: ['ADMIN'],
ROLE_DETAILS: ['ADMIN'],
MEMBERS_SETTINGS: ['ADMIN'],
SERVICE_ACCOUNTS_SETTINGS: ['ADMIN'],
BILLING: ['ADMIN'],
SUPPORT: ['ADMIN', 'EDITOR', 'VIEWER'],
SOMETHING_WENT_WRONG: ['ADMIN', 'EDITOR', 'VIEWER'],

354
go.mod
View File

@@ -1,51 +1,51 @@
module github.com/SigNoz/signoz
go 1.25.0
go 1.24.0
require (
dario.cat/mergo v1.0.2
dario.cat/mergo v1.0.1
github.com/AfterShip/clickhouse-sql-parser v0.4.16
github.com/ClickHouse/clickhouse-go/v2 v2.40.1
github.com/DATA-DOG/go-sqlmock v1.5.2
github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd
github.com/SigNoz/signoz-otel-collector v0.144.2
github.com/SigNoz/signoz-otel-collector v0.129.10-rc.9
github.com/antlr4-go/antlr/v4 v4.13.1
github.com/antonmedv/expr v1.15.3
github.com/bytedance/sonic v1.14.1
github.com/cespare/xxhash/v2 v2.3.0
github.com/coreos/go-oidc/v3 v3.17.0
github.com/coreos/go-oidc/v3 v3.14.1
github.com/dgraph-io/ristretto/v2 v2.3.0
github.com/dustin/go-humanize v1.0.1
github.com/gin-gonic/gin v1.11.0
github.com/go-co-op/gocron v1.30.1
github.com/go-openapi/runtime v0.29.2
github.com/go-openapi/strfmt v0.25.0
github.com/go-openapi/runtime v0.28.0
github.com/go-openapi/strfmt v0.23.0
github.com/go-redis/redismock/v9 v9.2.0
github.com/go-viper/mapstructure/v2 v2.5.0
github.com/go-viper/mapstructure/v2 v2.4.0
github.com/gojek/heimdall/v7 v7.0.3
github.com/golang-jwt/jwt/v5 v5.3.1
github.com/golang-jwt/jwt/v5 v5.3.0
github.com/google/uuid v1.6.0
github.com/gorilla/handlers v1.5.1
github.com/gorilla/mux v1.8.1
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674
github.com/huandu/go-sqlbuilder v1.35.0
github.com/jackc/pgx/v5 v5.7.6
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12
github.com/json-iterator/go v1.1.12
github.com/knadh/koanf v1.5.0
github.com/knadh/koanf/v2 v2.3.2
github.com/mailru/easyjson v0.9.0
github.com/open-telemetry/opamp-go v0.22.0
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/stanza v0.144.0
github.com/knadh/koanf/v2 v2.2.0
github.com/mailru/easyjson v0.7.7
github.com/open-telemetry/opamp-go v0.19.0
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/stanza v0.128.0
github.com/openfga/api/proto v0.0.0-20250909172242-b4b2a12f5c67
github.com/openfga/language/pkg/go v0.2.0-beta.2.0.20250428093642-7aeebe78bbfe
github.com/opentracing/opentracing-go v1.2.0
github.com/pkg/errors v0.9.1
github.com/prometheus/alertmanager v0.31.0
github.com/prometheus/alertmanager v0.28.1
github.com/prometheus/client_golang v1.23.2
github.com/prometheus/common v0.67.5
github.com/prometheus/prometheus v0.310.0
github.com/prometheus/common v0.66.1
github.com/prometheus/prometheus v0.304.1
github.com/redis/go-redis/extra/redisotel/v9 v9.15.1
github.com/redis/go-redis/v9 v9.17.2
github.com/redis/go-redis/v9 v9.15.1
github.com/rs/cors v1.11.1
github.com/russellhaering/gosaml2 v0.9.0
github.com/russellhaering/goxmldsig v1.2.0
@@ -54,7 +54,7 @@ require (
github.com/sethvargo/go-password v0.2.0
github.com/smartystreets/goconvey v1.8.1
github.com/soheilhy/cmux v0.1.5
github.com/spf13/cobra v1.10.2
github.com/spf13/cobra v1.10.1
github.com/srikanthccv/ClickHouse-go-mock v0.13.0
github.com/stretchr/testify v1.11.1
github.com/swaggest/jsonschema-go v0.3.78
@@ -64,71 +64,43 @@ require (
github.com/uptrace/bun/dialect/pgdialect v1.2.9
github.com/uptrace/bun/dialect/sqlitedialect v1.2.9
github.com/uptrace/bun/extra/bunotel v1.2.9
go.opentelemetry.io/collector/confmap v1.51.0
go.opentelemetry.io/collector/otelcol v0.144.0
go.opentelemetry.io/collector/pdata v1.51.0
go.opentelemetry.io/collector/confmap v1.34.0
go.opentelemetry.io/collector/otelcol v0.128.0
go.opentelemetry.io/collector/pdata v1.34.0
go.opentelemetry.io/contrib/config v0.10.0
go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.63.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0
go.opentelemetry.io/otel v1.40.0
go.opentelemetry.io/otel/metric v1.40.0
go.opentelemetry.io/otel/sdk v1.40.0
go.opentelemetry.io/otel/trace v1.40.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0
go.opentelemetry.io/otel v1.38.0
go.opentelemetry.io/otel/metric v1.38.0
go.opentelemetry.io/otel/sdk v1.38.0
go.opentelemetry.io/otel/trace v1.38.0
go.uber.org/multierr v1.11.0
go.uber.org/zap v1.27.1
golang.org/x/crypto v0.47.0
golang.org/x/exp v0.0.0-20260112195511-716be5621a96
golang.org/x/net v0.49.0
golang.org/x/oauth2 v0.34.0
go.uber.org/zap v1.27.0
golang.org/x/crypto v0.46.0
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b
golang.org/x/net v0.47.0
golang.org/x/oauth2 v0.30.0
golang.org/x/sync v0.19.0
golang.org/x/text v0.33.0
google.golang.org/protobuf v1.36.11
golang.org/x/text v0.32.0
google.golang.org/protobuf v1.36.9
gopkg.in/yaml.v2 v2.4.0
gopkg.in/yaml.v3 v3.0.1
k8s.io/apimachinery v0.35.0
k8s.io/apimachinery v0.34.0
modernc.org/sqlite v1.39.1
)
require (
github.com/aws/aws-sdk-go-v2 v1.41.1 // indirect
github.com/aws/aws-sdk-go-v2/config v1.32.7 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.19.7 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 // indirect
github.com/aws/aws-sdk-go-v2/service/sns v1.39.11 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 // indirect
github.com/aws/smithy-go v1.24.0 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/go-openapi/swag/cmdutils v0.25.4 // indirect
github.com/go-openapi/swag/conv v0.25.4 // indirect
github.com/go-openapi/swag/fileutils v0.25.4 // indirect
github.com/go-openapi/swag/jsonname v0.25.4 // indirect
github.com/go-openapi/swag/jsonutils v0.25.4 // indirect
github.com/go-openapi/swag/loading v0.25.4 // indirect
github.com/go-openapi/swag/mangling v0.25.4 // indirect
github.com/go-openapi/swag/netutils v0.25.4 // indirect
github.com/go-openapi/swag/stringutils v0.25.4 // indirect
github.com/go-openapi/swag/typeutils v0.25.4 // indirect
github.com/go-openapi/swag/yamlutils v0.25.4 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.27.0 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect
github.com/hashicorp/go-metrics v0.5.4 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/prometheus/client_golang/exp v0.0.0-20260108101519-fb0838f53562 // indirect
github.com/redis/go-redis/extra/rediscmd/v9 v9.15.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/swaggest/refl v1.4.0 // indirect
@@ -136,70 +108,69 @@ require (
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect
github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2 // indirect
go.opentelemetry.io/collector/client v1.50.0 // indirect
go.opentelemetry.io/collector/config/configoptional v1.50.0 // indirect
go.opentelemetry.io/collector/config/configretry v1.50.0 // indirect
go.opentelemetry.io/collector/exporter/exporterhelper v0.144.0 // indirect
go.opentelemetry.io/collector/internal/componentalias v0.145.0 // indirect
go.opentelemetry.io/collector/pdata/xpdata v0.144.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.opentelemetry.io/collector/config/configretry v1.34.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
golang.org/x/arch v0.20.0 // indirect
golang.org/x/tools/godoc v0.1.0-deprecated // indirect
modernc.org/libc v1.66.10 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
)
require (
cel.dev/expr v0.25.1 // indirect
cloud.google.com/go/auth v0.18.1 // indirect
cel.dev/expr v0.24.0 // indirect
cloud.google.com/go/auth v0.16.1 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect
cloud.google.com/go/compute/metadata v0.8.2 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect
github.com/ClickHouse/ch-go v0.67.0 // indirect
github.com/Masterminds/squirrel v1.5.4 // indirect
github.com/Yiling-J/theine-go v0.6.2 // indirect
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/armon/go-metrics v0.4.1 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/aws/aws-sdk-go v1.55.7 // indirect
github.com/beevik/etree v1.1.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/coder/quartz v0.3.0 // indirect
github.com/coreos/go-systemd/v22 v22.6.0 // indirect
github.com/coder/quartz v0.1.2 // indirect
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dennwc/varint v1.0.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/ebitengine/purego v0.9.1 // indirect
github.com/ebitengine/purego v0.8.4 // indirect
github.com/edsrzf/mmap-go v1.2.0 // indirect
github.com/elastic/lunes v0.2.0 // indirect
github.com/elastic/lunes v0.1.0 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect
github.com/expr-lang/expr v1.17.7
github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect
github.com/expr-lang/expr v1.17.5
github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-faster/city v1.0.1 // indirect
github.com/go-faster/errors v0.7.1 // indirect
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
github.com/go-jose/go-jose/v4 v4.1.1 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-openapi/analysis v0.24.2 // indirect
github.com/go-openapi/errors v0.22.6 // indirect
github.com/go-openapi/jsonpointer v0.22.4 // indirect
github.com/go-openapi/jsonreference v0.21.4 // indirect
github.com/go-openapi/loads v0.23.2 // indirect
github.com/go-openapi/spec v0.22.3 // indirect
github.com/go-openapi/swag v0.25.4 // indirect
github.com/go-openapi/validate v0.25.1 // indirect
github.com/go-openapi/analysis v0.23.0 // indirect
github.com/go-openapi/errors v0.22.0 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/loads v0.22.0 // indirect
github.com/go-openapi/spec v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/go-openapi/validate v0.24.0 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/gofrs/uuid v4.4.0+incompatible // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/gojek/valkyrie v0.0.0-20180215180059-6aee720afcdf // indirect
github.com/golang/protobuf v1.5.4 // indirect
@@ -207,22 +178,22 @@ require (
github.com/google/btree v1.1.3 // indirect
github.com/google/cel-go v0.26.1 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect
github.com/googleapis/gax-go/v2 v2.16.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
github.com/googleapis/gax-go/v2 v2.14.2 // indirect
github.com/gopherjs/gopherjs v1.17.2 // indirect
github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 // indirect
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
github.com/hashicorp/go-msgpack/v2 v2.1.5 // indirect
github.com/hashicorp/go-msgpack/v2 v2.1.1 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-sockaddr v1.0.7 // indirect
github.com/hashicorp/go-version v1.8.0 // indirect
github.com/hashicorp/go-version v1.7.0 // indirect
github.com/hashicorp/golang-lru v1.0.2 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/hashicorp/memberlist v0.5.4 // indirect
github.com/hashicorp/memberlist v0.5.1 // indirect
github.com/huandu/xstrings v1.4.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
@@ -230,25 +201,26 @@ require (
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jessevdk/go-flags v1.6.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/jonboulle/clockwork v0.5.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/jpillora/backoff v1.0.0 // indirect
github.com/jtolds/gls v4.20.0+incompatible // indirect
github.com/klauspost/compress v1.18.3 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
github.com/leodido/go-syslog/v4 v4.3.0 // indirect
github.com/leodido/go-syslog/v4 v4.2.0 // indirect
github.com/leodido/ragel-machinery v0.0.0-20190525184631-5f46317e436b // indirect
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect
github.com/magefile/mage v1.15.0 // indirect
github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/mdlayher/socket v0.5.1 // indirect
github.com/mdlayher/socket v0.4.1 // indirect
github.com/mdlayher/vsock v1.2.1 // indirect
github.com/mfridman/interpolate v0.0.2 // indirect
github.com/miekg/dns v1.1.72 // indirect
github.com/miekg/dns v1.1.65 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
@@ -257,27 +229,27 @@ require (
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect
github.com/natefinch/wrap v0.2.0 // indirect
github.com/oklog/run v1.2.0 // indirect
github.com/oklog/run v1.1.0 // indirect
github.com/oklog/ulid v1.3.1 // indirect
github.com/oklog/ulid/v2 v2.1.1
github.com/open-feature/go-sdk v1.17.0
github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal v0.144.0 // indirect
github.com/open-telemetry/opentelemetry-collector-contrib/internal/exp/metrics v0.145.0 // indirect
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.145.0 // indirect
github.com/open-telemetry/opentelemetry-collector-contrib/processor/deltatocumulativeprocessor v0.145.0 // indirect
github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal v0.128.0 // indirect
github.com/open-telemetry/opentelemetry-collector-contrib/internal/exp/metrics v0.128.0 // indirect
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.128.0 // indirect
github.com/open-telemetry/opentelemetry-collector-contrib/processor/deltatocumulativeprocessor v0.128.0 // indirect
github.com/openfga/openfga v1.10.1
github.com/paulmach/orb v0.11.1 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pierrec/lz4/v4 v4.1.23 // indirect
github.com/pierrec/lz4/v4 v4.1.22 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/pressly/goose/v3 v3.25.0 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/exporter-toolkit v0.15.1 // indirect
github.com/prometheus/otlptranslator v1.0.0 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
github.com/prometheus/sigv4 v0.4.1 // indirect
github.com/prometheus/exporter-toolkit v0.14.0 // indirect
github.com/prometheus/otlptranslator v0.0.0-20250320144820-d800c8b0eb07 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
github.com/prometheus/sigv4 v0.1.2 // indirect
github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/sagikazarmark/locafero v0.9.0 // indirect
@@ -285,7 +257,7 @@ require (
github.com/segmentio/asm v1.2.0 // indirect
github.com/segmentio/backo-go v1.0.1 // indirect
github.com/sethvargo/go-retry v0.3.0 // indirect
github.com/shirou/gopsutil/v4 v4.25.12 // indirect
github.com/shirou/gopsutil/v4 v4.25.5 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c // indirect
github.com/shurcooL/vfsgen v0.0.0-20230704071429-0000e147ea92 // indirect
@@ -300,92 +272,94 @@ require (
github.com/subosito/gotenv v1.6.0 // indirect
github.com/swaggest/openapi-go v0.2.60
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tklauser/go-sysconf v0.3.16 // indirect
github.com/tklauser/numcpus v0.11.0 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/tklauser/go-sysconf v0.3.15 // indirect
github.com/tklauser/numcpus v0.10.0 // indirect
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
github.com/valyala/fastjson v1.6.7 // indirect
github.com/trivago/tgo v1.0.7 // indirect
github.com/valyala/fastjson v1.6.4 // indirect
github.com/vjeantet/grok v1.0.1 // indirect
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
github.com/zeebo/xxh3 v1.0.2 // indirect
go.mongodb.org/mongo-driver v1.17.6 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/collector/component v1.51.0 // indirect
go.opentelemetry.io/collector/component/componentstatus v0.145.0 // indirect
go.opentelemetry.io/collector/component/componenttest v0.145.0 // indirect
go.opentelemetry.io/collector/config/configtelemetry v0.144.0 // indirect
go.opentelemetry.io/collector/confmap/provider/envprovider v1.50.0 // indirect
go.opentelemetry.io/collector/confmap/provider/fileprovider v1.50.0 // indirect
go.opentelemetry.io/collector/confmap/xconfmap v0.145.0 // indirect
go.opentelemetry.io/collector/connector v0.144.0 // indirect
go.opentelemetry.io/collector/connector/connectortest v0.144.0 // indirect
go.opentelemetry.io/collector/connector/xconnector v0.144.0 // indirect
go.opentelemetry.io/collector/consumer v1.51.0 // indirect
go.opentelemetry.io/collector/consumer/consumererror v0.144.0 // indirect
go.opentelemetry.io/collector/consumer/consumertest v0.145.0 // indirect
go.opentelemetry.io/collector/consumer/xconsumer v0.145.0 // indirect
go.opentelemetry.io/collector/exporter v1.50.0 // indirect
go.opentelemetry.io/collector/exporter/exportertest v0.144.0 // indirect
go.opentelemetry.io/collector/exporter/xexporter v0.144.0 // indirect
go.opentelemetry.io/collector/extension v1.50.0 // indirect
go.opentelemetry.io/collector/extension/extensioncapabilities v0.144.0 // indirect
go.opentelemetry.io/collector/extension/extensiontest v0.144.0 // indirect
go.opentelemetry.io/collector/extension/xextension v0.144.0 // indirect
go.opentelemetry.io/collector/featuregate v1.51.0 // indirect
go.opentelemetry.io/collector/internal/fanoutconsumer v0.144.0 // indirect
go.opentelemetry.io/collector/internal/telemetry v0.144.0 // indirect
go.opentelemetry.io/collector/pdata/pprofile v0.145.0 // indirect
go.opentelemetry.io/collector/pdata/testdata v0.145.0 // indirect
go.opentelemetry.io/collector/pipeline v1.51.0 // indirect
go.opentelemetry.io/collector/pipeline/xpipeline v0.144.0 // indirect
go.opentelemetry.io/collector/processor v1.51.0 // indirect
go.opentelemetry.io/collector/processor/processorhelper v0.144.0 // indirect
go.opentelemetry.io/collector/processor/processortest v0.145.0 // indirect
go.opentelemetry.io/collector/processor/xprocessor v0.145.0 // indirect
go.opentelemetry.io/collector/receiver v1.50.0 // indirect
go.opentelemetry.io/collector/receiver/receiverhelper v0.144.0 // indirect
go.opentelemetry.io/collector/receiver/receivertest v0.144.0 // indirect
go.opentelemetry.io/collector/receiver/xreceiver v0.144.0 // indirect
go.opentelemetry.io/collector/semconv v0.128.1-0.20250610090210-188191247685
go.opentelemetry.io/collector/service v0.144.0 // indirect
go.opentelemetry.io/collector/service/hostcapabilities v0.144.0 // indirect
go.opentelemetry.io/contrib/bridges/otelzap v0.13.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.65.0 // indirect
go.opentelemetry.io/contrib/otelconf v0.18.0 // indirect
go.opentelemetry.io/contrib/propagators/b3 v1.39.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.39.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 // indirect
go.opentelemetry.io/otel/exporters/prometheus v0.60.0
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.14.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.39.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.39.0 // indirect
go.opentelemetry.io/otel/log v0.15.0 // indirect
go.opentelemetry.io/otel/sdk/log v0.14.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.40.0
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
go.mongodb.org/mongo-driver v1.17.1 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/collector/component v1.34.0 // indirect
go.opentelemetry.io/collector/component/componentstatus v0.128.0 // indirect
go.opentelemetry.io/collector/component/componenttest v0.128.0 // indirect
go.opentelemetry.io/collector/config/configtelemetry v0.128.0 // indirect
go.opentelemetry.io/collector/confmap/provider/envprovider v1.34.0 // indirect
go.opentelemetry.io/collector/confmap/provider/fileprovider v1.34.0 // indirect
go.opentelemetry.io/collector/confmap/xconfmap v0.128.0 // indirect
go.opentelemetry.io/collector/connector v0.128.0 // indirect
go.opentelemetry.io/collector/connector/connectortest v0.128.0 // indirect
go.opentelemetry.io/collector/connector/xconnector v0.128.0 // indirect
go.opentelemetry.io/collector/consumer v1.34.0 // indirect
go.opentelemetry.io/collector/consumer/consumererror v0.128.0 // indirect
go.opentelemetry.io/collector/consumer/consumertest v0.128.0 // indirect
go.opentelemetry.io/collector/consumer/xconsumer v0.128.0 // indirect
go.opentelemetry.io/collector/exporter v0.128.0 // indirect
go.opentelemetry.io/collector/exporter/exportertest v0.128.0 // indirect
go.opentelemetry.io/collector/exporter/xexporter v0.128.0 // indirect
go.opentelemetry.io/collector/extension v1.34.0 // indirect
go.opentelemetry.io/collector/extension/extensioncapabilities v0.128.0 // indirect
go.opentelemetry.io/collector/extension/extensiontest v0.128.0 // indirect
go.opentelemetry.io/collector/extension/xextension v0.128.0 // indirect
go.opentelemetry.io/collector/featuregate v1.34.0 // indirect
go.opentelemetry.io/collector/internal/fanoutconsumer v0.128.0 // indirect
go.opentelemetry.io/collector/internal/telemetry v0.128.0 // indirect
go.opentelemetry.io/collector/pdata/pprofile v0.128.0 // indirect
go.opentelemetry.io/collector/pdata/testdata v0.128.0 // indirect
go.opentelemetry.io/collector/pipeline v0.128.0 // indirect
go.opentelemetry.io/collector/pipeline/xpipeline v0.128.0 // indirect
go.opentelemetry.io/collector/processor v1.34.0 // indirect
go.opentelemetry.io/collector/processor/processorhelper v0.128.0 // indirect
go.opentelemetry.io/collector/processor/processortest v0.128.0 // indirect
go.opentelemetry.io/collector/processor/xprocessor v0.128.0 // indirect
go.opentelemetry.io/collector/receiver v1.34.0 // indirect
go.opentelemetry.io/collector/receiver/receiverhelper v0.128.0 // indirect
go.opentelemetry.io/collector/receiver/receivertest v0.128.0 // indirect
go.opentelemetry.io/collector/receiver/xreceiver v0.128.0 // indirect
go.opentelemetry.io/collector/semconv v0.128.0
go.opentelemetry.io/collector/service v0.128.0 // indirect
go.opentelemetry.io/collector/service/hostcapabilities v0.128.0 // indirect
go.opentelemetry.io/contrib/bridges/otelzap v0.11.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0 // indirect
go.opentelemetry.io/contrib/otelconf v0.16.0 // indirect
go.opentelemetry.io/contrib/propagators/b3 v1.36.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.12.2 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.36.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.36.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0 // indirect
go.opentelemetry.io/otel/exporters/prometheus v0.58.0
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.12.2 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 // indirect
go.opentelemetry.io/otel/log v0.12.2 // indirect
go.opentelemetry.io/otel/sdk/log v0.12.2 // indirect
go.opentelemetry.io/otel/sdk/metric v1.38.0
go.opentelemetry.io/proto/otlp v1.8.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/mock v0.6.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/mod v0.32.0 // indirect
golang.org/x/sys v0.40.0 // indirect
golang.org/x/time v0.14.0 // indirect
golang.org/x/tools v0.41.0 // indirect
gonum.org/v1/gonum v0.17.0 // indirect
google.golang.org/api v0.265.0
google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect
google.golang.org/grpc v1.78.0 // indirect
golang.org/x/mod v0.30.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/time v0.11.0 // indirect
golang.org/x/tools v0.39.0 // indirect
gonum.org/v1/gonum v0.16.0 // indirect
google.golang.org/api v0.236.0
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 // indirect
google.golang.org/grpc v1.75.1 // indirect
gopkg.in/telebot.v3 v3.3.8 // indirect
k8s.io/client-go v0.35.0 // indirect
k8s.io/client-go v0.34.0 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect
sigs.k8s.io/yaml v1.6.0 // indirect
)
replace github.com/expr-lang/expr => github.com/SigNoz/expr v1.17.7-beta

918
go.sum

File diff suppressed because it is too large Load Diff

View File

@@ -95,7 +95,7 @@ func (d *Dispatcher) Run() {
d.ctx, d.cancel = context.WithCancel(context.Background())
d.mtx.Unlock()
d.run(d.alerts.Subscribe(fmt.Sprintf("dispatcher-%s", d.orgID)))
d.run(d.alerts.Subscribe())
close(d.done)
}
@@ -107,15 +107,14 @@ func (d *Dispatcher) run(it provider.AlertIterator) {
for {
select {
case alertWrapper, ok := <-it.Next():
if !ok || alertWrapper == nil {
case alert, ok := <-it.Next():
if !ok {
// Iterator exhausted for some reason.
if err := it.Err(); err != nil {
d.logger.ErrorContext(d.ctx, "Error on alert update", "err", err)
}
return
}
alert := alertWrapper.Data
d.logger.DebugContext(d.ctx, "SigNoz Custom Dispatcher: Received alert", "alert", alert)

View File

@@ -365,7 +365,7 @@ route:
logger := providerSettings.Logger
route := dispatch.NewRoute(conf.Route, nil)
marker := alertmanagertypes.NewMarker(prometheus.NewRegistry())
alerts, err := mem.NewAlerts(context.Background(), marker, time.Hour, 0, nil, logger, prometheus.NewRegistry(), nil)
alerts, err := mem.NewAlerts(context.Background(), marker, time.Hour, nil, logger, nil)
if err != nil {
t.Fatal(err)
}
@@ -496,7 +496,7 @@ route:
err := nfManager.SetNotificationConfig(orgId, ruleID, &config)
require.NoError(t, err)
}
err = alerts.Put(ctx, inputAlerts...)
err = alerts.Put(inputAlerts...)
if err != nil {
t.Fatal(err)
}
@@ -638,7 +638,7 @@ route:
logger := providerSettings.Logger
route := dispatch.NewRoute(conf.Route, nil)
marker := alertmanagertypes.NewMarker(prometheus.NewRegistry())
alerts, err := mem.NewAlerts(context.Background(), marker, time.Hour, 0, nil, logger, prometheus.NewRegistry(), nil)
alerts, err := mem.NewAlerts(context.Background(), marker, time.Hour, nil, logger, nil)
if err != nil {
t.Fatal(err)
}
@@ -798,7 +798,7 @@ route:
err := nfManager.SetNotificationConfig(orgId, ruleID, &config)
require.NoError(t, err)
}
err = alerts.Put(ctx, inputAlerts...)
err = alerts.Put(inputAlerts...)
if err != nil {
t.Fatal(err)
}
@@ -897,7 +897,7 @@ route:
logger := providerSettings.Logger
route := dispatch.NewRoute(conf.Route, nil)
marker := alertmanagertypes.NewMarker(prometheus.NewRegistry())
alerts, err := mem.NewAlerts(context.Background(), marker, time.Hour, 0, nil, logger, prometheus.NewRegistry(), nil)
alerts, err := mem.NewAlerts(context.Background(), marker, time.Hour, nil, logger, nil)
if err != nil {
t.Fatal(err)
}
@@ -1028,7 +1028,7 @@ route:
err := nfManager.SetNotificationConfig(orgId, ruleID, &config)
require.NoError(t, err)
}
err = alerts.Put(ctx, inputAlerts...)
err = alerts.Put(inputAlerts...)
if err != nil {
t.Fatal(err)
}
@@ -1159,7 +1159,7 @@ func newAlert(labels model.LabelSet) *alertmanagertypes.Alert {
func TestDispatcherRace(t *testing.T) {
logger := promslog.NewNopLogger()
marker := alertmanagertypes.NewMarker(prometheus.NewRegistry())
alerts, err := mem.NewAlerts(context.Background(), marker, time.Hour, 0, nil, logger, prometheus.NewRegistry(), nil)
alerts, err := mem.NewAlerts(context.Background(), marker, time.Hour, nil, logger, nil)
if err != nil {
t.Fatal(err)
}
@@ -1175,7 +1175,6 @@ func TestDispatcherRace(t *testing.T) {
}
func TestDispatcherRaceOnFirstAlertNotDeliveredWhenGroupWaitIsZero(t *testing.T) {
ctx := context.Background()
const numAlerts = 5000
confData := `receivers:
- name: 'slack'
@@ -1195,7 +1194,7 @@ route:
providerSettings := createTestProviderSettings()
logger := providerSettings.Logger
marker := alertmanagertypes.NewMarker(prometheus.NewRegistry())
alerts, err := mem.NewAlerts(context.Background(), marker, time.Hour, 0, nil, logger, prometheus.NewRegistry(), nil)
alerts, err := mem.NewAlerts(context.Background(), marker, time.Hour, nil, logger, nil)
if err != nil {
t.Fatal(err)
}
@@ -1248,7 +1247,7 @@ route:
for i := 0; i < numAlerts; i++ {
ruleId := fmt.Sprintf("Alert_%d", i)
alert := newAlert(model.LabelSet{"ruleId": model.LabelValue(ruleId)})
require.NoError(t, alerts.Put(ctx, alert))
require.NoError(t, alerts.Put(alert))
}
for deadline := time.Now().Add(5 * time.Second); time.Now().Before(deadline); {
@@ -1266,7 +1265,7 @@ func TestDispatcher_DoMaintenance(t *testing.T) {
r := prometheus.NewRegistry()
marker := alertmanagertypes.NewMarker(r)
alerts, err := mem.NewAlerts(context.Background(), marker, time.Minute, 0, nil, promslog.NewNopLogger(), prometheus.NewRegistry(), nil)
alerts, err := mem.NewAlerts(context.Background(), marker, time.Minute, nil, promslog.NewNopLogger(), nil)
if err != nil {
t.Fatal(err)
}
@@ -1371,7 +1370,7 @@ route:
logger := providerSettings.Logger
route := dispatch.NewRoute(conf.Route, nil)
marker := alertmanagertypes.NewMarker(prometheus.NewRegistry())
alerts, err := mem.NewAlerts(context.Background(), marker, time.Hour, 0, nil, logger, prometheus.NewRegistry(), nil)
alerts, err := mem.NewAlerts(context.Background(), marker, time.Hour, nil, logger, nil)
if err != nil {
t.Fatal(err)
}

View File

@@ -190,7 +190,7 @@ func New(ctx context.Context, logger *slog.Logger, registry prometheus.Registere
})
}()
server.alerts, err = mem.NewAlerts(ctx, server.marker, server.srvConfig.Alerts.GCInterval, 0, nil, server.logger, signozRegisterer, nil)
server.alerts, err = mem.NewAlerts(ctx, server.marker, server.srvConfig.Alerts.GCInterval, nil, server.logger, signozRegisterer)
if err != nil {
return nil, err
}
@@ -203,15 +203,15 @@ func New(ctx context.Context, logger *slog.Logger, registry prometheus.Registere
func (server *Server) GetAlerts(ctx context.Context, params alertmanagertypes.GettableAlertsParams) (alertmanagertypes.GettableAlerts, error) {
return alertmanagertypes.NewGettableAlertsFromAlertProvider(server.alerts, server.alertmanagerConfig, server.marker.Status, func(labels model.LabelSet) {
server.inhibitor.Mutes(ctx, labels)
server.silencer.Mutes(ctx, labels)
server.inhibitor.Mutes(labels)
server.silencer.Mutes(labels)
}, params)
}
func (server *Server) PutAlerts(ctx context.Context, postableAlerts alertmanagertypes.PostableAlerts) error {
alerts, err := alertmanagertypes.NewAlertsFromPostableAlerts(ctx, postableAlerts, time.Duration(server.srvConfig.Global.ResolveTimeout), time.Now())
alerts, err := alertmanagertypes.NewAlertsFromPostableAlerts(postableAlerts, time.Duration(server.srvConfig.Global.ResolveTimeout), time.Now())
// Notification sending alert takes precedence over validation errors.
if err := server.alerts.Put(ctx, alerts...); err != nil {
if err := server.alerts.Put(alerts...); err != nil {
return err
}
@@ -340,7 +340,6 @@ func (server *Server) TestAlert(ctx context.Context, receiversMap map[*alertmana
}
alerts, err := alertmanagertypes.NewAlertsFromPostableAlerts(
ctx,
postableAlerts,
time.Duration(server.srvConfig.Global.ResolveTimeout),
time.Now(),

View File

@@ -70,7 +70,7 @@ func TestServerTestReceiverTypeWebhook(t *testing.T) {
WebhookConfigs: []*config.WebhookConfig{
{
HTTPConfig: &commoncfg.HTTPClientConfig{},
URL: config.SecretTemplateURL(webhookURL.String()),
URL: &config.SecretURL{URL: webhookURL},
},
},
})
@@ -96,7 +96,7 @@ func TestServerPutAlerts(t *testing.T) {
WebhookConfigs: []*config.WebhookConfig{
{
HTTPConfig: &commoncfg.HTTPClientConfig{},
URL: config.SecretTemplateURL("http://localhost/test-receiver"),
URL: &config.SecretURL{URL: &url.URL{Host: "localhost", Path: "/test-receiver"}},
},
},
}))
@@ -176,7 +176,7 @@ func TestServerTestAlert(t *testing.T) {
WebhookConfigs: []*config.WebhookConfig{
{
HTTPConfig: &commoncfg.HTTPClientConfig{},
URL: config.SecretTemplateURL(webhook1URL.String()),
URL: &config.SecretURL{URL: webhook1URL},
},
},
}))
@@ -186,7 +186,7 @@ func TestServerTestAlert(t *testing.T) {
WebhookConfigs: []*config.WebhookConfig{
{
HTTPConfig: &commoncfg.HTTPClientConfig{},
URL: config.SecretTemplateURL(webhook2URL.String()),
URL: &config.SecretURL{URL: webhook2URL},
},
},
}))
@@ -268,7 +268,7 @@ func TestServerTestAlertContinuesOnFailure(t *testing.T) {
WebhookConfigs: []*config.WebhookConfig{
{
HTTPConfig: &commoncfg.HTTPClientConfig{},
URL: config.SecretTemplateURL(webhookURL.String()),
URL: &config.SecretURL{URL: webhookURL},
},
},
}))
@@ -278,7 +278,7 @@ func TestServerTestAlertContinuesOnFailure(t *testing.T) {
WebhookConfigs: []*config.WebhookConfig{
{
HTTPConfig: &commoncfg.HTTPClientConfig{},
URL: config.SecretTemplateURL("http://localhost:1/webhook"),
URL: &config.SecretURL{URL: &url.URL{Scheme: "http", Host: "localhost:1", Path: "/webhook"}},
},
},
}))

View File

@@ -15,7 +15,7 @@ import (
sdkmetric "go.opentelemetry.io/otel/metric"
sdkmetricnoop "go.opentelemetry.io/otel/metric/noop"
sdkresource "go.opentelemetry.io/otel/sdk/resource"
semconv "go.opentelemetry.io/otel/semconv/v1.39.0"
semconv "go.opentelemetry.io/otel/semconv/v1.37.0"
sdktrace "go.opentelemetry.io/otel/trace"
)

View File

@@ -6,7 +6,6 @@ import (
"math"
"strconv"
"strings"
"sync"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
@@ -91,41 +90,6 @@ func (client *client) Read(ctx context.Context, query *prompb.Query, sortSeries
return remote.FromQueryResult(sortSeries, res), nil
}
func (c *client) ReadMultiple(ctx context.Context, queries []*prompb.Query, sortSeries bool) (storage.SeriesSet, error) {
if len(queries) == 0 {
return storage.EmptySeriesSet(), nil
}
if len(queries) == 1 {
return c.Read(ctx, queries[0], sortSeries)
}
type result struct {
ss storage.SeriesSet
err error
}
results := make([]result, len(queries))
var wg sync.WaitGroup
wg.Add(len(queries))
for i, q := range queries {
go func(i int, q *prompb.Query) {
defer wg.Done()
ss, err := c.Read(ctx, q, sortSeries)
results[i] = result{ss, err}
}(i, q)
}
wg.Wait()
sets := make([]storage.SeriesSet, 0, len(queries))
for _, r := range results {
if r.err != nil {
return nil, r.err
}
sets = append(sets, r.ss)
}
return storage.NewMergeSeriesSet(sets, 0, storage.ChainedSeriesMerge), nil
}
func (client *client) queryToClickhouseQuery(_ context.Context, query *prompb.Query, metricName string, subQuery bool) (string, []any, error) {
var clickHouseQuery string
var conditions []string

View File

@@ -2,7 +2,6 @@ package prometheus
import (
"github.com/SigNoz/signoz/pkg/errors"
"github.com/prometheus/prometheus/model/labels"
"github.com/prometheus/prometheus/promql"
)
@@ -11,22 +10,35 @@ func RemoveExtraLabels(res *promql.Result, labelsToRemove ...string) error {
return nil
}
toRemove := make(map[string]struct{}, len(labelsToRemove))
for _, l := range labelsToRemove {
toRemove[l] = struct{}{}
}
switch res.Value.(type) {
case promql.Vector:
value := res.Value.(promql.Vector)
for i := range value {
b := labels.NewBuilder(value[i].Metric)
b.Del(labelsToRemove...)
newLabels := b.Labels()
value[i].Metric = newLabels
series := &(value)[i]
dst := series.Metric[:0]
for _, lbl := range series.Metric {
if _, drop := toRemove[lbl.Name]; !drop {
dst = append(dst, lbl)
}
}
series.Metric = dst
}
case promql.Matrix:
value := res.Value.(promql.Matrix)
for i := range value {
b := labels.NewBuilder(value[i].Metric)
b.Del(labelsToRemove...)
newLabels := b.Labels()
value[i].Metric = newLabels
series := &(value)[i]
dst := series.Metric[:0]
for _, lbl := range series.Metric {
if _, drop := toRemove[lbl.Name]; !drop {
dst = append(dst, lbl)
}
}
series.Metric = dst
}
case promql.Scalar:
return nil

View File

@@ -18,7 +18,6 @@ import (
"github.com/SigNoz/signoz/pkg/types/instrumentationtypes"
qbv5 "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/prometheus/prometheus/model/labels"
"github.com/prometheus/prometheus/promql"
"github.com/prometheus/prometheus/promql/parser"
)
@@ -255,16 +254,17 @@ func (q *promqlQuery) Execute(ctx context.Context) (*qbv5.Result, error) {
var series []*qbv5.TimeSeries
for _, v := range matrix {
var s qbv5.TimeSeries
lbls := make([]*qbv5.Label, 0, v.Metric.Len())
v.Metric.Range(func(l labels.Label) {
if excludeLabel(l.Name) {
return
lbls := make([]*qbv5.Label, 0, len(v.Metric))
for name, value := range v.Metric.Copy().Map() {
if excludeLabel(name) {
continue
}
lbls = append(lbls, &qbv5.Label{
Key: telemetrytypes.TelemetryFieldKey{Name: l.Name},
Value: l.Value,
Key: telemetrytypes.TelemetryFieldKey{Name: name},
Value: value,
})
})
}
s.Labels = lbls
for idx := range v.Floats {

View File

@@ -9,6 +9,7 @@ import (
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/prometheus"
"github.com/SigNoz/signoz/pkg/units"
"github.com/SigNoz/signoz/pkg/query-service/interfaces"
"github.com/SigNoz/signoz/pkg/query-service/model"
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
@@ -17,9 +18,7 @@ import (
"github.com/SigNoz/signoz/pkg/query-service/utils/timestamp"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/ruletypes"
"github.com/SigNoz/signoz/pkg/units"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/prometheus/prometheus/model/labels"
"github.com/prometheus/prometheus/promql"
)
@@ -462,12 +461,12 @@ func toCommonSeries(series promql.Series) v3.Series {
Points: make([]v3.Point, 0),
}
series.Metric.Range(func(lbl labels.Label) {
for _, lbl := range series.Metric {
commonSeries.Labels[lbl.Name] = lbl.Value
commonSeries.LabelsArray = append(commonSeries.LabelsArray, map[string]string{
lbl.Name: lbl.Value,
})
})
}
for _, f := range series.Floats {
commonSeries.Points = append(commonSeries.Points, v3.Point{

View File

@@ -35,7 +35,7 @@ func (c *conditionBuilder) conditionFor(
return "", err
}
if column.Type.GetType() == schema.ColumnTypeEnumJSON && querybuilder.BodyJSONQueryEnabled {
if column.IsJSONColumn() && querybuilder.BodyJSONQueryEnabled {
valueType, value := InferDataType(value, operator, key)
cond, err := NewJSONConditionBuilder(key, valueType).buildJSONCondition(operator, value, sb)
if err != nil {

View File

@@ -17,7 +17,7 @@ const (
LogsV2TimestampColumn = "timestamp"
LogsV2ObservedTimestampColumn = "observed_timestamp"
LogsV2BodyColumn = "body"
LogsV2BodyJSONColumn = constants.BodyV2Column
LogsV2BodyJSONColumn = constants.BodyJSONColumn
LogsV2BodyPromotedColumn = constants.BodyPromotedColumn
LogsV2TraceIDColumn = "trace_id"
LogsV2SpanIDColumn = "span_id"
@@ -34,7 +34,7 @@ const (
LogsV2ResourcesStringColumn = "resources_string"
LogsV2ScopeStringColumn = "scope_string"
BodyJSONColumnPrefix = constants.BodyV2ColumnPrefix
BodyJSONColumnPrefix = constants.BodyJSONColumnPrefix
BodyPromotedColumnPrefix = constants.BodyPromotedColumnPrefix
)

View File

@@ -61,7 +61,7 @@ var (
}
)
type fieldMapper struct{}
type fieldMapper struct {}
func NewFieldMapper() qbtypes.FieldMapper {
return &fieldMapper{}

File diff suppressed because one or more lines are too long

View File

@@ -260,7 +260,7 @@ func buildListLogsJSONIndexesQuery(cluster string, filters ...string) (string, [
sb.Where(sb.Equal("database", telemetrylogs.DBName))
sb.Where(sb.Equal("table", telemetrylogs.LogsV2LocalTableName))
sb.Where(sb.Or(
sb.ILike("expr", fmt.Sprintf("%%%s%%", querybuilder.FormatValueForContains(constants.BodyV2ColumnPrefix))),
sb.ILike("expr", fmt.Sprintf("%%%s%%", querybuilder.FormatValueForContains(constants.BodyJSONColumnPrefix))),
sb.ILike("expr", fmt.Sprintf("%%%s%%", querybuilder.FormatValueForContains(constants.BodyPromotedColumnPrefix))),
))

View File

@@ -117,7 +117,7 @@ func TestBuildListLogsJSONIndexesQuery(t *testing.T) {
expectedArgs: []any{
telemetrylogs.DBName,
telemetrylogs.LogsV2LocalTableName,
fmt.Sprintf("%%%s%%", querybuilder.FormatValueForContains(constants.BodyV2ColumnPrefix)),
fmt.Sprintf("%%%s%%", querybuilder.FormatValueForContains(constants.BodyJSONColumnPrefix)),
fmt.Sprintf("%%%s%%", querybuilder.FormatValueForContains(constants.BodyPromotedColumnPrefix)),
},
},
@@ -130,7 +130,7 @@ func TestBuildListLogsJSONIndexesQuery(t *testing.T) {
expectedArgs: []any{
telemetrylogs.DBName,
telemetrylogs.LogsV2LocalTableName,
fmt.Sprintf("%%%s%%", querybuilder.FormatValueForContains(constants.BodyV2ColumnPrefix)),
fmt.Sprintf("%%%s%%", querybuilder.FormatValueForContains(constants.BodyJSONColumnPrefix)),
fmt.Sprintf("%%%s%%", querybuilder.FormatValueForContains(constants.BodyPromotedColumnPrefix)),
fmt.Sprintf("%%%s%%", querybuilder.FormatValueForContains("foo")),
fmt.Sprintf("%%%s%%", querybuilder.FormatValueForContains("bar")),

View File

@@ -1,7 +1,6 @@
package alertmanagertypes
import (
"context"
"fmt"
"net/http"
"regexp"
@@ -91,8 +90,8 @@ func NewDeprecatedGettableAlertsFromGettableAlerts(gettableAlerts GettableAlerts
}
// Converts a slice of PostableAlert to a slice of Alert.
func NewAlertsFromPostableAlerts(ctx context.Context, postableAlerts PostableAlerts, resolveTimeout time.Duration, now time.Time) ([]*types.Alert, []error) {
alerts := v2.OpenAPIAlertsToAlerts(ctx, postableAlerts)
func NewAlertsFromPostableAlerts(postableAlerts PostableAlerts, resolveTimeout time.Duration, now time.Time) ([]*types.Alert, []error) {
alerts := v2.OpenAPIAlertsToAlerts(postableAlerts)
for _, alert := range alerts {
alert.UpdatedAt = now
@@ -197,15 +196,8 @@ func NewGettableAlertsFromAlertProvider(
if err = iterator.Err(); err != nil {
break
}
if a == nil {
break
}
alertData := a.Data
if alertData == nil {
continue
}
routes := dispatch.NewRoute(cfg.alertmanagerConfig.Route, nil).Match(alertData.Labels)
routes := dispatch.NewRoute(cfg.alertmanagerConfig.Route, nil).Match(a.Labels)
receivers := make([]string, 0, len(routes))
for _, r := range routes {
receivers = append(receivers, r.RouteOpts.Receiver)
@@ -215,11 +207,11 @@ func NewGettableAlertsFromAlertProvider(
continue
}
if !alertFilter(alertData, now) {
if !alertFilter(a, now) {
continue
}
alert := v2.AlertToOpenAPIAlert(alertData, getAlertStatusFunc(alertData.Fingerprint()), receivers, nil)
alert := v2.AlertToOpenAPIAlert(a, getAlertStatusFunc(a.Fingerprint()), receivers, nil)
res = append(res, alert)
}

Some files were not shown because too many files have changed in this diff Show More