Compare commits

..

24 Commits

Author SHA1 Message Date
Naman Verma
7011d60999 Merge branch 'main' into nv/4102 2026-03-03 22:38:40 +05:30
Naman Verma
aa124a3a56 fix: parallel calls for each query in readmultiple method 2026-03-03 22:37:59 +05:30
Vinicius Lourenço
ccb88ab03f feat(vite): migrate from webpack (#10392)
Some checks are pending
build-staging / prepare (push) Waiting to run
build-staging / js-build (push) Blocked by required conditions
build-staging / go-build (push) Blocked by required conditions
build-staging / staging (push) Blocked by required conditions
Release Drafter / update_release_draft (push) Waiting to run
2026-03-03 19:50:13 +05:30
Ashwin Bhatkal
4f0e245e3d chore(frontend): dynamic vars load with empty textbox variables (#10480)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* fix(frontend): dynamic vars load with empty textbox variables
2026-03-03 16:11:57 +05:30
Nikhil Mantri
a5549f6d5b chore: add status info icon and include additional metrics for status (#10231) 2026-03-03 10:13:52 +00:00
Srikanth Chekuri
1d967fadac chore(metrics-explorer): handle errors properly (#10474) 2026-03-03 09:27:09 +00:00
Naman Verma
bdef3660a5 Merge branch 'main' into nv/4102 2026-03-03 14:48:00 +05:30
Naman Verma
c22a81ba3b chore: 1.25 bullseye is still an rc so shifting to bookworm 2026-03-03 11:18:59 +05:30
Naman Verma
5b40ae3477 chore: no need for our own alert store callback 2026-03-03 11:12:25 +05:30
Naman Verma
196036988e chore: upgrade go version in dockerfile to 1.25 2026-03-03 10:49:53 +05:30
Naman Verma
0cbcc63cbe Merge branch 'main' into nv/4102 2026-03-03 09:31:50 +05:30
Naman Verma
f3b2cb245f Merge branch 'main' into nv/4102 2026-03-02 19:04:52 +05:30
Naman Verma
3be3727dc2 test: ut fix, pass no nil prometheus registry 2026-03-02 19:04:37 +05:30
Naman Verma
438bc0a014 test: ut fix, just matched expected and actual nothing else 2026-03-02 18:44:59 +05:30
Naman Verma
7c809d7d54 test: ut fix, just matched expected and actual nothing else 2026-03-02 18:43:52 +05:30
Naman Verma
cd9fd910bf test: ut fix, just matched expected and actual nothing else 2026-03-02 18:22:13 +05:30
Naman Verma
ec4d55e796 test: ut fix, just matched expected and actual nothing else 2026-03-02 18:18:54 +05:30
Naman Verma
59a43cb1ef fix: correct imported version of semconv in sdk.go 2026-03-02 17:59:44 +05:30
Naman Verma
50ed7e99cc Merge branch 'main' into nv/4102 2026-03-02 17:44:23 +05:30
Naman Verma
87455bd014 chore: slightly unsure changes 2026-03-02 14:04:48 +05:30
Naman Verma
f5b9c55408 Merge branch 'main' into nv/4102 2026-03-02 13:35:08 +05:30
Naman Verma
cb1ab0024f chore: easy changes first 2026-03-02 13:31:33 +05:30
Naman Verma
0bbf1a3bee chore: upgrade prometheus/prometheus to latest available version 2026-03-02 11:25:59 +05:30
Naman Verma
2595ee0b53 chore: upgrade prometheus/common to latest available version 2026-03-02 11:04:25 +05:30
132 changed files with 6470 additions and 6715 deletions

View File

@@ -58,19 +58,19 @@ jobs:
run: |
mkdir -p frontend
echo 'CI=1' > frontend/.env
echo 'INTERCOM_APP_ID="${{ secrets.INTERCOM_APP_ID }}"' >> frontend/.env
echo 'SEGMENT_ID="${{ secrets.SEGMENT_ID }}"' >> frontend/.env
echo 'SENTRY_AUTH_TOKEN="${{ secrets.SENTRY_AUTH_TOKEN }}"' >> frontend/.env
echo 'SENTRY_ORG="${{ secrets.SENTRY_ORG }}"' >> frontend/.env
echo 'SENTRY_PROJECT_ID="${{ secrets.SENTRY_PROJECT_ID }}"' >> frontend/.env
echo 'SENTRY_DSN="${{ secrets.SENTRY_DSN }}"' >> frontend/.env
echo 'TUNNEL_URL="${{ secrets.TUNNEL_URL }}"' >> frontend/.env
echo 'TUNNEL_DOMAIN="${{ secrets.TUNNEL_DOMAIN }}"' >> frontend/.env
echo 'POSTHOG_KEY="${{ secrets.POSTHOG_KEY }}"' >> frontend/.env
echo 'PYLON_APP_ID="${{ secrets.PYLON_APP_ID }}"' >> frontend/.env
echo 'APPCUES_APP_ID="${{ secrets.APPCUES_APP_ID }}"' >> frontend/.env
echo 'PYLON_IDENTITY_SECRET="${{ secrets.PYLON_IDENTITY_SECRET }}"' >> frontend/.env
echo 'DOCS_BASE_URL="https://signoz.io"' >> frontend/.env
echo 'VITE_INTERCOM_APP_ID="${{ secrets.INTERCOM_APP_ID }}"' >> frontend/.env
echo 'VITE_SEGMENT_ID="${{ secrets.SEGMENT_ID }}"' >> frontend/.env
echo 'VITE_SENTRY_AUTH_TOKEN="${{ secrets.SENTRY_AUTH_TOKEN }}"' >> frontend/.env
echo 'VITE_SENTRY_ORG="${{ secrets.SENTRY_ORG }}"' >> frontend/.env
echo 'VITE_SENTRY_PROJECT_ID="${{ secrets.SENTRY_PROJECT_ID }}"' >> frontend/.env
echo 'VITE_SENTRY_DSN="${{ secrets.SENTRY_DSN }}"' >> frontend/.env
echo 'VITE_TUNNEL_URL="${{ secrets.TUNNEL_URL }}"' >> frontend/.env
echo 'VITE_TUNNEL_DOMAIN="${{ secrets.TUNNEL_DOMAIN }}"' >> frontend/.env
echo 'VITE_POSTHOG_KEY="${{ secrets.POSTHOG_KEY }}"' >> frontend/.env
echo 'VITE_PYLON_APP_ID="${{ secrets.PYLON_APP_ID }}"' >> frontend/.env
echo 'VITE_APPCUES_APP_ID="${{ secrets.APPCUES_APP_ID }}"' >> frontend/.env
echo 'VITE_PYLON_IDENTITY_SECRET="${{ secrets.PYLON_IDENTITY_SECRET }}"' >> frontend/.env
echo 'VITE_DOCS_BASE_URL="https://signoz.io"' >> frontend/.env
- name: cache-dotenv
uses: actions/cache@v4
with:

View File

@@ -64,12 +64,12 @@ jobs:
run: |
mkdir -p frontend
echo 'CI=1' > frontend/.env
echo 'TUNNEL_URL="${{ secrets.NP_TUNNEL_URL }}"' >> frontend/.env
echo 'TUNNEL_DOMAIN="${{ secrets.NP_TUNNEL_DOMAIN }}"' >> frontend/.env
echo 'PYLON_APP_ID="${{ secrets.NP_PYLON_APP_ID }}"' >> frontend/.env
echo 'APPCUES_APP_ID="${{ secrets.NP_APPCUES_APP_ID }}"' >> frontend/.env
echo 'PYLON_IDENTITY_SECRET="${{ secrets.NP_PYLON_IDENTITY_SECRET }}"' >> frontend/.env
echo 'DOCS_BASE_URL="https://staging.signoz.io"' >> frontend/.env
echo 'VITE_TUNNEL_URL="${{ secrets.NP_TUNNEL_URL }}"' >> frontend/.env
echo 'VITE_TUNNEL_DOMAIN="${{ secrets.NP_TUNNEL_DOMAIN }}"' >> frontend/.env
echo 'VITE_PYLON_APP_ID="${{ secrets.NP_PYLON_APP_ID }}"' >> frontend/.env
echo 'VITE_APPCUES_APP_ID="${{ secrets.NP_APPCUES_APP_ID }}"' >> frontend/.env
echo 'VITE_PYLON_IDENTITY_SECRET="${{ secrets.NP_PYLON_IDENTITY_SECRET }}"' >> frontend/.env
echo 'VITE_DOCS_BASE_URL="https://staging.signoz.io"' >> frontend/.env
- name: cache-dotenv
uses: actions/cache@v4
with:

View File

@@ -24,19 +24,19 @@ jobs:
- name: dotenv-frontend
working-directory: frontend
run: |
echo 'INTERCOM_APP_ID="${{ secrets.INTERCOM_APP_ID }}"' > .env
echo 'SEGMENT_ID="${{ secrets.SEGMENT_ID }}"' >> .env
echo 'SENTRY_AUTH_TOKEN="${{ secrets.SENTRY_AUTH_TOKEN }}"' >> .env
echo 'SENTRY_ORG="${{ secrets.SENTRY_ORG }}"' >> .env
echo 'SENTRY_PROJECT_ID="${{ secrets.SENTRY_PROJECT_ID }}"' >> .env
echo 'SENTRY_DSN="${{ secrets.SENTRY_DSN }}"' >> .env
echo 'TUNNEL_URL="${{ secrets.TUNNEL_URL }}"' >> .env
echo 'TUNNEL_DOMAIN="${{ secrets.TUNNEL_DOMAIN }}"' >> .env
echo 'POSTHOG_KEY="${{ secrets.POSTHOG_KEY }}"' >> .env
echo 'PYLON_APP_ID="${{ secrets.PYLON_APP_ID }}"' >> .env
echo 'APPCUES_APP_ID="${{ secrets.APPCUES_APP_ID }}"' >> .env
echo 'PYLON_IDENTITY_SECRET="${{ secrets.PYLON_IDENTITY_SECRET }}"' >> .env
echo 'DOCS_BASE_URL="https://signoz.io"' >> .env
echo 'VITE_INTERCOM_APP_ID="${{ secrets.INTERCOM_APP_ID }}"' > .env
echo 'VITE_SEGMENT_ID="${{ secrets.SEGMENT_ID }}"' >> .env
echo 'VITE_SENTRY_AUTH_TOKEN="${{ secrets.SENTRY_AUTH_TOKEN }}"' >> .env
echo 'VITE_SENTRY_ORG="${{ secrets.SENTRY_ORG }}"' >> .env
echo 'VITE_SENTRY_PROJECT_ID="${{ secrets.SENTRY_PROJECT_ID }}"' >> .env
echo 'VITE_SENTRY_DSN="${{ secrets.SENTRY_DSN }}"' >> .env
echo 'VITE_TUNNEL_URL="${{ secrets.TUNNEL_URL }}"' >> .env
echo 'VITE_TUNNEL_DOMAIN="${{ secrets.TUNNEL_DOMAIN }}"' >> .env
echo 'VITE_POSTHOG_KEY="${{ secrets.POSTHOG_KEY }}"' >> .env
echo 'VITE_PYLON_APP_ID="${{ secrets.PYLON_APP_ID }}"' >> .env
echo 'VITE_APPCUES_APP_ID="${{ secrets.APPCUES_APP_ID }}"' >> .env
echo 'VITE_PYLON_IDENTITY_SECRET="${{ secrets.PYLON_IDENTITY_SECRET }}"' >> .env
echo 'VITE_DOCS_BASE_URL="https://signoz.io"' >> .env
- name: node-setup
uses: actions/setup-node@v5
with:

View File

@@ -103,8 +103,9 @@ jobs:
make py-test-setup
- name: Generate permissions.type.ts
working-directory: ./frontend
run: |
node frontend/scripts/generate-permissions-type.js
yarn generate:permissions-type
- name: Teardown test environment
if: always()

View File

@@ -238,4 +238,4 @@ py-clean: ## Clear all pycache and pytest cache from tests directory recursively
.PHONY: gen-mocks
gen-mocks:
@echo ">> Generating mocks"
@mockery --config .mockery.yml
@mockery --config .mockery.yml

View File

@@ -1,4 +1,4 @@
FROM golang:1.24-bullseye
FROM golang:1.25-bookworm
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.24-bullseye
FROM golang:1.25-bookworm
ARG OS="linux"
ARG TARGETARCH

View File

@@ -1,16 +0,0 @@
{
"presets": [
"@babel/preset-env",
["@babel/preset-react", { "runtime": "automatic" }],
"@babel/preset-typescript"
],
"plugins": [
"react-hot-loader/babel",
"@babel/plugin-proposal-class-properties"
],
"env": {
"production": {
"presets": ["minify"]
}
}
}

View File

@@ -11,7 +11,6 @@ module.exports = {
browser: true,
es2021: true,
node: true,
'jest/globals': true,
},
extends: [
'eslint:recommended',
@@ -25,6 +24,7 @@ module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: './tsconfig.json',
tsconfigRootDir: __dirname,
ecmaFeatures: {
jsx: true,
},
@@ -37,7 +37,7 @@ module.exports = {
'simple-import-sort', // Auto-sort imports
'react-hooks', // React Hooks rules
'prettier', // Code formatting
'jest', // Jest test rules
// 'jest', // TODO: Wait support on Biome to enable again
'jsx-a11y', // Accessibility rules
'import', // Import/export linting
'sonarjs', // Code quality/complexity

View File

@@ -1 +1 @@
16.15.0
22

View File

@@ -0,0 +1,4 @@
export const ENVIRONMENT = {
baseURL: process.env.VITE_FRONTEND_API_ENDPOINT || '',
wsURL: process.env.VITE_WEBSOCKET_API_ENDPOINT || '',
};

20
frontend/babel.config.cjs Normal file
View File

@@ -0,0 +1,20 @@
module.exports = {
presets: [
['@babel/preset-env', { modules: 'auto' }],
['@babel/preset-react', { runtime: 'automatic' }],
['@babel/preset-typescript'],
],
plugins: ['@babel/plugin-proposal-class-properties'],
env: {
test: {
presets: [
[
'@babel/preset-env',
{ modules: 'commonjs', targets: { node: 'current' } },
],
['@babel/preset-react', { runtime: 'automatic' }],
['@babel/preset-typescript'],
],
},
},
};

View File

@@ -1,6 +0,0 @@
module.exports = {
presets: [
['@babel/preset-env', { targets: { node: 'current' } }],
'@babel/preset-typescript',
],
};

View File

@@ -1,8 +0,0 @@
{
"files": [
{
"path": "./build/**.js",
"maxSize": "1.2MB"
}
]
}

View File

@@ -1,8 +1,8 @@
NODE_ENV="development"
BUNDLE_ANALYSER="true"
FRONTEND_API_ENDPOINT="http://localhost:8080/"
PYLON_APP_ID="pylon-app-id"
APPCUES_APP_ID="appcess-app-id"
PYLON_IDENTITY_SECRET="pylon-identity-secret"
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"
CI="1"
CI="1"

View File

@@ -1,127 +1,119 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta
http-equiv="Cache-Control"
content="no-cache, no-store, must-revalidate, max-age: 0"
/>
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" />
<!-- Preconnect to CDNs -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="preconnect" href="https://cdn.vercel.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap"
rel="stylesheet"
/>
<title data-react-helmet="true">
Open source Observability platform | SigNoz
</title>
<meta
data-react-helmet="true"
property="og:title"
content="Open source Observability platform | SigNoz"
/>
<meta
data-react-helmet="true"
name="description"
content="SigNoz is an open source observability platform to help you find issues in your deployed applications & solve them quickly. It provides a single pane of glass for metrics, traces and logs with deep filtering and aggregation to pin down specific issues very quickly."
/>
<meta
data-react-helmet="true"
property="og:description"
content="SigNoz is an open source observability platform to help you find issues in your deployed applications & solve them quickly. It provides a single pane of glass for metrics, traces and logs with deep filtering and aggregation to pin down specific issues very quickly."
/>
<meta
data-react-helmet="true"
property="og:image"
content="/images/signoz-hero-image.webp"
/>
<meta
data-react-helmet="true"
name="twitter:image"
content="/images/signoz-hero-image.webp"
/>
<meta
data-react-helmet="true"
name="twitter:image:alt"
content="Image for Open source Observability platform | SigNoz"
/>
<meta
data-react-helmet="true"
name="twitter:card"
content="summary_large_image"
/>
<meta data-react-helmet="true" name="docusaurus_locale" content="en" />
<meta data-react-helmet="true" name="docusaurus_tag" content="default" />
<meta name="robots" content="noindex" />
<link data-react-helmet="true" rel="shortcut icon" href="/favicon.ico" />
<% if (htmlWebpackPlugin.options.templateParameters.preloadFonts) { %> <%
htmlWebpackPlugin.options.templateParameters.preloadFonts.forEach(function(font)
{ %>
<link
rel="preload"
href="<%= font.href %>"
as="<%= font.as %>"
type="<%= font.type %>"
crossorigin="<%= font.crossorigin %>"
/>
<% }); %> <% } %>
</head>
<body data-theme="default">
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script>
const PYLON_APP_ID = '<%= htmlWebpackPlugin.options.PYLON_APP_ID %>';
(function () {
var e = window;
var t = document;
var n = function () {
n.e(arguments);
};
n.q = [];
n.e = function (e) {
n.q.push(e);
};
e.Pylon = n;
var r = function () {
var e = t.createElement('script');
e.setAttribute('type', 'text/javascript');
e.setAttribute('async', 'true');
e.setAttribute(
'src',
'https://widget.usepylon.com/widget/' + PYLON_APP_ID,
);
var n = t.getElementsByTagName('script')[0];
n.parentNode.insertBefore(e, n);
};
if (t.readyState === 'complete') {
r();
} else if (e.addEventListener) {
e.addEventListener('load', r, false);
}
})();
</script>
<script type="text/javascript">
window.AppcuesSettings = { enableURLDetection: true };
</script>
<script>
const APPCUES_APP_ID = '<%= htmlWebpackPlugin.options.APPCUES_APP_ID %>';
(function (d, t) {
var a = d.createElement(t);
a.async = 1;
a.src = '//fast.appcues.com/' + APPCUES_APP_ID + '.js';
var s = d.getElementsByTagName(t)[0];
s.parentNode.insertBefore(a, s);
})(document, 'script');
</script>
<link rel="stylesheet" href="/css/uPlot.min.css" />
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta
http-equiv="Cache-Control"
content="no-cache, no-store, must-revalidate, max-age: 0"
/>
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="preconnect" href="https://cdn.vercel.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap"
rel="stylesheet"
/>
<title data-react-helmet="true">
Open source Observability platform | SigNoz
</title>
<meta
data-react-helmet="true"
property="og:title"
content="Open source Observability platform | SigNoz"
/>
<meta
data-react-helmet="true"
name="description"
content="SigNoz is an open source observability platform to help you find issues in your deployed applications & solve them quickly. It provides a single pane of glass for metrics, traces and logs with deep filtering and aggregation to pin down specific issues very quickly."
/>
<meta
data-react-helmet="true"
property="og:description"
content="SigNoz is an open source observability platform to help you find issues in your deployed applications & solve them quickly. It provides a single pane of glass for metrics, traces and logs with deep filtering and aggregation to pin down specific issues very quickly."
/>
<meta
data-react-helmet="true"
property="og:image"
content="/images/signoz-hero-image.webp"
/>
<meta
data-react-helmet="true"
name="twitter:image"
content="/images/signoz-hero-image.webp"
/>
<meta
data-react-helmet="true"
name="twitter:image:alt"
content="Image for Open source Observability platform | SigNoz"
/>
<meta
data-react-helmet="true"
name="twitter:card"
content="summary_large_image"
/>
<meta data-react-helmet="true" name="docusaurus_locale" content="en" />
<meta data-react-helmet="true" name="docusaurus_tag" content="default" />
<meta name="robots" content="noindex" />
<link data-react-helmet="true" rel="shortcut icon" href="/favicon.ico" />
</head>
<body data-theme="default">
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script>
var PYLON_APP_ID = '<%- PYLON_APP_ID %>';
if (PYLON_APP_ID) {
(function () {
var e = window;
var t = document;
var n = function () {
n.e(arguments);
};
n.q = [];
n.e = function (e) {
n.q.push(e);
};
e.Pylon = n;
var r = function () {
var e = t.createElement('script');
e.setAttribute('type', 'text/javascript');
e.setAttribute('async', 'true');
e.setAttribute(
'src',
'https://widget.usepylon.com/widget/' + PYLON_APP_ID,
);
var n = t.getElementsByTagName('script')[0];
n.parentNode.insertBefore(e, n);
};
if (t.readyState === 'complete') {
r();
} else if (e.addEventListener) {
e.addEventListener('load', r, false);
}
})();
}
</script>
<script type="text/javascript">
window.AppcuesSettings = { enableURLDetection: true };
</script>
<script>
var APPCUES_APP_ID = '<%- APPCUES_APP_ID %>';
if (APPCUES_APP_ID) {
(function (d, t) {
var a = d.createElement(t);
a.async = 1;
a.src = '//fast.appcues.com/' + APPCUES_APP_ID + '.js';
var s = d.getElementsByTagName(t)[0];
s.parentNode.insertBefore(a, s);
})(document, 'script');
}
</script>
<link rel="stylesheet" href="/css/uPlot.min.css" />
<script type="module" src="./src/index.tsx"></script>
</body>
</html>

View File

@@ -17,29 +17,49 @@ const config: Config.InitialOptions = {
'^hooks/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,
'^src/hooks/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,
'^.*/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,
'^constants/env$': '<rootDir>/__mocks__/env.ts',
'^src/constants/env$': '<rootDir>/__mocks__/env.ts',
'^@signozhq/icons$':
'<rootDir>/node_modules/@signozhq/icons/dist/index.esm.js',
'^react-syntax-highlighter/dist/esm/(.*)$':
'<rootDir>/node_modules/react-syntax-highlighter/dist/cjs/$1',
'^@signozhq/sonner$':
'<rootDir>/node_modules/@signozhq/sonner/dist/sonner.js',
'^@signozhq/button$':
'<rootDir>/node_modules/@signozhq/button/dist/button.js',
'^@signozhq/calendar$':
'<rootDir>/node_modules/@signozhq/calendar/dist/calendar.js',
'^@signozhq/badge': '<rootDir>/node_modules/@signozhq/badge/dist/badge.js',
'^@signozhq/checkbox':
'<rootDir>/node_modules/@signozhq/checkbox/dist/checkbox.js',
'^@signozhq/switch': '<rootDir>/node_modules/@signozhq/switch/dist/switch.js',
'^@signozhq/callout':
'<rootDir>/node_modules/@signozhq/callout/dist/callout.js',
'^@signozhq/combobox':
'<rootDir>/node_modules/@signozhq/combobox/dist/combobox.js',
'^@signozhq/input': '<rootDir>/node_modules/@signozhq/input/dist/input.js',
'^@signozhq/command':
'<rootDir>/node_modules/@signozhq/command/dist/command.js',
'^@signozhq/radio-group':
'<rootDir>/node_modules/@signozhq/radio-group/dist/radio-group.js',
},
globals: {
extensionsToTreatAsEsm: ['.ts'],
'ts-jest': {
useESM: true,
isolatedModules: true,
tsconfig: '<rootDir>/tsconfig.jest.json',
},
},
extensionsToTreatAsEsm: ['.ts'],
testMatch: ['<rootDir>/src/**/*?(*.)(test).(ts|js)?(x)'],
preset: 'ts-jest/presets/js-with-ts-esm',
transform: {
'^.+\\.(ts|tsx)?$': 'ts-jest',
'^.+\\.(ts|tsx)?$': [
'ts-jest',
{
useESM: true,
tsconfig: '<rootDir>/tsconfig.jest.json',
},
],
'^.+\\.(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)/)',
'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'],
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
testPathIgnorePatterns: ['/node_modules/', '/public/'],
moduleDirectories: ['node_modules', 'src'],
testEnvironment: 'jest-environment-jsdom',

View File

@@ -1,293 +1,283 @@
{
"name": "frontend",
"version": "1.0.0",
"description": "",
"main": "webpack.config.js",
"scripts": {
"i18n:generate-hash": "node ./i18-generate-hash.js",
"dev": "cross-env NODE_ENV=development webpack serve --progress",
"build": "webpack --config=webpack.config.prod.js --progress",
"prettify": "prettier --write .",
"fmt": "prettier --check .",
"lint": "eslint ./src",
"lint:fix": "eslint ./src --fix",
"jest": "jest",
"jest:coverage": "jest --coverage",
"jest:watch": "jest --watch",
"postinstall": "yarn i18n:generate-hash && (is-ci || yarn husky:configure) && node scripts/update-registry.js",
"husky:configure": "cd .. && husky install frontend/.husky && cd frontend && chmod ug+x .husky/*",
"commitlint": "commitlint --edit $1",
"test": "jest",
"test:changedsince": "jest --changedSince=main --coverage --silent",
"generate:api": "orval --config ./orval.config.ts && sh scripts/post-types-generation.sh",
"generate:permissions-type": "node scripts/generate-permissions-type.js"
},
"engines": {
"node": ">=16.15.0"
},
"author": "",
"license": "ISC",
"dependencies": {
"@ant-design/colors": "6.0.0",
"@ant-design/icons": "4.8.0",
"@codemirror/autocomplete": "6.18.6",
"@codemirror/lang-javascript": "6.2.3",
"@dnd-kit/core": "6.1.0",
"@dnd-kit/modifiers": "7.0.0",
"@dnd-kit/sortable": "8.0.0",
"@dnd-kit/utilities": "3.2.2",
"@grafana/data": "^11.2.3",
"@mdx-js/loader": "2.3.0",
"@mdx-js/react": "2.3.0",
"@monaco-editor/react": "^4.3.1",
"@playwright/test": "1.55.1",
"@radix-ui/react-tabs": "1.0.4",
"@radix-ui/react-tooltip": "1.0.7",
"@sentry/react": "8.41.0",
"@sentry/webpack-plugin": "2.22.6",
"@signozhq/badge": "0.0.2",
"@signozhq/button": "0.0.2",
"@signozhq/calendar": "0.0.0",
"@signozhq/callout": "0.0.2",
"@signozhq/checkbox": "0.0.2",
"@signozhq/combobox": "0.0.2",
"@signozhq/command": "0.0.0",
"@signozhq/design-tokens": "2.1.1",
"@signozhq/icons": "0.1.0",
"@signozhq/input": "0.0.2",
"@signozhq/popover": "0.0.0",
"@signozhq/radio-group": "0.0.2",
"@signozhq/resizable": "0.0.0",
"@signozhq/sonner": "0.1.0",
"@signozhq/switch": "0.0.2",
"@signozhq/table": "0.3.7",
"@signozhq/tooltip": "0.0.2",
"@tanstack/react-table": "8.20.6",
"@tanstack/react-virtual": "3.11.2",
"@uiw/codemirror-theme-copilot": "4.23.11",
"@uiw/codemirror-theme-github": "4.24.1",
"@uiw/react-codemirror": "4.23.10",
"@uiw/react-md-editor": "3.23.5",
"@visx/group": "3.3.0",
"@visx/hierarchy": "3.12.0",
"@visx/shape": "3.5.0",
"@visx/tooltip": "3.3.0",
"@xstate/react": "^3.0.0",
"ansi-to-html": "0.7.2",
"antd": "5.11.0",
"antd-table-saveas-excel": "2.2.1",
"antlr4": "4.13.2",
"axios": "1.12.0",
"babel-eslint": "^10.1.0",
"babel-jest": "^29.6.4",
"babel-loader": "9.1.3",
"babel-plugin-named-asset-import": "^0.3.7",
"babel-preset-minify": "^0.5.1",
"babel-preset-react-app": "^10.0.1",
"chart.js": "3.9.1",
"chartjs-adapter-date-fns": "^2.0.0",
"chartjs-plugin-annotation": "^1.4.0",
"classnames": "2.3.2",
"color": "^4.2.1",
"color-alpha": "1.1.3",
"cross-env": "^7.0.3",
"crypto-js": "4.2.0",
"css-loader": "5.0.0",
"css-minimizer-webpack-plugin": "5.0.1",
"d3-hierarchy": "3.1.2",
"dayjs": "^1.10.7",
"dompurify": "3.2.4",
"dotenv": "8.2.0",
"event-source-polyfill": "1.0.31",
"eventemitter3": "5.0.1",
"file-loader": "6.1.1",
"fontfaceobserver": "2.3.0",
"history": "4.10.1",
"html-webpack-plugin": "5.5.0",
"http-proxy-middleware": "3.0.5",
"http-status-codes": "2.3.0",
"i18next": "^21.6.12",
"i18next-browser-languagedetector": "^6.1.3",
"i18next-http-backend": "^1.3.2",
"immer": "11.1.3",
"jest": "^27.5.1",
"js-base64": "^3.7.2",
"less": "^4.1.2",
"less-loader": "^10.2.0",
"lodash-es": "^4.17.21",
"lucide-react": "0.498.0",
"mini-css-extract-plugin": "2.4.5",
"motion": "12.4.13",
"nuqs": "2.8.8",
"overlayscrollbars": "^2.8.1",
"overlayscrollbars-react": "^0.5.6",
"papaparse": "5.4.1",
"posthog-js": "1.298.0",
"rc-tween-one": "3.0.6",
"react": "18.2.0",
"react-addons-update": "15.6.3",
"react-beautiful-dnd": "13.1.1",
"react-dnd": "16.0.1",
"react-dnd-html5-backend": "16.0.1",
"react-dom": "18.2.0",
"react-drag-listview": "2.0.0",
"react-error-boundary": "4.0.11",
"react-force-graph-2d": "^1.29.1",
"react-full-screen": "1.1.1",
"react-grid-layout": "^1.3.4",
"react-helmet-async": "1.3.0",
"react-i18next": "^11.16.1",
"react-lottie": "1.2.10",
"react-markdown": "8.0.7",
"react-query": "3.39.3",
"react-redux": "^7.2.2",
"react-router-dom": "^5.2.0",
"react-router-dom-v5-compat": "6.27.0",
"react-syntax-highlighter": "15.5.0",
"react-use": "^17.3.2",
"react-virtuoso": "4.0.3",
"redux": "^4.0.5",
"redux-thunk": "^2.3.0",
"rehype-raw": "7.0.0",
"rrule": "2.8.1",
"stream": "^0.0.2",
"style-loader": "1.3.0",
"styled-components": "^5.3.11",
"terser-webpack-plugin": "^5.2.5",
"timestamp-nano": "^1.0.0",
"ts-node": "^10.2.1",
"tsconfig-paths-webpack-plugin": "^3.5.1",
"typescript": "^4.0.5",
"uplot": "1.6.31",
"uuid": "^8.3.2",
"web-vitals": "^0.2.4",
"webpack": "5.94.0",
"webpack-dev-server": "^5.2.1",
"webpack-retry-chunk-load-plugin": "3.1.1",
"xstate": "^4.31.0",
"zustand": "5.0.11"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@babel/core": "^7.22.11",
"@babel/plugin-proposal-class-properties": "^7.18.6",
"@babel/plugin-syntax-jsx": "^7.12.13",
"@babel/preset-env": "^7.22.14",
"@babel/preset-react": "^7.12.13",
"@babel/preset-typescript": "^7.21.4",
"@commitlint/cli": "^16.3.0",
"@commitlint/config-conventional": "^16.2.4",
"@faker-js/faker": "9.3.0",
"@jest/globals": "^27.5.1",
"@testing-library/jest-dom": "5.16.5",
"@testing-library/react": "13.4.0",
"@testing-library/user-event": "14.4.3",
"@types/color": "^3.0.3",
"@types/compression-webpack-plugin": "^9.0.0",
"@types/copy-webpack-plugin": "^8.0.1",
"@types/crypto-js": "4.2.2",
"@types/dompurify": "^2.4.0",
"@types/event-source-polyfill": "^1.0.0",
"@types/fontfaceobserver": "2.1.0",
"@types/jest": "^27.5.1",
"@types/lodash-es": "^4.17.4",
"@types/mini-css-extract-plugin": "^2.5.1",
"@types/node": "^16.10.3",
"@types/papaparse": "5.3.7",
"@types/react": "18.0.26",
"@types/react-addons-update": "0.14.21",
"@types/react-beautiful-dnd": "13.1.8",
"@types/react-dom": "18.0.10",
"@types/react-grid-layout": "^1.1.2",
"@types/react-helmet-async": "1.0.3",
"@types/react-lottie": "1.2.10",
"@types/react-redux": "^7.1.11",
"@types/react-resizable": "3.0.3",
"@types/react-router-dom": "^5.1.6",
"@types/react-syntax-highlighter": "15.5.13",
"@types/redux-mock-store": "1.0.4",
"@types/styled-components": "^5.1.4",
"@types/uuid": "^8.3.1",
"@types/webpack": "^5.28.0",
"@types/webpack-dev-server": "^4.7.2",
"@typescript-eslint/eslint-plugin": "^4.33.0",
"@typescript-eslint/parser": "^4.33.0",
"autoprefixer": "10.4.19",
"babel-plugin-styled-components": "^1.12.0",
"compression-webpack-plugin": "9.0.0",
"copy-webpack-plugin": "^11.0.0",
"eslint": "^7.32.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-import": "^2.28.1",
"eslint-plugin-jest": "^26.9.0",
"eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-react": "^7.24.0",
"eslint-plugin-react-hooks": "^4.3.0",
"eslint-plugin-simple-import-sort": "^7.0.0",
"eslint-plugin-sonarjs": "^0.12.0",
"husky": "^7.0.4",
"image-minimizer-webpack-plugin": "^4.0.0",
"imagemin": "^8.0.1",
"imagemin-svgo": "^10.0.1",
"is-ci": "^3.0.1",
"jest-styled-components": "^7.0.8",
"lint-staged": "^12.5.0",
"msw": "1.3.2",
"npm-run-all": "latest",
"orval": "7.18.0",
"portfinder-sync": "^0.0.2",
"postcss": "8.4.38",
"prettier": "2.2.1",
"prop-types": "15.8.1",
"raw-loader": "4.0.2",
"react-hooks-testing-library": "0.6.0",
"react-hot-loader": "^4.13.0",
"react-resizable": "3.0.4",
"redux-mock-store": "1.5.4",
"sass": "1.66.1",
"sass-loader": "13.3.2",
"sharp": "^0.33.4",
"ts-jest": "^27.1.5",
"ts-node": "^10.2.1",
"typescript-plugin-css-modules": "5.2.0",
"webpack-bundle-analyzer": "^4.5.0",
"webpack-cli": "^5.1.4"
},
"lint-staged": {
"*.(js|jsx|ts|tsx)": [
"eslint --fix",
"sh scripts/typecheck-staged.sh"
]
},
"resolutions": {
"@types/react": "18.0.26",
"@types/react-dom": "18.0.10",
"debug": "4.3.4",
"semver": "7.5.4",
"xml2js": "0.5.0",
"phin": "^3.7.1",
"body-parser": "1.20.3",
"http-proxy-middleware": "3.0.5",
"cross-spawn": "7.0.5",
"cookie": "^0.7.1",
"serialize-javascript": "6.0.2",
"prismjs": "1.30.0",
"got": "11.8.5",
"form-data": "4.0.4",
"brace-expansion": "^2.0.2",
"on-headers": "^1.1.0",
"tmp": "0.2.4"
}
}
"name": "frontend",
"version": "1.0.0",
"description": "",
"type": "module",
"scripts": {
"i18n:generate-hash": "node ./i18-generate-hash.cjs",
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"prettify": "prettier --write .",
"fmt": "prettier --check .",
"lint": "eslint ./src",
"lint:fix": "eslint ./src --fix",
"jest": "jest",
"jest:coverage": "jest --coverage",
"jest:watch": "jest --watch",
"postinstall": "yarn i18n:generate-hash && (is-ci || yarn husky:configure) && node scripts/update-registry.cjs",
"husky:configure": "cd .. && husky install frontend/.husky && cd frontend && chmod ug+x .husky/*",
"commitlint": "commitlint --edit $1",
"test": "jest",
"test:changedsince": "jest --changedSince=main --coverage --silent",
"generate:api": "orval --config ./orval.config.ts && sh scripts/post-types-generation.sh",
"generate:permissions-type": "node scripts/generate-permissions-type.cjs"
},
"engines": {
"node": ">=22.0.0"
},
"author": "",
"license": "ISC",
"dependencies": {
"@ant-design/colors": "6.0.0",
"@ant-design/icons": "4.8.0",
"@codemirror/autocomplete": "6.18.6",
"@codemirror/lang-javascript": "6.2.3",
"@dnd-kit/core": "6.1.0",
"@dnd-kit/modifiers": "7.0.0",
"@dnd-kit/sortable": "8.0.0",
"@dnd-kit/utilities": "3.2.2",
"@grafana/data": "^11.2.3",
"@mdx-js/loader": "2.3.0",
"@mdx-js/react": "2.3.0",
"@monaco-editor/react": "^4.3.1",
"@playwright/test": "1.55.1",
"@radix-ui/react-tabs": "1.0.4",
"@radix-ui/react-tooltip": "1.0.7",
"@sentry/react": "8.41.0",
"@sentry/vite-plugin": "2.22.6",
"@signozhq/badge": "0.0.2",
"@signozhq/button": "0.0.2",
"@signozhq/calendar": "0.0.0",
"@signozhq/callout": "0.0.2",
"@signozhq/checkbox": "0.0.2",
"@signozhq/combobox": "0.0.2",
"@signozhq/command": "0.0.0",
"@signozhq/design-tokens": "2.1.1",
"@signozhq/icons": "0.1.0",
"@signozhq/input": "0.0.2",
"@signozhq/popover": "0.0.0",
"@signozhq/radio-group": "0.0.2",
"@signozhq/resizable": "0.0.0",
"@signozhq/sonner": "0.1.0",
"@signozhq/switch": "0.0.2",
"@signozhq/table": "0.3.7",
"@signozhq/tooltip": "0.0.2",
"@tanstack/react-table": "8.20.6",
"@tanstack/react-virtual": "3.11.2",
"@uiw/codemirror-theme-copilot": "4.23.11",
"@uiw/codemirror-theme-github": "4.24.1",
"@uiw/react-codemirror": "4.23.10",
"@uiw/react-md-editor": "3.23.5",
"@visx/group": "3.3.0",
"@visx/hierarchy": "3.12.0",
"@visx/shape": "3.5.0",
"@visx/tooltip": "3.3.0",
"@vitejs/plugin-react": "5.1.4",
"@xstate/react": "^3.0.0",
"ansi-to-html": "0.7.2",
"antd": "5.11.0",
"antd-table-saveas-excel": "2.2.1",
"antlr4": "4.13.2",
"axios": "1.12.0",
"babel-eslint": "^10.1.0",
"babel-jest": "^29.6.4",
"babel-loader": "9.1.3",
"babel-plugin-named-asset-import": "^0.3.7",
"babel-preset-minify": "^0.5.1",
"babel-preset-react-app": "^10.0.1",
"chart.js": "3.9.1",
"chartjs-adapter-date-fns": "^2.0.0",
"chartjs-plugin-annotation": "^1.4.0",
"classnames": "2.3.2",
"color": "^4.2.1",
"color-alpha": "2.0.0",
"cross-env": "^7.0.3",
"crypto-js": "4.2.0",
"d3-hierarchy": "3.1.2",
"dayjs": "^1.10.7",
"dompurify": "3.2.4",
"dotenv": "8.2.0",
"event-source-polyfill": "1.0.31",
"eventemitter3": "5.0.1",
"fontfaceobserver": "2.3.0",
"history": "4.10.1",
"http-proxy-middleware": "3.0.5",
"http-status-codes": "2.3.0",
"i18next": "^21.6.12",
"i18next-browser-languagedetector": "^6.1.3",
"i18next-http-backend": "^1.3.2",
"immer": "11.1.3",
"jest": "30.2.0",
"js-base64": "^3.7.2",
"lodash-es": "^4.17.21",
"lucide-react": "0.498.0",
"mini-css-extract-plugin": "2.4.5",
"motion": "12.4.13",
"nuqs": "2.8.8",
"overlayscrollbars": "^2.8.1",
"overlayscrollbars-react": "^0.5.6",
"papaparse": "5.4.1",
"posthog-js": "1.298.0",
"rc-tween-one": "3.0.6",
"react": "18.2.0",
"react-addons-update": "15.6.3",
"react-beautiful-dnd": "13.1.1",
"react-dnd": "16.0.1",
"react-dnd-html5-backend": "16.0.1",
"react-dom": "18.2.0",
"react-drag-listview": "2.0.0",
"react-error-boundary": "4.0.11",
"react-force-graph-2d": "^1.29.1",
"react-full-screen": "1.1.1",
"react-grid-layout": "^1.3.4",
"react-helmet-async": "1.3.0",
"react-i18next": "^11.16.1",
"react-lottie": "1.2.10",
"react-markdown": "8.0.7",
"react-query": "3.39.3",
"react-redux": "^7.2.2",
"react-router-dom": "^5.2.0",
"react-router-dom-v5-compat": "6.27.0",
"react-syntax-highlighter": "15.5.0",
"react-use": "^17.3.2",
"react-virtuoso": "4.0.3",
"redux": "^4.0.5",
"redux-thunk": "^2.3.0",
"rehype-raw": "7.0.0",
"rollup-plugin-visualizer": "7.0.0",
"rrule": "2.8.1",
"stream": "^0.0.2",
"styled-components": "^5.3.11",
"timestamp-nano": "^1.0.0",
"ts-node": "^10.2.1",
"typescript": "5.9.3",
"uplot": "1.6.31",
"uuid": "^8.3.2",
"vite": "npm:rolldown-vite@7.3.1",
"vite-plugin-html": "3.2.2",
"web-vitals": "^0.2.4",
"xstate": "^4.31.0",
"zustand": "5.0.11"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@babel/core": "^7.22.11",
"@babel/plugin-proposal-class-properties": "^7.18.6",
"@babel/plugin-syntax-jsx": "^7.12.13",
"@babel/preset-env": "^7.22.14",
"@babel/preset-react": "^7.12.13",
"@babel/preset-typescript": "^7.21.4",
"@commitlint/cli": "^20.4.2",
"@commitlint/config-conventional": "^20.4.2",
"@faker-js/faker": "9.3.0",
"@jest/globals": "30.2.0",
"@testing-library/jest-dom": "5.16.5",
"@testing-library/react": "13.4.0",
"@testing-library/user-event": "14.4.3",
"@types/color": "^3.0.3",
"@types/crypto-js": "4.2.2",
"@types/dompurify": "^2.4.0",
"@types/event-source-polyfill": "^1.0.0",
"@types/fontfaceobserver": "2.1.0",
"@types/jest": "30.0.0",
"@types/lodash-es": "^4.17.4",
"@types/mini-css-extract-plugin": "^2.5.1",
"@types/node": "^16.10.3",
"@types/papaparse": "5.3.7",
"@types/react": "18.0.26",
"@types/react-addons-update": "0.14.21",
"@types/react-beautiful-dnd": "13.1.8",
"@types/react-dom": "18.0.10",
"@types/react-grid-layout": "^1.1.2",
"@types/react-helmet-async": "1.0.3",
"@types/react-lottie": "1.2.10",
"@types/react-redux": "^7.1.11",
"@types/react-resizable": "3.0.3",
"@types/react-router-dom": "^5.1.6",
"@types/react-syntax-highlighter": "15.5.13",
"@types/redux-mock-store": "1.0.4",
"@types/styled-components": "^5.1.4",
"@types/uuid": "^8.3.1",
"@typescript-eslint/eslint-plugin": "^4.33.0",
"@typescript-eslint/parser": "^4.33.0",
"autoprefixer": "10.4.19",
"babel-plugin-styled-components": "^1.12.0",
"eslint": "^7.32.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-import": "^2.28.1",
"eslint-plugin-jest": "^29.15.0",
"eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-react": "^7.24.0",
"eslint-plugin-react-hooks": "^4.3.0",
"eslint-plugin-simple-import-sort": "^7.0.0",
"eslint-plugin-sonarjs": "^0.12.0",
"husky": "^7.0.4",
"imagemin": "^8.0.1",
"imagemin-svgo": "^10.0.1",
"is-ci": "^3.0.1",
"jest-environment-jsdom": "29.7.0",
"jest-environment-node": "29.7.0",
"jest-styled-components": "^7.2.0",
"lint-staged": "^12.5.0",
"msw": "1.3.2",
"npm-run-all": "latest",
"orval": "7.18.0",
"portfinder-sync": "^0.0.2",
"postcss": "8.5.6",
"prettier": "2.2.1",
"prop-types": "15.8.1",
"react-hooks-testing-library": "0.6.0",
"react-resizable": "3.0.4",
"redux-mock-store": "1.5.4",
"sass": "1.97.3",
"sharp": "0.34.5",
"svgo": "4.0.0",
"ts-api-utils": "2.4.0",
"ts-jest": "29.4.6",
"ts-node": "^10.2.1",
"typescript-plugin-css-modules": "5.2.0",
"vite-plugin-checker": "0.12.0",
"vite-plugin-compression": "0.5.1",
"vite-plugin-image-optimizer": "2.0.3",
"vite-tsconfig-paths": "6.1.1"
},
"lint-staged": {
"*.(js|jsx|ts|tsx)": [
"eslint --fix",
"sh scripts/typecheck-staged.sh"
]
},
"resolutions": {
"@types/react": "18.0.26",
"@types/react-dom": "18.0.10",
"debug": "4.3.4",
"semver": "7.5.4",
"xml2js": "0.5.0",
"phin": "^3.7.1",
"body-parser": "1.20.3",
"http-proxy-middleware": "3.0.5",
"cross-spawn": "7.0.5",
"cookie": "^0.7.1",
"serialize-javascript": "6.0.2",
"prismjs": "1.30.0",
"got": "11.8.5",
"form-data": "4.0.4",
"brace-expansion": "^2.0.2",
"on-headers": "^1.1.0",
"tmp": "0.2.4",
"vite": "npm:rolldown-vite@7.3.1"
}
}

View File

@@ -27,7 +27,7 @@ const signozPackages = Object.keys(allDeps).filter((dep) =>
const fileContent = `// -------------------------------------------------------------------------
// AUTO-GENERATED FILE
// -------------------------------------------------------------------------
// This file is generated by scripts/update-registry.js automatically
// This file is generated by scripts/update-registry.cjs automatically
// whenever you run 'yarn install' or 'npm install'.
//
// It forces VS Code to index these specific packages to fix auto-import

View File

@@ -218,9 +218,9 @@ function App(): JSX.Element {
pathname === ROUTES.ONBOARDING ||
pathname.startsWith('/public/dashboard/')
) {
window.Pylon('hideChatBubble');
window.Pylon?.('hideChatBubble');
} else {
window.Pylon('showChatBubble');
window.Pylon?.('showChatBubble');
}
}, [pathname]);

View File

@@ -2,7 +2,7 @@ import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import APIError from 'types/api/error';
// Handles errors from generated API hooks (which use RenderErrorResponseDTO)
// @deprecated Use convertToApiError instead
export function ErrorResponseHandlerForGeneratedAPIs(
error: AxiosError<RenderErrorResponseDTO>,
): never {
@@ -46,3 +46,34 @@ export function ErrorResponseHandlerForGeneratedAPIs(
},
});
}
// convertToApiError converts an AxiosError from generated API
// hooks into an APIError.
export function convertToApiError(
error: AxiosError<RenderErrorResponseDTO> | null,
): APIError | undefined {
if (!error) {
return undefined;
}
const response = error.response;
const errorData = response?.data?.error;
return new APIError({
httpStatusCode: response?.status || error.status || 500,
error: {
code:
errorData?.code ||
String(response?.status || error.code || 'unknown_error'),
message:
errorData?.message ||
response?.statusText ||
error.message ||
'Something went wrong',
url: errorData?.url ?? '',
errors: (errorData?.errors ?? []).map((e) => ({
message: e.message ?? '',
})),
},
});
}

View File

@@ -29,10 +29,6 @@ import type {
UpdateAuthDomainPathParameters,
} from '../sigNoz.schemas';
type AwaitedInput<T> = PromiseLike<T> | T;
type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
/**
* This endpoint lists all auth domains
* @summary List all auth domains

View File

@@ -26,10 +26,6 @@ import type {
RenderErrorResponseDTO,
} from '../sigNoz.schemas';
type AwaitedInput<T> = PromiseLike<T> | T;
type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
/**
* Checks if the authenticated user has permissions for given transactions
* @summary Check permissions

View File

@@ -35,10 +35,6 @@ import type {
UpdatePublicDashboardPathParameters,
} from '../sigNoz.schemas';
type AwaitedInput<T> = PromiseLike<T> | T;
type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
/**
* This endpoint deletes the public sharing config and disables the public sharing of a dashboard
* @summary Delete public dashboard

View File

@@ -18,10 +18,6 @@ import type { ErrorType } from '../../../generatedAPIInstance';
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { GetFeatures200, RenderErrorResponseDTO } from '../sigNoz.schemas';
type AwaitedInput<T> = PromiseLike<T> | T;
type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
/**
* This endpoint returns the supported features and their details
* @summary Get features

View File

@@ -24,10 +24,6 @@ import type {
RenderErrorResponseDTO,
} from '../sigNoz.schemas';
type AwaitedInput<T> = PromiseLike<T> | T;
type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
/**
* This endpoint returns field keys
* @summary Get field keys

View File

@@ -37,10 +37,6 @@ import type {
UpdateIngestionKeyPathParameters,
} from '../sigNoz.schemas';
type AwaitedInput<T> = PromiseLike<T> | T;
type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
/**
* This endpoint returns the ingestion keys for a workspace
* @summary Get ingestion keys for workspace

View File

@@ -21,10 +21,6 @@ import type {
RenderErrorResponseDTO,
} from '../sigNoz.schemas';
type AwaitedInput<T> = PromiseLike<T> | T;
type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
/**
* This endpoint returns global config
* @summary Get global config

View File

@@ -25,10 +25,6 @@ import type {
RenderErrorResponseDTO,
} from '../sigNoz.schemas';
type AwaitedInput<T> = PromiseLike<T> | T;
type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
/**
* This endpoints promotes and indexes paths
* @summary Promote and index paths

View File

@@ -42,10 +42,6 @@ import type {
UpdateMetricMetadataPathParameters,
} from '../sigNoz.schemas';
type AwaitedInput<T> = PromiseLike<T> | T;
type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
/**
* This endpoint returns a list of distinct metric names within the specified time range
* @summary List metric names

View File

@@ -25,10 +25,6 @@ import type {
TypesOrganizationDTO,
} from '../sigNoz.schemas';
type AwaitedInput<T> = PromiseLike<T> | T;
type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
/**
* This endpoint returns the organization I belong to
* @summary Get my organization

View File

@@ -32,10 +32,6 @@ import type {
UpdateUserPreferencePathParameters,
} from '../sigNoz.schemas';
type AwaitedInput<T> = PromiseLike<T> | T;
type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
/**
* This endpoint lists all org preferences
* @summary List org preferences

View File

@@ -20,10 +20,6 @@ import type {
ReplaceVariables200,
} from '../sigNoz.schemas';
type AwaitedInput<T> = PromiseLike<T> | T;
type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
/**
* Execute a composite query over a time range. Supports builder queries (traces, logs, metrics), formulas, trace operators, PromQL, and ClickHouse SQL.
* @summary Query range

View File

@@ -35,10 +35,6 @@ import type {
RoletypesPostableRoleDTO,
} from '../sigNoz.schemas';
type AwaitedInput<T> = PromiseLike<T> | T;
type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
/**
* This endpoint lists all roles
* @summary List roles

View File

@@ -41,10 +41,6 @@ import type {
UpdateServiceAccountStatusPathParameters,
} from '../sigNoz.schemas';
type AwaitedInput<T> = PromiseLike<T> | T;
type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
/**
* This endpoint lists the service accounts for an organisation
* @summary List service accounts

View File

@@ -33,10 +33,6 @@ import type {
RotateSession200,
} from '../sigNoz.schemas';
type AwaitedInput<T> = PromiseLike<T> | T;
type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
/**
* This endpoint creates a session for a user using google callback
* @summary Create session by google callback

View File

@@ -51,10 +51,6 @@ import type {
UpdateUserPathParameters,
} from '../sigNoz.schemas';
type AwaitedInput<T> = PromiseLike<T> | T;
type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
/**
* This endpoint changes the password by id
* @summary Change password

View File

@@ -26,10 +26,6 @@ import type {
ZeustypesPostableProfileDTO,
} from '../sigNoz.schemas';
type AwaitedInput<T> = PromiseLike<T> | T;
type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
/**
* This endpoint gets the host info from zeus.
* @summary Get host info from Zeus.

View File

@@ -1,54 +0,0 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { TreemapViewType } from 'container/MetricsExplorer/Summary/types';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
export interface MetricsTreeMapPayload {
filters: TagFilter;
limit?: number;
treemap?: TreemapViewType;
}
export interface MetricsTreeMapResponse {
status: string;
data: {
[TreemapViewType.TIMESERIES]: TimeseriesData[];
[TreemapViewType.SAMPLES]: SamplesData[];
};
}
export interface TimeseriesData {
percentage: number;
total_value: number;
metric_name: string;
}
export interface SamplesData {
percentage: number;
metric_name: string;
}
export const getMetricsTreeMap = async (
props: MetricsTreeMapPayload,
signal?: AbortSignal,
headers?: Record<string, string>,
): Promise<SuccessResponse<MetricsTreeMapResponse> | ErrorResponse> => {
try {
const response = await axios.post('/metrics/treemap', props, {
signal,
headers,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
params: props,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};

View File

@@ -1,36 +0,0 @@
import axios from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { Temporality } from './getMetricDetails';
import { MetricType } from './getMetricsList';
export interface UpdateMetricMetadataProps {
description: string;
metricType: MetricType;
temporality?: Temporality;
isMonotonic?: boolean;
unit?: string;
}
export interface UpdateMetricMetadataResponse {
success: boolean;
message: string;
}
const updateMetricMetadata = async (
metricName: string,
props: UpdateMetricMetadataProps,
): Promise<SuccessResponse<UpdateMetricMetadataResponse> | ErrorResponse> => {
const response = await axios.post(`/metrics/${metricName}/metadata`, {
...props,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
};
export default updateMetricMetadata;

View File

@@ -1,7 +1,7 @@
// -------------------------------------------------------------------------
// AUTO-GENERATED FILE
// -------------------------------------------------------------------------
// This file is generated by scripts/update-registry.js automatically
// This file is generated by scripts/update-registry.cjs automatically
// whenever you run 'yarn install' or 'npm install'.
//
// It forces VS Code to index these specific packages to fix auto-import

View File

@@ -1,4 +1,4 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`DraggableTableRow Snapshot test should render DraggableTableRow 1`] = `
<DocumentFragment>

View File

@@ -1,4 +1,4 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`Editor renders correctly with custom props 1`] = `
<div>

View File

@@ -1,4 +1,4 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`MessageTip custom action 1`] = `
.c0 {

View File

@@ -1,4 +1,4 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`Not Found page test should render Not Found page without errors 1`] = `
<DocumentFragment>

View File

@@ -49,7 +49,6 @@ export const REACT_QUERY_KEY = {
// Metrics Explorer Query Keys
GET_METRICS_LIST: 'GET_METRICS_LIST',
GET_METRICS_TREE_MAP: 'GET_METRICS_TREE_MAP',
GET_METRICS_LIST_FILTER_KEYS: 'GET_METRICS_LIST_FILTER_KEYS',
GET_METRICS_LIST_FILTER_VALUES: 'GET_METRICS_LIST_FILTER_VALUES',
GET_METRIC_DETAILS: 'GET_METRIC_DETAILS',

View File

@@ -74,7 +74,7 @@ describe('Alert Channels Settings List page', () => {
});
await waitFor(() => {
expect(successNotification).toBeCalledWith({
expect(successNotification).toHaveBeenCalledWith({
message: 'Success',
description: 'channel_delete_success',
});

View File

@@ -179,19 +179,6 @@
&__input.description {
color: var(--text-ink-300);
}
.ant-btn {
color: var(--text-ink-400);
background: var(--bg-vanilla-100);
border: 1px solid var(--bg-vanilla-300);
&:hover,
&:focus {
color: var(--text-ink-500);
background: var(--bg-vanilla-200);
border-color: var(--bg-vanilla-400);
}
}
}
.edit-alert-header {

View File

@@ -15,7 +15,7 @@ export const getRandomColor = (): string => {
};
export const DATASOURCE_VS_ROUTES: Record<DataSource, string> = {
[DataSource.METRICS]: ROUTES.METRICS_EXPLORER,
[DataSource.METRICS]: ROUTES.METRICS_EXPLORER_EXPLORER,
[DataSource.TRACES]: ROUTES.TRACES_EXPLORER,
[DataSource.LOGS]: ROUTES.LOGS_EXPLORER,
};

View File

@@ -39,7 +39,7 @@ const mockProps: WidgetGraphComponentProps = {
columnUnits: {},
description: '',
fillSpans: false,
id: '17f905f6-d355-46bd-a78e-cbc87e6f58cc',
id: 'w-17f905f6-d355-46bd-a78e-cbc87e6f58cc',
mergeAllActiveQueries: false,
nullZeroValues: 'zero',
opacity: '1',

View File

@@ -190,6 +190,11 @@
.ant-table-cell:nth-child(n + 3) {
padding-right: 24px;
}
.status-header {
display: flex;
align-items: center;
gap: 4px;
}
.memory-usage-header {
display: flex;
align-items: center;

View File

@@ -146,7 +146,14 @@ export const getHostsListColumns = (): ColumnType<HostRowData>[] => [
),
},
{
title: 'Status',
title: (
<div className="status-header">
Status
<Tooltip title="Sent system metrics in last 10 mins">
<InfoCircleOutlined />
</Tooltip>
</div>
),
dataIndex: 'active',
key: 'active',
width: 100,

View File

@@ -12,14 +12,21 @@ import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interface
import DateTimeSelector from 'container/TopNav/DateTimeSelectionV2';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
import {
ICurrentQueryData,
useHandleExplorerTabChange,
} from 'hooks/useHandleExplorerTabChange';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { isEmpty } from 'lodash-es';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import { ExplorerViews } from 'pages/LogsExplorer/utils';
import { Warning } from 'types/api';
import { Dashboard } from 'types/api/dashboard/getAll';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { MetricAggregation } from 'types/api/v5/queryRange';
import { DataSource } from 'types/common/queryBuilder';
import { generateExportToDashboardLink } from 'utils/dashboard/generateExportToDashboardLink';
import { explorerViewToPanelType } from 'utils/explorerUtils';
import { v4 as uuid } from 'uuid';
import { MetricsExplorerEventKeys, MetricsExplorerEvents } from '../events';
@@ -42,15 +49,20 @@ function Explorer(): JSX.Element {
stagedQuery,
updateAllQueriesOperators,
currentQuery,
handleSetConfig,
} = useQueryBuilder();
const { safeNavigate } = useSafeNavigate();
const { handleExplorerTabChange } = useHandleExplorerTabChange();
const [isMetricDetailsOpen, setIsMetricDetailsOpen] = useState(false);
const metricNames = useMemo(() => {
const currentMetricNames: string[] = [];
stagedQuery?.builder.queryData.forEach((query) => {
if (query.aggregateAttribute?.key) {
currentMetricNames.push(query.aggregateAttribute?.key);
const metricName =
query.aggregateAttribute?.key ||
(query.aggregations?.[0] as MetricAggregation | undefined)?.metricName;
if (metricName) {
currentMetricNames.push(metricName);
}
});
return currentMetricNames;
@@ -176,6 +188,16 @@ function Explorer(): JSX.Element {
useShareBuilderUrl({ defaultValue: defaultQuery });
const handleChangeSelectedView = useCallback(
(view: ExplorerViews, querySearchParameters?: ICurrentQueryData): void => {
const nextPanelType =
explorerViewToPanelType[view] || PANEL_TYPES.TIME_SERIES;
handleSetConfig(nextPanelType, DataSource.METRICS);
handleExplorerTabChange(nextPanelType, querySearchParameters);
},
[handleSetConfig, handleExplorerTabChange],
);
const handleExport = useCallback(
(
dashboard: Dashboard | null,
@@ -348,6 +370,7 @@ function Explorer(): JSX.Element {
onExport={handleExport}
isOneChartPerQuery={showOneChartPerQuery}
splitedQueries={splitedQueries}
handleChangeSelectedView={handleChangeSelectedView}
/>
{isMetricDetailsOpen && selectedMetricName && (
<MetricDetails

View File

@@ -12,6 +12,7 @@ import { initialQueriesMap } from 'constants/queryBuilder';
import * as useOptionsMenuHooks from 'container/OptionsMenu';
import * as useUpdateDashboardHooks from 'hooks/dashboard/useUpdateDashboard';
import * as useQueryBuilderHooks from 'hooks/queryBuilder/useQueryBuilder';
import * as useHandleExplorerTabChangeHooks from 'hooks/useHandleExplorerTabChange';
import * as appContextHooks from 'providers/App/App';
import { ErrorModalProvider } from 'providers/ErrorModalProvider';
import * as timezoneHooks from 'providers/Timezone';
@@ -29,6 +30,8 @@ const queryClient = new QueryClient();
const mockUpdateAllQueriesOperators = jest
.fn()
.mockReturnValue(initialQueriesMap[DataSource.METRICS]);
const mockHandleSetConfig = jest.fn();
const mockHandleExplorerTabChange = jest.fn();
const mockUseQueryBuilderData = {
handleRunQuery: jest.fn(),
stagedQuery: initialQueriesMap[DataSource.METRICS],
@@ -40,7 +43,7 @@ const mockUseQueryBuilderData = {
handleSetQueryData: jest.fn(),
handleSetFormulaData: jest.fn(),
handleSetQueryItemData: jest.fn(),
handleSetConfig: jest.fn(),
handleSetConfig: mockHandleSetConfig,
removeQueryBuilderEntityByIndex: jest.fn(),
removeQueryTypeItemByIndex: jest.fn(),
isDefaultQuery: jest.fn(),
@@ -135,6 +138,11 @@ jest.spyOn(appContextHooks, 'useAppContext').mockReturnValue({
jest.spyOn(useQueryBuilderHooks, 'useQueryBuilder').mockReturnValue({
...mockUseQueryBuilderData,
} as any);
jest
.spyOn(useHandleExplorerTabChangeHooks, 'useHandleExplorerTabChange')
.mockReturnValue({
handleExplorerTabChange: mockHandleExplorerTabChange,
});
const Y_AXIS_UNIT_SELECTOR_TEST_ID = 'y-axis-unit-selector';
@@ -382,4 +390,109 @@ describe('Explorer', () => {
expect(oneChartPerQueryToggle).toBeEnabled();
expect(oneChartPerQueryToggle).not.toBeChecked();
});
describe('loading saved views with v5 query format', () => {
const EMPTY_STATE_TEXT = 'Select a metric and run a query to see the results';
it('should show empty state when no metric is selected', () => {
(useSearchParams as jest.Mock).mockReturnValue([
new URLSearchParams({}),
mockSetSearchParams,
]);
jest.spyOn(useGetMetricsHooks, 'useGetMetrics').mockReturnValue({
isLoading: false,
isError: false,
metrics: [],
});
jest.spyOn(useQueryBuilderHooks, 'useQueryBuilder').mockReturnValue({
...mockUseQueryBuilderData,
} as any);
renderExplorer();
expect(screen.getByText(EMPTY_STATE_TEXT)).toBeInTheDocument();
});
it('should not show empty state when saved view has v5 aggregations format', () => {
(useSearchParams as jest.Mock).mockReturnValue([
new URLSearchParams({}),
mockSetSearchParams,
]);
jest.spyOn(useGetMetricsHooks, 'useGetMetrics').mockReturnValue({
isLoading: false,
isError: false,
metrics: [MOCK_METRIC_METADATA],
});
// saved view loaded back from v5 format
// aggregateAttribute.key is empty (lost in v3/v4 -> v5 -> v3/v4 round trip)
// but aggregations[0].metricName has metric name
// TODO(srikanthccv): remove this mess
const mockQueryData = {
...initialQueriesMap[DataSource.METRICS].builder.queryData[0],
aggregateAttribute: {
...(initialQueriesMap[DataSource.METRICS].builder.queryData[0]
.aggregateAttribute as BaseAutocompleteData),
key: '',
},
aggregations: [
{
metricName: 'http_requests_total',
temporality: 'cumulative',
timeAggregation: 'rate',
spaceAggregation: 'sum',
},
],
};
jest.spyOn(useQueryBuilderHooks, 'useQueryBuilder').mockReturnValue(({
...mockUseQueryBuilderData,
stagedQuery: {
...initialQueriesMap[DataSource.METRICS],
builder: {
...initialQueriesMap[DataSource.METRICS].builder,
queryData: [mockQueryData],
},
},
} as Partial<QueryBuilderContextType>) as QueryBuilderContextType);
renderExplorer();
expect(screen.queryByText(EMPTY_STATE_TEXT)).not.toBeInTheDocument();
});
it('should not show empty state when query uses v3 aggregateAttribute format', () => {
(useSearchParams as jest.Mock).mockReturnValue([
new URLSearchParams({}),
mockSetSearchParams,
]);
jest.spyOn(useGetMetricsHooks, 'useGetMetrics').mockReturnValue({
isLoading: false,
isError: false,
metrics: [MOCK_METRIC_METADATA],
});
const mockQueryData = {
...initialQueriesMap[DataSource.METRICS].builder.queryData[0],
aggregateAttribute: {
...(initialQueriesMap[DataSource.METRICS].builder.queryData[0]
.aggregateAttribute as BaseAutocompleteData),
key: 'system_cpu_usage',
},
};
jest.spyOn(useQueryBuilderHooks, 'useQueryBuilder').mockReturnValue(({
...mockUseQueryBuilderData,
stagedQuery: {
...initialQueriesMap[DataSource.METRICS],
builder: {
...initialQueriesMap[DataSource.METRICS].builder,
queryData: [mockQueryData],
},
},
} as Partial<QueryBuilderContextType>) as QueryBuilderContextType);
renderExplorer();
expect(screen.queryByText(EMPTY_STATE_TEXT)).not.toBeInTheDocument();
});
});
});

View File

@@ -9,7 +9,6 @@ import {
} from 'api/generated/services/metrics';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import useUrlQuery from 'hooks/useUrlQuery';
import { Bell, Grid } from 'lucide-react';
import { pluralize } from 'utils/pluralize';
@@ -18,8 +17,6 @@ import { DashboardsAndAlertsPopoverProps } from './types';
function DashboardsAndAlertsPopover({
metricName,
}: DashboardsAndAlertsPopoverProps): JSX.Element | null {
const params = useUrlQuery();
const {
data: alertsData,
isLoading: isLoadingAlerts,
@@ -71,8 +68,10 @@ function DashboardsAndAlertsPopover({
<Typography.Link
key={alert.alertId}
onClick={(): void => {
params.set(QueryParams.ruleId, alert.alertId);
window.open(`${ROUTES.ALERT_OVERVIEW}?${params.toString()}`, '_blank');
window.open(
`${ROUTES.ALERT_OVERVIEW}?${QueryParams.ruleId}=${alert.alertId}`,
'_blank',
);
}}
className="dashboards-popover-content-item"
>
@@ -82,7 +81,7 @@ function DashboardsAndAlertsPopover({
}));
}
return null;
}, [alerts, params]);
}, [alerts]);
const dashboardsPopoverContent = useMemo(() => {
if (dashboards && dashboards.length > 0) {

View File

@@ -16,15 +16,6 @@ import {
const mockWindowOpen = jest.fn();
Object.defineProperty(window, 'open', { value: mockWindowOpen });
const mockSetQuery = jest.fn();
const mockUrlQuery = {
set: mockSetQuery,
toString: jest.fn(),
};
jest.mock('hooks/useUrlQuery', () => ({
__esModule: true,
default: jest.fn(() => mockUrlQuery),
}));
const useGetMetricAlertsMock = jest.spyOn(
metricsExplorerHooks,
@@ -156,12 +147,10 @@ describe('DashboardsAndAlertsPopover', () => {
// Click on the first alert rule
await userEvent.click(screen.getByText(MOCK_ALERT_1.alertName));
// Should open alert in new tab
expect(mockSetQuery).toHaveBeenCalledWith(
QueryParams.ruleId,
MOCK_ALERT_1.alertId,
expect(mockWindowOpen).toHaveBeenCalledWith(
`/alerts/overview?${QueryParams.ruleId}=${MOCK_ALERT_1.alertId}`,
'_blank',
);
expect(mockWindowOpen).toHaveBeenCalled();
});
it('renders unique dashboards even when there are duplicates', async () => {

View File

@@ -1,76 +0,0 @@
import { useMemo } from 'react';
import { Color } from '@signozhq/design-tokens';
import { Typography } from 'antd';
import { MetricType } from 'api/metricsExplorer/getMetricsList';
import {
BarChart,
BarChart2,
BarChartHorizontal,
Diff,
Gauge,
} from 'lucide-react';
import { METRIC_TYPE_LABEL_MAP } from './constants';
// TODO: @amlannandy Delete this component after API migration is complete
function MetricTypeRenderer({ type }: { type: MetricType }): JSX.Element {
const [icon, color] = useMemo(() => {
switch (type) {
case MetricType.SUM:
return [
<Diff key={type} size={12} color={Color.BG_ROBIN_500} />,
Color.BG_ROBIN_500,
];
case MetricType.GAUGE:
return [
<Gauge key={type} size={12} color={Color.BG_SAKURA_500} />,
Color.BG_SAKURA_500,
];
case MetricType.HISTOGRAM:
return [
<BarChart2 key={type} size={12} color={Color.BG_SIENNA_500} />,
Color.BG_SIENNA_500,
];
case MetricType.SUMMARY:
return [
<BarChartHorizontal key={type} size={12} color={Color.BG_FOREST_500} />,
Color.BG_FOREST_500,
];
case MetricType.EXPONENTIAL_HISTOGRAM:
return [
<BarChart key={type} size={12} color={Color.BG_AQUA_500} />,
Color.BG_AQUA_500,
];
default:
return [null, ''];
}
}, [type]);
const metricTypeRendererStyle = useMemo(
() => ({
backgroundColor: `${color}33`,
border: `1px solid ${color}`,
color,
}),
[color],
);
const metricTypeRendererTextStyle = useMemo(
() => ({
color,
fontSize: 12,
}),
[color],
);
return (
<div className="metric-type-renderer" style={metricTypeRendererStyle}>
{icon}
<Typography.Text style={metricTypeRendererTextStyle}>
{METRIC_TYPE_LABEL_MAP[type]}
</Typography.Text>
</div>
);
}
export default MetricTypeRenderer;

View File

@@ -47,7 +47,7 @@ function MetricsSearch({
}}
onRun={handleRunQuery}
showFilterSuggestionsWithoutMetric
placeholder="Try metric_name CONTAINS 'http.server' to view all HTTP Server metrics being sent"
placeholder="Search your metrics. Try service.name='api' to see all API service metrics, or http.client for HTTP client metrics."
/>
</div>
<RunQueryBtn

View File

@@ -10,6 +10,7 @@ import {
} from 'antd';
import { SorterResult } from 'antd/es/table/interface';
import { Querybuildertypesv5OrderDirectionDTO } from 'api/generated/services/sigNoz.schemas';
import ErrorInPlace from 'components/ErrorInPlace/ErrorInPlace';
import { Info } from 'lucide-react';
import { MetricsListItemRowData, MetricsTableProps } from './types';
@@ -18,6 +19,7 @@ import { getMetricsTableColumns } from './utils';
function MetricsTable({
isLoading,
isError,
error,
data,
pageSize,
currentPage,
@@ -71,54 +73,54 @@ function MetricsTable({
<Info size={16} />
</Tooltip>
</div>
<Table
loading={{
spinning: isLoading,
indicator: (
<Spin
data-testid="metrics-table-loading-state"
indicator={<LoadingOutlined size={14} spin />}
/>
),
}}
dataSource={data}
columns={getMetricsTableColumns(queryFilterExpression, onFilterChange)}
locale={{
emptyText: isLoading ? null : (
<div
className="no-metrics-message-container"
data-testid={
isError ? 'metrics-table-error-state' : 'metrics-table-empty-state'
}
>
<img
src="/Icons/emptyState.svg"
alt="thinking-emoji"
className="empty-state-svg"
{isError && error ? (
<ErrorInPlace error={error} />
) : (
<Table
loading={{
spinning: isLoading,
indicator: (
<Spin
data-testid="metrics-table-loading-state"
indicator={<LoadingOutlined size={14} spin />}
/>
<Typography.Text className="no-metrics-message">
{isError
? 'Error fetching metrics. If the problem persists, please contact support.'
: 'This query had no results. Edit your query and try again!'}
</Typography.Text>
</div>
),
}}
tableLayout="fixed"
onChange={handleTableChange}
pagination={{
current: currentPage,
pageSize,
showSizeChanger: true,
hideOnSinglePage: false,
onChange: onPaginationChange,
total: totalCount,
}}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => openMetricDetails(record.key, 'list'),
className: 'clickable-row',
})}
/>
),
}}
dataSource={data}
columns={getMetricsTableColumns(queryFilterExpression, onFilterChange)}
locale={{
emptyText: isLoading ? null : (
<div
className="no-metrics-message-container"
data-testid="metrics-table-empty-state"
>
<img
src="/Icons/emptyState.svg"
alt="thinking-emoji"
className="empty-state-svg"
/>
<Typography.Text className="no-metrics-message">
This query had no results. Edit your query and try again!
</Typography.Text>
</div>
),
}}
tableLayout="fixed"
onChange={handleTableChange}
pagination={{
current: currentPage,
pageSize,
showSizeChanger: true,
hideOnSinglePage: false,
onChange: onPaginationChange,
total: totalCount,
}}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => openMetricDetails(record.key, 'list'),
className: 'clickable-row',
})}
/>
)}
</div>
);
}

View File

@@ -4,6 +4,7 @@ import { Group } from '@visx/group';
import { Treemap } from '@visx/hierarchy';
import { Empty, Select, Skeleton, Tooltip, Typography } from 'antd';
import { MetricsexplorertypesTreemapModeDTO } from 'api/generated/services/sigNoz.schemas';
import ErrorInPlace from 'components/ErrorInPlace/ErrorInPlace';
import { HierarchyNode, stratify, treemapBinary } from 'd3-hierarchy';
import { Info } from 'lucide-react';
@@ -27,6 +28,7 @@ import {
function MetricsTreemapInternal({
isLoading,
isError,
error,
data,
viewType,
openMetricDetails,
@@ -91,6 +93,10 @@ function MetricsTreemapInternal({
);
}
if (isError && error) {
return <ErrorInPlace error={error} />;
}
if (isError) {
return (
<Empty
@@ -174,6 +180,7 @@ function MetricsTreemap({
data,
isLoading,
isError,
error,
openMetricDetails,
setHeatmapView,
}: MetricsTreemapProps): JSX.Element {
@@ -202,6 +209,7 @@ function MetricsTreemap({
<MetricsTreemapInternal
isLoading={isLoading}
isError={isError}
error={error}
data={data}
viewType={viewType}
openMetricDetails={openMetricDetails}

View File

@@ -4,6 +4,7 @@ import { useSelector } from 'react-redux';
import { useSearchParams } from 'react-router-dom-v5-compat';
import * as Sentry from '@sentry/react';
import logEvent from 'api/common/logEvent';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import {
useGetMetricsStats,
useGetMetricsTreemap,
@@ -63,13 +64,20 @@ function Summary(): JSX.Element {
MetricsexplorertypesTreemapModeDTO.samples,
);
const { currentQuery, redirectWithQueryBuilderData } = useQueryBuilder();
const {
currentQuery,
stagedQuery,
redirectWithQueryBuilderData,
} = useQueryBuilder();
useShareBuilderUrl({ defaultValue: initialQueriesMap[DataSource.METRICS] });
const query = useMemo(() => currentQuery?.builder?.queryData[0], [
currentQuery,
]);
const query = useMemo(
() =>
stagedQuery?.builder?.queryData?.[0] ||
initialQueriesMap[DataSource.METRICS].builder.queryData[0],
[stagedQuery],
);
const [searchParams, setSearchParams] = useSearchParams();
const [isMetricDetailsOpen, setIsMetricDetailsOpen] = useState(
@@ -86,14 +94,16 @@ function Summary(): JSX.Element {
(state) => state.globalTime,
);
const appliedFilterExpression = query?.filter?.expression || '';
const [
currentQueryFilterExpression,
setCurrentQueryFilterExpression,
] = useState<string>(query?.filter?.expression || '');
] = useState<string>(appliedFilterExpression);
const [appliedFilterExpression, setAppliedFilterExpression] = useState(
query?.filter?.expression || '',
);
useEffect(() => {
setCurrentQueryFilterExpression(appliedFilterExpression);
}, [appliedFilterExpression]);
const queryFilterExpression = useMemo(
() => ({ expression: appliedFilterExpression }),
@@ -150,6 +160,7 @@ function Summary(): JSX.Element {
mutate: getMetricsStats,
isLoading: isGetMetricsStatsLoading,
isError: isGetMetricsStatsError,
error: metricsStatsError,
} = useGetMetricsStats();
const {
@@ -157,8 +168,19 @@ function Summary(): JSX.Element {
mutate: getMetricsTreemap,
isLoading: isGetMetricsTreemapLoading,
isError: isGetMetricsTreemapError,
error: metricsTreemapError,
} = useGetMetricsTreemap();
const metricsStatsApiError = useMemo(
() => convertToApiError(metricsStatsError),
[metricsStatsError],
);
const metricsTreemapApiError = useMemo(
() => convertToApiError(metricsTreemapError),
[metricsTreemapError],
);
useEffect(() => {
getMetricsStats({
data: metricsListQuery,
@@ -192,8 +214,6 @@ function Summary(): JSX.Element {
],
},
});
setCurrentQueryFilterExpression(expression);
setAppliedFilterExpression(expression);
setCurrentPage(1);
if (expression) {
logEvent(MetricsExplorerEvents.FilterApplied, {
@@ -290,10 +310,14 @@ function Summary(): JSX.Element {
};
const isMetricsListDataEmpty =
formattedMetricsData.length === 0 && !isGetMetricsStatsLoading;
formattedMetricsData.length === 0 &&
!isGetMetricsStatsLoading &&
!isGetMetricsStatsError;
const isMetricsTreeMapDataEmpty =
!treeMapData?.data[heatmapView]?.length && !isGetMetricsTreemapLoading;
!treeMapData?.data[heatmapView]?.length &&
!isGetMetricsTreemapLoading &&
!isGetMetricsTreemapError;
const showFullScreenLoading =
(isGetMetricsStatsLoading || isGetMetricsTreemapLoading) &&
@@ -322,6 +346,7 @@ function Summary(): JSX.Element {
data={treeMapData?.data}
isLoading={isGetMetricsTreemapLoading}
isError={isGetMetricsTreemapError}
error={metricsTreemapApiError}
viewType={heatmapView}
openMetricDetails={openMetricDetails}
setHeatmapView={handleSetHeatmapView}
@@ -329,6 +354,7 @@ function Summary(): JSX.Element {
<MetricsTable
isLoading={isGetMetricsStatsLoading}
isError={isGetMetricsStatsError}
error={metricsStatsApiError}
data={formattedMetricsData}
pageSize={pageSize}
currentPage={currentPage}

View File

@@ -1,63 +0,0 @@
import { Color } from '@signozhq/design-tokens';
import { render, screen } from '@testing-library/react';
import { MetricType } from 'api/metricsExplorer/getMetricsList';
import MetricTypeRenderer from '../MetricTypeRenderer';
jest.mock('lucide-react', () => {
return {
__esModule: true,
Diff: (): JSX.Element => <svg data-testid="diff-icon" />,
Gauge: (): JSX.Element => <svg data-testid="gauge-icon" />,
BarChart2: (): JSX.Element => <svg data-testid="bar-chart-2-icon" />,
BarChartHorizontal: (): JSX.Element => (
<svg data-testid="bar-chart-horizontal-icon" />
),
BarChart: (): JSX.Element => <svg data-testid="bar-chart-icon" />,
};
});
describe('MetricTypeRenderer', () => {
it('should render correct icon and color for each metric type', () => {
const types = [
{
type: MetricType.SUM,
color: Color.BG_ROBIN_500,
iconTestId: 'diff-icon',
},
{
type: MetricType.GAUGE,
color: Color.BG_SAKURA_500,
iconTestId: 'gauge-icon',
},
{
type: MetricType.HISTOGRAM,
color: Color.BG_SIENNA_500,
iconTestId: 'bar-chart-2-icon',
},
{
type: MetricType.SUMMARY,
color: Color.BG_FOREST_500,
iconTestId: 'bar-chart-horizontal-icon',
},
{
type: MetricType.EXPONENTIAL_HISTOGRAM,
color: Color.BG_AQUA_500,
iconTestId: 'bar-chart-icon',
},
];
types.forEach(({ type, color, iconTestId }) => {
const { container } = render(<MetricTypeRenderer type={type} />);
const rendererDiv = container.firstChild as HTMLElement;
expect(rendererDiv).toHaveStyle({
backgroundColor: `${color}33`,
border: `1px solid ${color}`,
color,
});
expect(screen.getByTestId(iconTestId)).toBeInTheDocument();
});
});
});

View File

@@ -6,6 +6,7 @@ import { Filter } from 'api/v5/v5';
import * as useGetMetricsListFilterValues from 'hooks/metricsExplorer/useGetMetricsListFilterValues';
import * as useQueryBuilderOperationsHooks from 'hooks/queryBuilder/useQueryBuilderOperations';
import store from 'store';
import APIError from 'types/api/error';
import MetricsTable from '../MetricsTable';
import { MetricsListItemRowData } from '../types';
@@ -119,12 +120,23 @@ describe('MetricsTable', () => {
});
it('shows error state', () => {
const mockError = new APIError({
httpStatusCode: 400,
error: {
code: '400',
message: 'invalid filter expression',
url: '',
errors: [],
},
});
render(
<MemoryRouter>
<Provider store={store}>
<MetricsTable
isLoading={false}
isError
error={mockError}
data={[]}
pageSize={10}
currentPage={1}
@@ -139,12 +151,8 @@ describe('MetricsTable', () => {
</MemoryRouter>,
);
expect(screen.getByTestId('metrics-table-error-state')).toBeInTheDocument();
expect(
screen.getByText(
'Error fetching metrics. If the problem persists, please contact support.',
),
).toBeInTheDocument();
expect(screen.getByText('400')).toBeInTheDocument();
expect(screen.getByText('invalid filter expression')).toBeInTheDocument();
});
it('shows empty state when no data', () => {

View File

@@ -1,16 +1,12 @@
import { QueryClient, QueryClientProvider } from 'react-query';
// eslint-disable-next-line no-restricted-imports
import { Provider } from 'react-redux';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { MetricType } from 'api/metricsExplorer/getMetricsList';
import * as metricsHooks from 'api/generated/services/metrics';
import { initialQueriesMap } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import * as useGetMetricsListHooks from 'hooks/metricsExplorer/useGetMetricsList';
import * as useGetMetricsTreeMapHooks from 'hooks/metricsExplorer/useGetMetricsTreeMap';
import store from 'store';
import { render, screen } from 'tests/test-utils';
import * as useQueryBuilderHooks from 'hooks/queryBuilder/useQueryBuilder';
import { render, screen, waitFor } from 'tests/test-utils';
import { DataSource, QueryBuilderContextType } from 'types/common/queryBuilder';
import Summary from '../Summary';
import { TreemapViewType } from '../types';
jest.mock('d3-hierarchy', () => ({
stratify: jest.fn().mockReturnValue({
@@ -44,58 +40,135 @@ jest.mock('react-router-dom', () => ({
pathname: `${ROUTES.METRICS_EXPLORER_BASE}`,
}),
}));
jest.mock('hooks/queryBuilder/useShareBuilderUrl', () => ({
useShareBuilderUrl: jest.fn(),
}));
// so filter expression assertions easy
jest.mock('../MetricsSearch', () => {
return function MockMetricsSearch(props: {
currentQueryFilterExpression: string;
}): JSX.Element {
return (
<div data-testid="metrics-search-expression">
{props.currentQueryFilterExpression}
</div>
);
};
});
const queryClient = new QueryClient();
const mockMetricName = 'test-metric';
jest.spyOn(useGetMetricsListHooks, 'useGetMetricsList').mockReturnValue({
data: {
payload: {
status: 'success',
data: {
metrics: [
{
metric_name: mockMetricName,
description: 'description for a test metric',
type: MetricType.GAUGE,
unit: 'count',
lastReceived: '1715702400',
[TreemapViewType.TIMESERIES]: 100,
[TreemapViewType.SAMPLES]: 100,
},
],
},
},
},
isError: false,
isLoading: false,
} as any);
jest.spyOn(useGetMetricsTreeMapHooks, 'useGetMetricsTreeMap').mockReturnValue({
data: {
payload: {
status: 'success',
data: {
[TreemapViewType.TIMESERIES]: [
{
metric_name: mockMetricName,
percentage: 100,
total_value: 100,
},
],
[TreemapViewType.SAMPLES]: [
{
metric_name: mockMetricName,
percentage: 100,
},
],
},
},
},
isError: false,
isLoading: false,
} as any);
const mockSetSearchParams = jest.fn();
const mockGetMetricsStats = jest.fn();
const mockGetMetricsTreemap = jest.fn();
const mockUseQueryBuilderData = {
handleRunQuery: jest.fn(),
stagedQuery: initialQueriesMap[DataSource.METRICS],
updateAllQueriesOperators: jest.fn(),
currentQuery: initialQueriesMap[DataSource.METRICS],
resetQuery: jest.fn(),
redirectWithQueryBuilderData: jest.fn(),
isStagedQueryUpdated: jest.fn(),
handleSetQueryData: jest.fn(),
handleSetFormulaData: jest.fn(),
handleSetQueryItemData: jest.fn(),
handleSetConfig: jest.fn(),
removeQueryBuilderEntityByIndex: jest.fn(),
removeQueryTypeItemByIndex: jest.fn(),
isDefaultQuery: jest.fn(),
};
const useGetMetricsStatsSpy = jest.spyOn(metricsHooks, 'useGetMetricsStats');
const useGetMetricsTreemapSpy = jest.spyOn(
metricsHooks,
'useGetMetricsTreemap',
);
const useQueryBuilderSpy = jest.spyOn(useQueryBuilderHooks, 'useQueryBuilder');
describe('Summary', () => {
beforeEach(() => {
jest.clearAllMocks();
(useSearchParams as jest.Mock).mockReturnValue([
new URLSearchParams(),
mockSetSearchParams,
]);
useGetMetricsStatsSpy.mockReturnValue({
data: null,
mutate: mockGetMetricsStats,
isLoading: true,
isError: false,
error: null,
isIdle: true,
isSuccess: false,
reset: jest.fn(),
status: 'idle',
} as any);
useGetMetricsTreemapSpy.mockReturnValue({
data: null,
mutate: mockGetMetricsTreemap,
isLoading: true,
isError: false,
error: null,
isIdle: true,
isSuccess: false,
reset: jest.fn(),
status: 'idle',
} as any);
useQueryBuilderSpy.mockReturnValue(({
...mockUseQueryBuilderData,
} as Partial<QueryBuilderContextType>) as QueryBuilderContextType);
});
it('does not carry filter expression from a previous page', async () => {
const staleFilterExpression = "service.name = 'redis'";
// prev filter from logs explorer
const staleQuery = {
...initialQueriesMap[DataSource.METRICS],
builder: {
...initialQueriesMap[DataSource.METRICS].builder,
queryData: [
{
...initialQueriesMap[DataSource.METRICS].builder.queryData[0],
filter: { expression: staleFilterExpression },
},
],
},
};
// stagedQuery has stale filter (before QueryBuilder resets it)
useQueryBuilderSpy.mockReturnValue(({
...mockUseQueryBuilderData,
stagedQuery: staleQuery,
currentQuery: staleQuery,
} as Partial<QueryBuilderContextType>) as QueryBuilderContextType);
const { rerender } = render(<Summary />);
expect(screen.getByTestId('metrics-search-expression')).toHaveTextContent(
staleFilterExpression,
);
// QB route change effect resets stagedQuery to null
useQueryBuilderSpy.mockReturnValue(({
...mockUseQueryBuilderData,
stagedQuery: null,
currentQuery: initialQueriesMap[DataSource.METRICS],
} as Partial<QueryBuilderContextType>) as QueryBuilderContextType);
rerender(<Summary />);
await waitFor(() => {
expect(
screen.getByTestId('metrics-search-expression'),
).toBeEmptyDOMElement();
});
});
it('persists inspect modal open state across page refresh', () => {
(useSearchParams as jest.Mock).mockReturnValue([
new URLSearchParams({
@@ -105,13 +178,7 @@ describe('Summary', () => {
mockSetSearchParams,
]);
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<Summary />
</Provider>
</QueryClientProvider>,
);
render(<Summary />);
expect(screen.queryByText('Proportion View')).not.toBeInTheDocument();
});
@@ -120,18 +187,12 @@ describe('Summary', () => {
(useSearchParams as jest.Mock).mockReturnValue([
new URLSearchParams({
isMetricDetailsOpen: 'true',
selectedMetricName: mockMetricName,
selectedMetricName: 'test-metric',
}),
mockSetSearchParams,
]);
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<Summary />
</Provider>
</QueryClientProvider>,
);
render(<Summary />);
expect(screen.queryByText('Proportion View')).not.toBeInTheDocument();
});

View File

@@ -2,7 +2,6 @@ import {
MetricsexplorertypesTreemapModeDTO,
MetrictypesTypeDTO,
} from 'api/generated/services/sigNoz.schemas';
import { MetricType } from 'api/metricsExplorer/getMetricsList';
export const METRICS_TABLE_PAGE_SIZE = 10;
@@ -19,15 +18,6 @@ export const TREEMAP_SQUARE_PADDING = 5;
export const TREEMAP_MARGINS = { TOP: 10, LEFT: 10, RIGHT: 10, BOTTOM: 10 };
// TODO: Remove this once API migration is complete
export const METRIC_TYPE_LABEL_MAP = {
[MetricType.SUM]: 'Sum',
[MetricType.GAUGE]: 'Gauge',
[MetricType.HISTOGRAM]: 'Histogram',
[MetricType.SUMMARY]: 'Summary',
[MetricType.EXPONENTIAL_HISTOGRAM]: 'Exp. Histogram',
};
export const METRIC_TYPE_VIEW_LABEL_MAP: Record<MetrictypesTypeDTO, string> = {
[MetrictypesTypeDTO.sum]: 'Sum',
[MetrictypesTypeDTO.gauge]: 'Gauge',
@@ -36,15 +26,6 @@ export const METRIC_TYPE_VIEW_LABEL_MAP: Record<MetrictypesTypeDTO, string> = {
[MetrictypesTypeDTO.exponentialhistogram]: 'Exp. Histogram',
};
// TODO(@amlannandy): To remove this once API migration is complete
export const METRIC_TYPE_VALUES_MAP: Record<MetricType, string> = {
[MetricType.SUM]: 'Sum',
[MetricType.GAUGE]: 'Gauge',
[MetricType.HISTOGRAM]: 'Histogram',
[MetricType.SUMMARY]: 'Summary',
[MetricType.EXPONENTIAL_HISTOGRAM]: 'ExponentialHistogram',
};
export const METRIC_TYPE_VIEW_VALUES_MAP: Record<MetrictypesTypeDTO, string> = {
[MetrictypesTypeDTO.sum]: 'Sum',
[MetrictypesTypeDTO.gauge]: 'Gauge',

View File

@@ -5,11 +5,13 @@ import {
Querybuildertypesv5OrderByDTO,
} from 'api/generated/services/sigNoz.schemas';
import { Filter } from 'api/v5/v5';
import APIError from 'types/api/error';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
export interface MetricsTableProps {
isLoading: boolean;
isError: boolean;
error?: APIError;
data: MetricsListItemRowData[];
pageSize: number;
currentPage: number;
@@ -33,6 +35,7 @@ export interface MetricsTreemapProps {
data: MetricsexplorertypesTreemapResponseDTO | undefined;
isLoading: boolean;
isError: boolean;
error?: APIError;
viewType: MetricsexplorertypesTreemapModeDTO;
openMetricDetails: (metricName: string, view: 'list' | 'treemap') => void;
setHeatmapView: (value: MetricsexplorertypesTreemapModeDTO) => void;
@@ -41,6 +44,7 @@ export interface MetricsTreemapProps {
export interface MetricsTreemapInternalProps {
isLoading: boolean;
isError: boolean;
error?: APIError;
data: MetricsexplorertypesTreemapResponseDTO | undefined;
viewType: MetricsexplorertypesTreemapModeDTO;
openMetricDetails: (metricName: string, view: 'list' | 'treemap') => void;

View File

@@ -1,4 +1,4 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`Table panel wrappper tests table should render fine with the query response and column units 1`] = `
.c1 {

View File

@@ -1,4 +1,4 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`Value panel wrappper tests should render tooltip when there are conflicting thresholds 1`] = `
.c1 {

View File

@@ -53,7 +53,7 @@ describe('PipelinePage container test', () => {
expect(editButton).toBeInTheDocument();
await userEvent.click(editButton);
expect(logEvent).toBeCalledWith('Logs: Pipelines: Entered Edit Mode', {
expect(logEvent).toHaveBeenCalledWith('Logs: Pipelines: Entered Edit Mode', {
source: 'signoz-ui',
});
});
@@ -78,8 +78,11 @@ describe('PipelinePage container test', () => {
expect(editButton).toBeInTheDocument();
await userEvent.click(editButton);
expect(logEvent).toBeCalledWith('Logs: Pipelines: Clicked Add New Pipeline', {
source: 'signoz-ui',
});
expect(logEvent).toHaveBeenCalledWith(
'Logs: Pipelines: Clicked Add New Pipeline',
{
source: 'signoz-ui',
},
);
});
});

View File

@@ -1,3 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`PipelinePage container test should render AddNewPipeline section 1`] = `<DocumentFragment />`;

View File

@@ -1,4 +1,4 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`PipelinePage container test should render CreatePipelineButton section 1`] = `
<DocumentFragment>

View File

@@ -1,4 +1,4 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`PipelinePage container test should render DeleteAction section 1`] = `
<DocumentFragment>

View File

@@ -1,4 +1,4 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`PipelinePage container test should render DragAction section 1`] = `
<DocumentFragment>

View File

@@ -1,4 +1,4 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`PipelinePage container test should render EditAction section 1`] = `
<DocumentFragment>

View File

@@ -1,4 +1,4 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`PipelinePage container test should render PipelineActions section 1`] = `
<DocumentFragment>

View File

@@ -1,4 +1,4 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`PipelinePage should render PipelineExpandView section 1`] = `
<DocumentFragment>

View File

@@ -1,4 +1,4 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`PipelinePage container test should render PipelinePageLayout section 1`] = `
<DocumentFragment>

View File

@@ -1,4 +1,4 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`PipelinePage container test should render PipelinesSearchSection section 1`] = `
<DocumentFragment>

View File

@@ -1,4 +1,4 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`PipelinePage container test should render TableExpandIcon section 1`] = `
<DocumentFragment>

View File

@@ -1,4 +1,4 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`Pipeline Page should render TagInput section 1`] = `
<DocumentFragment>

View File

@@ -1,4 +1,4 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`PipelinePage container test should render Tags section 1`] = `
<DocumentFragment>

View File

@@ -51,10 +51,10 @@ describe('ToolbarActions', () => {
expect(queryByTestId('clickhouse-view')).not.toBeInTheDocument();
await userEvent.click(screen.getByTestId('search-view'));
expect(handleChangeSelectedView).toBeCalled();
expect(handleChangeSelectedView).toHaveBeenCalled();
await userEvent.click(screen.getByTestId('query-builder-view'));
expect(handleChangeSelectedView).toBeCalled();
expect(handleChangeSelectedView).toHaveBeenCalled();
});
it('renders - clickhouse view and test view switching', async () => {
@@ -78,14 +78,14 @@ describe('ToolbarActions', () => {
expect(clickHouseView).toBeInTheDocument();
await userEvent.click(clickHouseView as HTMLElement);
expect(handleChangeSelectedView).toBeCalled();
expect(handleChangeSelectedView).toHaveBeenCalled();
// Test that timeseries view is also present and clickable
const timeseriesView = queryByTestId('query-builder-view');
expect(timeseriesView).toBeInTheDocument();
await userEvent.click(timeseriesView as HTMLElement);
expect(handleChangeSelectedView).toBeCalled();
expect(handleChangeSelectedView).toHaveBeenCalled();
});
it('RightToolbarActions - render correctly with props', async () => {
@@ -99,6 +99,6 @@ describe('ToolbarActions', () => {
const runQueryBtn = queryByText('Run Query');
expect(runQueryBtn).toBeInTheDocument();
await userEvent.click(runQueryBtn as HTMLElement);
expect(onStageRunQuery).toBeCalled();
expect(onStageRunQuery).toHaveBeenCalled();
});
});

View File

@@ -1,5 +1,11 @@
import { useEffect, useState } from 'react';
import { fireEvent, render, screen, within } from '@testing-library/react';
import {
fireEvent,
render,
screen,
waitFor,
within,
} from '@testing-library/react';
import {
MetricsexplorertypesListMetricDTO,
MetrictypesTypeDTO,
@@ -221,6 +227,108 @@ describe('MetricNameSelector', () => {
expect(container.querySelector('.ant-spin-spinning')).toBeInTheDocument();
});
it('preserves metric search text for signalSource normalization transition (undefined -> empty)', async () => {
returnMetrics([makeMetric({ metricName: 'http_requests_total' })]);
const query = makeQuery({
aggregateAttribute: {
key: 'http_requests_total',
type: '',
dataType: DataTypes.Float64,
},
aggregations: [
{
metricName: 'http_requests_total',
timeAggregation: 'rate',
spaceAggregation: 'sum',
temporality: '',
},
] as MetricAggregation[],
});
const { rerender } = render(
<MetricNameSelector
query={query}
onChange={jest.fn()}
signalSource={undefined}
/>,
);
rerender(
<MetricNameSelector query={query} onChange={jest.fn()} signalSource="" />,
);
await waitFor(() => {
const lastCall =
mockUseListMetrics.mock.calls[mockUseListMetrics.mock.calls.length - 1];
expect(lastCall?.[0]).toMatchObject({
searchText: 'http_requests_total',
limit: 100,
});
});
});
it('updates search text when metric name is hydrated after initial mount', async () => {
returnMetrics([makeMetric({ metricName: 'signoz_latency.bucket' })]);
const emptyQuery = makeQuery({
aggregateAttribute: {
key: '',
type: '',
dataType: DataTypes.Float64,
},
aggregations: [
{
metricName: '',
timeAggregation: 'rate',
spaceAggregation: 'sum',
temporality: '',
},
] as MetricAggregation[],
});
const hydratedQuery = makeQuery({
aggregateAttribute: {
key: '',
type: '',
dataType: DataTypes.Float64,
},
aggregations: [
{
metricName: 'signoz_latency.bucket',
timeAggregation: 'rate',
spaceAggregation: 'sum',
temporality: '',
},
] as MetricAggregation[],
});
const { rerender } = render(
<MetricNameSelector
query={emptyQuery}
onChange={jest.fn()}
signalSource=""
/>,
);
rerender(
<MetricNameSelector
query={hydratedQuery}
onChange={jest.fn()}
signalSource=""
/>,
);
await waitFor(() => {
const lastCall =
mockUseListMetrics.mock.calls[mockUseListMetrics.mock.calls.length - 1];
expect(lastCall?.[0]).toMatchObject({
searchText: 'signoz_latency.bucket',
limit: 100,
});
});
});
});
describe('selecting a metric type updates the aggregation options', () => {

View File

@@ -98,15 +98,30 @@ export const MetricNameSelector = memo(function MetricNameSelector({
useEffect(() => {
setInputValue(currentMetricName || defaultValue || '');
if (currentMetricName) {
setSearchText(currentMetricName);
}
}, [defaultValue, currentMetricName]);
useEffect(() => {
if (prevSignalSourceRef.current !== signalSource) {
const previousSignalSource = prevSignalSourceRef.current;
prevSignalSourceRef.current = signalSource;
const isNormalizationTransition =
(previousSignalSource === undefined && signalSource === '') ||
(previousSignalSource === '' && signalSource === undefined);
if (isNormalizationTransition && currentMetricName) {
setSearchText(currentMetricName);
setInputValue(currentMetricName || defaultValue || '');
return;
}
setSearchText('');
setInputValue('');
}
}, [signalSource]);
}, [signalSource, currentMetricName, defaultValue]);
const debouncedValue = useDebounce(searchText, DEBOUNCE_DELAY);
@@ -152,7 +167,9 @@ export const MetricNameSelector = memo(function MetricNameSelector({
}, [metrics]);
useEffect(() => {
const metricName = (query.aggregations?.[0] as MetricAggregation)?.metricName;
const metricName =
(query.aggregations?.[0] as MetricAggregation)?.metricName ||
query.aggregateAttribute?.key;
const hasAggregateAttributeType = query.aggregateAttribute?.type;
if (metricName && !hasAggregateAttributeType && metrics.length > 0) {
@@ -164,7 +181,13 @@ export const MetricNameSelector = memo(function MetricNameSelector({
);
}
}
}, [metrics, query.aggregations, query.aggregateAttribute?.type, onChange]);
}, [
metrics,
query.aggregations,
query.aggregateAttribute?.key,
query.aggregateAttribute?.type,
onChange,
]);
const resolveMetricFromText = useCallback(
(text: string): BaseAutocompleteData => {

View File

@@ -1,4 +1,4 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`loads and displays greeting 1`] = `
<DocumentFragment>

View File

@@ -1,50 +0,0 @@
import { useMemo } from 'react';
import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query';
import {
getMetricsTreeMap,
MetricsTreeMapPayload,
MetricsTreeMapResponse,
} from 'api/metricsExplorer/getMetricsTreeMap';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { ErrorResponse, SuccessResponse } from 'types/api';
type UseGetMetricsTreeMap = (
requestData: MetricsTreeMapPayload,
options?: UseQueryOptions<
SuccessResponse<MetricsTreeMapResponse> | ErrorResponse,
Error
>,
headers?: Record<string, string>,
) => UseQueryResult<
SuccessResponse<MetricsTreeMapResponse> | ErrorResponse,
Error
>;
export const useGetMetricsTreeMap: UseGetMetricsTreeMap = (
requestData,
options,
headers,
) => {
const queryKey = useMemo(() => {
if (options?.queryKey && Array.isArray(options.queryKey)) {
return [...options.queryKey];
}
if (options?.queryKey && typeof options.queryKey === 'string') {
return options.queryKey;
}
return [REACT_QUERY_KEY.GET_METRICS_TREE_MAP, requestData];
}, [options?.queryKey, requestData]);
return useQuery<
SuccessResponse<MetricsTreeMapResponse> | ErrorResponse,
Error
>({
queryFn: ({ signal }) => getMetricsTreeMap(requestData, signal, headers),
...options,
queryKey,
});
};

View File

@@ -1,26 +0,0 @@
import { useMutation, UseMutationResult } from 'react-query';
import updateMetricMetadata, {
UpdateMetricMetadataProps,
UpdateMetricMetadataResponse,
} from 'api/metricsExplorer/updateMetricMetadata';
import { ErrorResponse, SuccessResponse } from 'types/api';
export interface UseUpdateMetricMetadataProps {
metricName: string;
payload: UpdateMetricMetadataProps;
}
export function useUpdateMetricMetadata(): UseMutationResult<
SuccessResponse<UpdateMetricMetadataResponse> | ErrorResponse,
Error,
UseUpdateMetricMetadataProps
> {
return useMutation<
SuccessResponse<UpdateMetricMetadataResponse> | ErrorResponse,
Error,
UseUpdateMetricMetadataProps
>({
mutationFn: ({ metricName, payload }) =>
updateMetricMetadata(metricName, payload),
});
}

View File

@@ -252,23 +252,4 @@
}
}
}
.alert-details-v2 {
.tabs-and-filters {
.ant-tabs {
.ant-tabs-tab {
.ant-tabs-tab-btn {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
color: var(--text-ink-400) !important;
&[aria-selected='true'] {
background: var(--bg-vanilla-200);
color: var(--text-robin-500) !important;
}
}
}
}
}
}
}

View File

@@ -24,25 +24,3 @@
}
}
}
.lightMode {
.service-route-tab {
.ant-tabs-nav {
&::before {
border-bottom: 1px solid var(--bg-vanilla-300);
}
.ant-tabs-nav-wrap {
.ant-tabs-nav-list {
.ant-tabs-tab {
color: var(--text-ink-400);
}
.ant-tabs-tab.ant-tabs-tab-active .ant-tabs-tab-btn {
color: var(--text-robin-500);
}
}
}
}
}
}

View File

@@ -1,5 +1,5 @@
@import './components/Header/Header.styles.scss';
@import './components/SearchBar/SearchBar.styles.scss';
@use 'components/Header/Header.styles.scss';
@use 'components/SearchBar/SearchBar.styles.scss';
.traces-funnels {
margin-top: 113px;

View File

@@ -25,7 +25,7 @@ function resetStore(): void {
function mockContext(overrides: Partial<VariableFetchContext> = {}): void {
getVariableDependencyContextSpy.mockReturnValue({
doAllVariablesHaveValuesSelected: false,
doAllQueryVariablesHaveValuesSelected: false,
variableTypes: {},
dynamicVariableOrder: [],
dependencyData: null,
@@ -175,9 +175,9 @@ describe('variableFetchStore', () => {
expect(storeSnapshot.cycleIds.b).toBe(1);
});
it('should set dynamic variables to waiting when not all variables have values', () => {
it('should set dynamic variables to waiting when not all query variables have values', () => {
mockContext({
doAllVariablesHaveValuesSelected: false,
doAllQueryVariablesHaveValuesSelected: false,
dependencyData: buildDependencyData({ order: [] }),
variableTypes: { dyn1: 'DYNAMIC' },
dynamicVariableOrder: ['dyn1'],
@@ -190,9 +190,9 @@ describe('variableFetchStore', () => {
expect(storeSnapshot.states.dyn1).toBe('waiting');
});
it('should set dynamic variables to loading when all variables have values', () => {
it('should set dynamic variables to loading when all query variables have values', () => {
mockContext({
doAllVariablesHaveValuesSelected: true,
doAllQueryVariablesHaveValuesSelected: true,
dependencyData: buildDependencyData({ order: [] }),
variableTypes: { dyn1: 'DYNAMIC' },
dynamicVariableOrder: ['dyn1'],
@@ -523,5 +523,77 @@ describe('variableFetchStore', () => {
expect(variableFetchStore.getSnapshot().states.b).toBe('revalidating');
});
it('should enqueue dynamic variables immediately when all query variables are settled', () => {
mockContext({
dependencyData: buildDependencyData({
transitiveDescendants: { customVar: [] },
parentDependencyGraph: {},
}),
variableTypes: { q1: 'QUERY', customVar: 'CUSTOM', dyn1: 'DYNAMIC' },
dynamicVariableOrder: ['dyn1'],
});
variableFetchStore.update((d) => {
d.states.q1 = 'idle';
d.states.customVar = 'idle';
d.states.dyn1 = 'idle';
});
enqueueDescendantsOfVariable('customVar');
const snapshot = variableFetchStore.getSnapshot();
expect(snapshot.states.dyn1).toBe('loading');
expect(snapshot.cycleIds.dyn1).toBe(1);
});
it('should set dynamic variables to waiting when query variables are not yet settled', () => {
// a is a query variable still loading; changing customVar should queue dyn1 as waiting
mockContext({
dependencyData: buildDependencyData({
transitiveDescendants: { customVar: [] },
parentDependencyGraph: {},
}),
variableTypes: { a: 'QUERY', customVar: 'CUSTOM', dyn1: 'DYNAMIC' },
dynamicVariableOrder: ['dyn1'],
});
variableFetchStore.update((d) => {
d.states.a = 'loading';
d.states.customVar = 'idle';
d.states.dyn1 = 'idle';
});
enqueueDescendantsOfVariable('customVar');
expect(variableFetchStore.getSnapshot().states.dyn1).toBe('waiting');
});
it('should set dynamic variables to waiting when a query descendant is now loading', () => {
// a -> b (QUERY), dyn1 (DYNAMIC). When a changes, b starts loading,
// so dyn1 should wait until b settles.
mockContext({
dependencyData: buildDependencyData({
transitiveDescendants: { a: ['b'] },
parentDependencyGraph: { b: ['a'] },
}),
variableTypes: { a: 'QUERY', b: 'QUERY', dyn1: 'DYNAMIC' },
dynamicVariableOrder: ['dyn1'],
});
variableFetchStore.update((d) => {
d.states.a = 'idle';
d.states.b = 'idle';
d.states.dyn1 = 'idle';
});
enqueueDescendantsOfVariable('a');
const snapshot = variableFetchStore.getSnapshot();
// b's parent (a) is idle → b starts loading
expect(snapshot.states.b).toBe('loading');
// dyn1 must wait because b is now loading (not settled)
expect(snapshot.states.dyn1).toBe('waiting');
});
});
});

View File

@@ -134,7 +134,7 @@ describe('dashboardVariablesStore', () => {
expect(dependencyData).not.toBeNull();
});
it('should report doAllVariablesHaveValuesSelected as true when all variables have selectedValue', () => {
it('should report doAllQueryVariablesHaveValuesSelected as true when all query variables have values', () => {
setDashboardVariablesStore({
dashboardId: 'dash-1',
variables: {
@@ -146,18 +146,46 @@ describe('dashboardVariablesStore', () => {
}),
region: createVariable({
name: 'region',
type: 'CUSTOM',
type: 'QUERY',
order: 1,
selectedValue: 'us-east',
}),
},
});
const { doAllVariablesHaveValuesSelected } = getVariableDependencyContext();
expect(doAllVariablesHaveValuesSelected).toBe(true);
const {
doAllQueryVariablesHaveValuesSelected,
} = getVariableDependencyContext();
expect(doAllQueryVariablesHaveValuesSelected).toBe(true);
});
it('should report doAllVariablesHaveValuesSelected as false when some variables lack selectedValue', () => {
it('should report doAllQueryVariablesHaveValuesSelected as false when a query variable lacks a selectedValue', () => {
setDashboardVariablesStore({
dashboardId: 'dash-1',
variables: {
env: createVariable({
name: 'env',
type: 'QUERY',
order: 0,
selectedValue: 'prod',
}),
region: createVariable({
name: 'region',
type: 'QUERY',
order: 1,
selectedValue: undefined,
}),
},
});
const {
doAllQueryVariablesHaveValuesSelected,
} = getVariableDependencyContext();
expect(doAllQueryVariablesHaveValuesSelected).toBe(false);
});
it('should ignore non-QUERY variables when computing doAllQueryVariablesHaveValuesSelected', () => {
// env (QUERY) has a value; region (CUSTOM) and dyn1 (DYNAMIC) do not — they are ignored
setDashboardVariablesStore({
dashboardId: 'dash-1',
variables: {
@@ -173,62 +201,22 @@ describe('dashboardVariablesStore', () => {
order: 1,
selectedValue: undefined,
}),
},
});
const { doAllVariablesHaveValuesSelected } = getVariableDependencyContext();
expect(doAllVariablesHaveValuesSelected).toBe(false);
});
it('should treat DYNAMIC variable with allSelected=true and selectedValue=null as having a value', () => {
setDashboardVariablesStore({
dashboardId: 'dash-1',
variables: {
dyn1: createVariable({
name: 'dyn1',
type: 'DYNAMIC',
order: 0,
selectedValue: null as any,
allSelected: true,
}),
env: createVariable({
name: 'env',
type: 'QUERY',
order: 1,
selectedValue: 'prod',
order: 2,
selectedValue: '',
}),
},
});
const { doAllVariablesHaveValuesSelected } = getVariableDependencyContext();
expect(doAllVariablesHaveValuesSelected).toBe(true);
const {
doAllQueryVariablesHaveValuesSelected,
} = getVariableDependencyContext();
expect(doAllQueryVariablesHaveValuesSelected).toBe(true);
});
it('should treat DYNAMIC variable with allSelected=true and selectedValue=undefined as having a value', () => {
setDashboardVariablesStore({
dashboardId: 'dash-1',
variables: {
dyn1: createVariable({
name: 'dyn1',
type: 'DYNAMIC',
order: 0,
selectedValue: undefined,
allSelected: true,
}),
env: createVariable({
name: 'env',
type: 'QUERY',
order: 1,
selectedValue: 'prod',
}),
},
});
const { doAllVariablesHaveValuesSelected } = getVariableDependencyContext();
expect(doAllVariablesHaveValuesSelected).toBe(true);
});
it('should treat DYNAMIC variable with allSelected=true and empty string selectedValue as having a value', () => {
it('should return true for doAllQueryVariablesHaveValuesSelected when there are no query variables', () => {
setDashboardVariablesStore({
dashboardId: 'dash-1',
variables: {
@@ -237,61 +225,72 @@ describe('dashboardVariablesStore', () => {
type: 'DYNAMIC',
order: 0,
selectedValue: '',
allSelected: true,
}),
env: createVariable({
name: 'env',
type: 'QUERY',
order: 1,
selectedValue: 'prod',
}),
},
});
const { doAllVariablesHaveValuesSelected } = getVariableDependencyContext();
expect(doAllVariablesHaveValuesSelected).toBe(true);
const {
doAllQueryVariablesHaveValuesSelected,
} = getVariableDependencyContext();
expect(doAllQueryVariablesHaveValuesSelected).toBe(true);
});
it('should treat DYNAMIC variable with allSelected=true and empty array selectedValue as having a value', () => {
// Any non-nil, non-empty-array selectedValue is treated as selected
it.each([
{ label: 'numeric 0', selectedValue: 0 as number },
{ label: 'boolean false', selectedValue: false as boolean },
// ideally not possible but till we have concrete schema, we should not block dynamic variables
{ label: 'empty string', selectedValue: '' },
{
label: 'non-empty array',
selectedValue: ['a', 'b'] as (string | number | boolean)[],
},
])('should return true when selectedValue is $label', ({ selectedValue }) => {
setDashboardVariablesStore({
dashboardId: 'dash-1',
variables: {
dyn1: createVariable({
name: 'dyn1',
type: 'DYNAMIC',
order: 0,
selectedValue: [] as any,
allSelected: true,
}),
env: createVariable({
name: 'env',
type: 'QUERY',
order: 1,
selectedValue: 'prod',
}),
},
});
const { doAllVariablesHaveValuesSelected } = getVariableDependencyContext();
expect(doAllVariablesHaveValuesSelected).toBe(true);
});
it('should report false when a DYNAMIC variable has empty selectedValue and allSelected is not true', () => {
setDashboardVariablesStore({
dashboardId: 'dash-1',
variables: {
dyn1: createVariable({
name: 'dyn1',
type: 'DYNAMIC',
order: 0,
selectedValue: '',
allSelected: false,
selectedValue,
}),
},
});
const { doAllVariablesHaveValuesSelected } = getVariableDependencyContext();
expect(doAllVariablesHaveValuesSelected).toBe(false);
const {
doAllQueryVariablesHaveValuesSelected,
} = getVariableDependencyContext();
expect(doAllQueryVariablesHaveValuesSelected).toBe(true);
});
// null/undefined (tested above) and empty array are treated as not selected
it.each([
{
label: 'null',
selectedValue: null as IDashboardVariable['selectedValue'],
},
{ label: 'empty array', selectedValue: [] as (string | number | boolean)[] },
])(
'should return false when selectedValue is $label',
({ selectedValue }) => {
setDashboardVariablesStore({
dashboardId: 'dash-1',
variables: {
env: createVariable({
name: 'env',
type: 'QUERY',
order: 0,
selectedValue,
}),
},
});
const {
doAllQueryVariablesHaveValuesSelected,
} = getVariableDependencyContext();
expect(doAllQueryVariablesHaveValuesSelected).toBe(false);
},
);
});
});

View File

@@ -1,4 +1,4 @@
import { isEmpty, isUndefined } from 'lodash-es';
import { isNil } from 'lodash-es';
import createStore from '../store';
import { VariableFetchContext } from '../variableFetchStore';
@@ -68,28 +68,28 @@ export function updateDashboardVariablesStore({
*/
export function getVariableDependencyContext(): VariableFetchContext {
const state = dashboardVariablesStore.getSnapshot();
// Dynamic variables should only wait on query variables having values,
// not on CUSTOM, TEXTBOX, or other types.
const doAllQueryVariablesHaveValuesSelected = Object.values(
state.variables,
).every((variable) => {
if (variable.type !== 'QUERY') {
return true;
}
// If every variable already has a selectedValue (e.g. persisted from
// localStorage/URL), dynamic variables can start in parallel.
// Otherwise they wait for query vars to settle first.
const doAllVariablesHaveValuesSelected = Object.values(state.variables).every(
(variable) => {
if (
variable.type === 'DYNAMIC' &&
(variable.selectedValue === null || isEmpty(variable.selectedValue)) &&
variable.allSelected === true
) {
return true;
}
if (isNil(variable.selectedValue)) {
return false;
}
return (
!isUndefined(variable.selectedValue) && !isEmpty(variable.selectedValue)
);
},
);
if (Array.isArray(variable.selectedValue)) {
return variable.selectedValue.length > 0;
}
return true;
});
return {
doAllVariablesHaveValuesSelected,
doAllQueryVariablesHaveValuesSelected,
variableTypes: state.variableTypes,
dynamicVariableOrder: state.dynamicVariableOrder,
dependencyData: state.dependencyData,

View File

@@ -36,7 +36,7 @@ export type VariableFetchContext = Pick<
IDashboardVariablesStoreState,
'variableTypes' | 'dynamicVariableOrder' | 'dependencyData'
> & {
doAllVariablesHaveValuesSelected: boolean;
doAllQueryVariablesHaveValuesSelected: boolean;
};
const initialState: IVariableFetchStoreState = {
@@ -88,7 +88,7 @@ export function initializeVariableFetchStore(variableNames: string[]): void {
*/
export function enqueueFetchOfAllVariables(): void {
const {
doAllVariablesHaveValuesSelected,
doAllQueryVariablesHaveValuesSelected,
dependencyData,
variableTypes,
dynamicVariableOrder,
@@ -116,7 +116,7 @@ export function enqueueFetchOfAllVariables(): void {
// otherwise wait for query variables to settle first
dynamicVariableOrder.forEach((name) => {
draft.cycleIds[name] = (draft.cycleIds[name] || 0) + 1;
draft.states[name] = doAllVariablesHaveValuesSelected
draft.states[name] = doAllQueryVariablesHaveValuesSelected
? resolveFetchState(draft, name)
: 'waiting';
});
@@ -208,7 +208,11 @@ export function onVariableFetchFailure(name: string): void {
* ensures parents are set before children within a single update).
*/
export function enqueueDescendantsOfVariable(name: string): void {
const { dependencyData, variableTypes } = getVariableDependencyContext();
const {
dependencyData,
variableTypes,
dynamicVariableOrder,
} = getVariableDependencyContext();
if (!dependencyData) {
return;
}
@@ -230,5 +234,18 @@ export function enqueueDescendantsOfVariable(name: string): void {
? resolveFetchState(draft, desc)
: 'waiting';
});
// Dynamic variables implicitly depend on all query variable values.
// If all query variables are currently settled, start them immediately;
// otherwise they wait until query vars finish (unlocked via onVariableFetchComplete).
dynamicVariableOrder.forEach((dynName) => {
draft.cycleIds[dynName] = (draft.cycleIds[dynName] || 0) + 1;
draft.states[dynName] = areAllQueryVariablesSettled(
draft.states,
variableTypes,
)
? resolveFetchState(draft, dynName)
: 'waiting';
});
});
}

View File

@@ -1,10 +1,10 @@
@use './periscope.scss';
@import '@signozhq/design-tokens/dist/style.css';
@import 'overlayscrollbars/overlayscrollbars.css';
@import '@signozhq/design-tokens/dist/themes/signoz-tokens.css';
@import './periscope.scss';
/* Import fonts from CDN */
@import url('https://fonts.googleapis.com/css2?family=Work+Sans&wght@500&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Space+Mono&display=swap');

View File

@@ -1,39 +1,39 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`utils/spanToTree should return a single tree on valid trace data 1`] = `
Object {
"missingSpanTree": Array [],
"spanTree": Array [
Object {
"childReferences": Array [
Object {
{
"missingSpanTree": [],
"spanTree": [
{
"childReferences": [
{
"RefType": "CHILD_OF",
"SpanId": "",
"TraceId": "0000000000000000span_1",
},
],
"children": Array [
Object {
"childReferences": Array [
Object {
"children": [
{
"childReferences": [
{
"RefType": "CHILD_OF",
"SpanId": "span_1",
"TraceId": "0000000000000000span_1",
},
],
"children": Array [
Object {
"childReferences": Array [
Object {
"children": [
{
"childReferences": [
{
"RefType": "CHILD_OF",
"SpanId": "span_2",
"TraceId": "0000000000000000span_1",
},
],
"children": Array [],
"event": Array [
Object {
"attributeMap": Object {
"children": [],
"event": [
{
"attributeMap": {
"event": "HTTP request received S3",
"level": "info",
"method": "GET",
@@ -46,15 +46,15 @@ Object {
"id": "span_3",
"isProcessed": true,
"name": "HTTP GET SPAN 3",
"nonChildReferences": Array [],
"nonChildReferences": [],
"serviceColour": "",
"serviceName": "frontend",
"spanKind": "Lorem Ipsum2",
"startTime": 1657275433246,
"statusCodeString": "Unset2",
"statusMessage": "Server2",
"tags": Array [
Object {
"tags": [
{
"key": "host.name.span3",
"value": "span_3",
},
@@ -63,9 +63,9 @@ Object {
"value": 683273000,
},
],
"event": Array [
Object {
"attributeMap": Object {
"event": [
{
"attributeMap": {
"event": "HTTP request received S2",
"level": "info",
"method": "GET",
@@ -78,15 +78,15 @@ Object {
"id": "span_2",
"isProcessed": true,
"name": "HTTP GET SPAN 2",
"nonChildReferences": Array [],
"nonChildReferences": [],
"serviceColour": "",
"serviceName": "frontend",
"spanKind": "Lorem Ipsum1",
"startTime": 1657275433246,
"statusCodeString": "Unset1",
"statusMessage": "Server1",
"tags": Array [
Object {
"tags": [
{
"key": "host.name.span2",
"value": "span_2",
},
@@ -95,9 +95,9 @@ Object {
"value": 683273000,
},
],
"event": Array [
Object {
"attributeMap": Object {
"event": [
{
"attributeMap": {
"event": "HTTP request received S1",
"level": "info",
"method": "GET",
@@ -109,15 +109,15 @@ Object {
"hasError": false,
"id": "span_1",
"name": "HTTP GET SPAN 1",
"nonChildReferences": Array [],
"nonChildReferences": [],
"serviceColour": "",
"serviceName": "frontend",
"spanKind": "Lorem Ipsum",
"startTime": 1657275433246,
"statusCodeString": "Unset",
"statusMessage": "Server",
"tags": Array [
Object {
"tags": [
{
"key": "host.name.span1",
"value": "span_1",
},
@@ -130,22 +130,22 @@ Object {
`;
exports[`utils/spanToTree should return a single tree on valid trace data 2`] = `
Object {
"missingSpanTree": Array [
Object {
"children": Array [
Object {
"childReferences": Array [
Object {
{
"missingSpanTree": [
{
"children": [
{
"childReferences": [
{
"RefType": "CHILD_OF",
"SpanId": "span_2",
"TraceId": "0000000000000000span_1",
},
],
"children": Array [],
"event": Array [
Object {
"attributeMap": Object {
"children": [],
"event": [
{
"attributeMap": {
"event": "HTTP request received S3",
"level": "info",
"method": "GET",
@@ -158,15 +158,15 @@ Object {
"id": "span_3",
"isProcessed": true,
"name": "HTTP GET SPAN 3",
"nonChildReferences": Array [],
"nonChildReferences": [],
"serviceColour": "",
"serviceName": "frontend",
"spanKind": "Lorem Ipsum2",
"startTime": 1657275433246,
"statusCodeString": "Unset2",
"statusMessage": "Server2",
"tags": Array [
Object {
"tags": [
{
"key": "host.name.span3",
"value": "span_3",
},
@@ -184,24 +184,24 @@ Object {
"startTime": null,
"statusCodeString": "",
"statusMessage": "",
"tags": Array [],
"tags": [],
"time": null,
"value": null,
},
],
"spanTree": Array [
Object {
"childReferences": Array [
Object {
"spanTree": [
{
"childReferences": [
{
"RefType": "CHILD_OF",
"SpanId": "",
"TraceId": "0000000000000000span_1",
},
],
"children": Array [],
"event": Array [
Object {
"attributeMap": Object {
"children": [],
"event": [
{
"attributeMap": {
"event": "HTTP request received S1",
"level": "info",
"method": "GET",
@@ -213,15 +213,15 @@ Object {
"hasError": false,
"id": "span_1",
"name": "HTTP GET SPAN 1",
"nonChildReferences": Array [],
"nonChildReferences": [],
"serviceColour": "",
"serviceName": "frontend",
"spanKind": "Lorem Ipsum",
"startTime": 1657275433246,
"statusCodeString": "Unset",
"statusMessage": "Server",
"tags": Array [
Object {
"tags": [
{
"key": "host.name.span1",
"value": "span_1",
},

View File

@@ -1,34 +1,26 @@
import { getLocation } from 'utils/getLocation';
import { buildAbsolutePath } from '../app';
jest.mock('utils/getLocation');
const BASE_PATH = '/some-base-path';
describe('buildAbsolutePath', () => {
const originalLocation = window.location;
afterEach(() => {
Object.defineProperty(window, 'location', {
writable: true,
value: originalLocation,
});
const mockLocation = (pathname: string): void => {
(getLocation as jest.Mock).mockReturnValue({
pathname,
href: `http://localhost:8080${pathname}`,
origin: 'http://localhost:8080',
protocol: 'http:',
host: 'localhost',
hostname: 'localhost',
port: '',
search: '',
hash: '',
});
};
const mockLocation = (pathname: string): void => {
Object.defineProperty(window, 'location', {
writable: true,
value: {
pathname,
href: `http://localhost:8080${pathname}`,
origin: 'http://localhost:8080',
protocol: 'http:',
host: 'localhost',
hostname: 'localhost',
port: '',
search: '',
hash: '',
},
});
};
describe('buildAbsolutePath', () => {
describe('when base path ends with a forward slash', () => {
beforeEach(() => {
mockLocation(`${BASE_PATH}/`);

View File

@@ -2,6 +2,7 @@ import getLocalStorage from 'api/browser/localstorage/get';
import { FeatureKeys } from 'constants/features';
import { SKIP_ONBOARDING } from 'constants/onboarding';
import { get } from 'lodash-es';
import { getLocation } from 'utils/getLocation';
export const isOnboardingSkipped = (): boolean =>
getLocalStorage(SKIP_ONBOARDING) === 'true';
@@ -57,7 +58,7 @@ export function buildAbsolutePath({
relativePath: string;
urlQueryString?: string;
}): string {
const { pathname } = window.location;
const { pathname } = getLocation();
// ensure base path always ends with a forward slash
const basePath = pathname.endsWith('/') ? pathname : `${pathname}/`;

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