Merge pull request #6405 from SigNoz/release/v0.58.x

Release/v0.58.x
This commit is contained in:
Prashant Shahi 2024-11-08 21:49:57 +05:30 committed by GitHub
commit eb6670980a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
136 changed files with 6876 additions and 1851 deletions

83
.github/workflows/docs.yml vendored Normal file
View File

@ -0,0 +1,83 @@
name: "Update PR labels and Block PR until related docs are shipped for the feature"
on:
pull_request:
branches:
- develop
types: [opened, edited, labeled, unlabeled]
permissions:
pull-requests: write
contents: read
jobs:
docs_label_check:
runs-on: ubuntu-latest
steps:
- name: Check PR Title and Manage Labels
uses: actions/github-script@v6
with:
script: |
const prTitle = context.payload.pull_request.title;
const prNumber = context.payload.pull_request.number;
const owner = context.repo.owner;
const repo = context.repo.repo;
// Fetch the current PR details to get labels
const pr = await github.rest.pulls.get({
owner,
repo,
pull_number: prNumber
});
const labels = pr.data.labels.map(label => label.name);
if (prTitle.startsWith('feat:')) {
const hasDocsRequired = labels.includes('docs required');
const hasDocsShipped = labels.includes('docs shipped');
const hasDocsNotRequired = labels.includes('docs not required');
// If "docs not required" is present, skip the checks
if (hasDocsNotRequired && !hasDocsRequired) {
console.log("Skipping checks due to 'docs not required' label.");
return; // Exit the script early
}
// If "docs shipped" is present, remove "docs required" if it exists
if (hasDocsShipped && hasDocsRequired) {
await github.rest.issues.removeLabel({
owner,
repo,
issue_number: prNumber,
name: 'docs required'
});
console.log("Removed 'docs required' label.");
}
// Add "docs required" label if neither "docs shipped" nor "docs required" are present
if (!hasDocsRequired && !hasDocsShipped) {
await github.rest.issues.addLabels({
owner,
repo,
issue_number: prNumber,
labels: ['docs required']
});
console.log("Added 'docs required' label.");
}
}
// Fetch the updated labels after any changes
const updatedPr = await github.rest.pulls.get({
owner,
repo,
pull_number: prNumber
});
const updatedLabels = updatedPr.data.labels.map(label => label.name);
const updatedHasDocsRequired = updatedLabels.includes('docs required');
const updatedHasDocsShipped = updatedLabels.includes('docs shipped');
// Block PR if "docs required" is still present and "docs shipped" is missing
if (updatedHasDocsRequired && !updatedHasDocsShipped) {
core.setFailed("This PR requires documentation. Please remove the 'docs required' label and add the 'docs shipped' label to proceed.");
}

View File

@ -31,7 +31,6 @@ jobs:
GCP_ZONE: ${{ secrets.GCP_ZONE }} GCP_ZONE: ${{ secrets.GCP_ZONE }}
GCP_INSTANCE: ${{ secrets.GCP_INSTANCE }} GCP_INSTANCE: ${{ secrets.GCP_INSTANCE }}
CLOUDSDK_CORE_DISABLE_PROMPTS: 1 CLOUDSDK_CORE_DISABLE_PROMPTS: 1
KAFKA_SPAN_EVAL: true
run: | run: |
read -r -d '' COMMAND <<EOF || true read -r -d '' COMMAND <<EOF || true
echo "GITHUB_BRANCH: ${GITHUB_BRANCH}" echo "GITHUB_BRANCH: ${GITHUB_BRANCH}"
@ -39,6 +38,7 @@ jobs:
export DOCKER_TAG="${GITHUB_SHA:0:7}" # needed for child process to access it export DOCKER_TAG="${GITHUB_SHA:0:7}" # needed for child process to access it
export OTELCOL_TAG="main" export OTELCOL_TAG="main"
export PATH="/usr/local/go/bin/:$PATH" # needed for Golang to work export PATH="/usr/local/go/bin/:$PATH" # needed for Golang to work
export KAFKA_SPAN_EVAL="true"
docker system prune --force docker system prune --force
docker pull signoz/signoz-otel-collector:main docker pull signoz/signoz-otel-collector:main
docker pull signoz/signoz-schema-migrator:main docker pull signoz/signoz-schema-migrator:main

View File

@ -146,7 +146,7 @@ services:
condition: on-failure condition: on-failure
query-service: query-service:
image: signoz/query-service:0.57.0 image: signoz/query-service:0.58.0
command: command:
[ [
"-config=/root/config/prometheus.yml", "-config=/root/config/prometheus.yml",
@ -186,7 +186,7 @@ services:
<<: *db-depend <<: *db-depend
frontend: frontend:
image: signoz/frontend:0.57.0 image: signoz/frontend:0.58.0
deploy: deploy:
restart_policy: restart_policy:
condition: on-failure condition: on-failure
@ -199,7 +199,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf - ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector: otel-collector:
image: signoz/signoz-otel-collector:0.111.5 image: signoz/signoz-otel-collector:0.111.8
command: command:
[ [
"--config=/etc/otel-collector-config.yaml", "--config=/etc/otel-collector-config.yaml",
@ -237,7 +237,7 @@ services:
- query-service - query-service
otel-collector-migrator: otel-collector-migrator:
image: signoz/signoz-schema-migrator:0.111.5 image: signoz/signoz-schema-migrator:0.111.8
deploy: deploy:
restart_policy: restart_policy:
condition: on-failure condition: on-failure

View File

@ -69,7 +69,7 @@ services:
- --storage.path=/data - --storage.path=/data
otel-collector-migrator: otel-collector-migrator:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.5} image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.8}
container_name: otel-migrator container_name: otel-migrator
command: command:
- "--dsn=tcp://clickhouse:9000" - "--dsn=tcp://clickhouse:9000"
@ -84,7 +84,7 @@ services:
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md` # Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
otel-collector: otel-collector:
container_name: signoz-otel-collector container_name: signoz-otel-collector
image: signoz/signoz-otel-collector:0.111.5 image: signoz/signoz-otel-collector:0.111.8
command: command:
[ [
"--config=/etc/otel-collector-config.yaml", "--config=/etc/otel-collector-config.yaml",

View File

@ -162,7 +162,7 @@ services:
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md` # Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
query-service: query-service:
image: signoz/query-service:${DOCKER_TAG:-0.57.0} image: signoz/query-service:${DOCKER_TAG:-0.58.0}
container_name: signoz-query-service container_name: signoz-query-service
command: command:
[ [
@ -201,7 +201,7 @@ services:
<<: *db-depend <<: *db-depend
frontend: frontend:
image: signoz/frontend:${DOCKER_TAG:-0.57.0} image: signoz/frontend:${DOCKER_TAG:-0.58.0}
container_name: signoz-frontend container_name: signoz-frontend
restart: on-failure restart: on-failure
depends_on: depends_on:
@ -213,7 +213,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf - ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector-migrator-sync: otel-collector-migrator-sync:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.5} image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.8}
container_name: otel-migrator-sync container_name: otel-migrator-sync
command: command:
- "sync" - "sync"
@ -228,7 +228,7 @@ services:
# condition: service_healthy # condition: service_healthy
otel-collector-migrator-async: otel-collector-migrator-async:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.5} image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.8}
container_name: otel-migrator-async container_name: otel-migrator-async
command: command:
- "async" - "async"
@ -245,7 +245,7 @@ services:
# condition: service_healthy # condition: service_healthy
otel-collector: otel-collector:
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.5} image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.8}
container_name: signoz-otel-collector container_name: signoz-otel-collector
command: command:
[ [

View File

@ -167,7 +167,7 @@ services:
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md` # Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
query-service: query-service:
image: signoz/query-service:${DOCKER_TAG:-0.57.0} image: signoz/query-service:${DOCKER_TAG:-0.58.0}
container_name: signoz-query-service container_name: signoz-query-service
command: command:
[ [
@ -191,6 +191,7 @@ services:
- GODEBUG=netdns=go - GODEBUG=netdns=go
- TELEMETRY_ENABLED=true - TELEMETRY_ENABLED=true
- DEPLOYMENT_TYPE=docker-standalone-amd - DEPLOYMENT_TYPE=docker-standalone-amd
- KAFKA_SPAN_EVAL=${KAFKA_SPAN_EVAL:-false}
restart: on-failure restart: on-failure
healthcheck: healthcheck:
test: test:
@ -207,7 +208,7 @@ services:
<<: *db-depend <<: *db-depend
frontend: frontend:
image: signoz/frontend:${DOCKER_TAG:-0.57.0} image: signoz/frontend:${DOCKER_TAG:-0.58.0}
container_name: signoz-frontend container_name: signoz-frontend
restart: on-failure restart: on-failure
depends_on: depends_on:
@ -219,7 +220,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf - ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector-migrator: otel-collector-migrator:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.5} image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.8}
container_name: otel-migrator container_name: otel-migrator
command: command:
- "--dsn=tcp://clickhouse:9000" - "--dsn=tcp://clickhouse:9000"
@ -233,7 +234,7 @@ services:
otel-collector: otel-collector:
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.5} image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.8}
container_name: signoz-otel-collector container_name: signoz-otel-collector
command: command:
[ [

View File

@ -1,6 +1,7 @@
package model package model
import ( import (
"go.signoz.io/signoz/pkg/query-service/constants"
basemodel "go.signoz.io/signoz/pkg/query-service/model" basemodel "go.signoz.io/signoz/pkg/query-service/model"
) )
@ -134,6 +135,13 @@ var BasicPlan = basemodel.FeatureSet{
UsageLimit: -1, UsageLimit: -1,
Route: "", Route: "",
}, },
basemodel.Feature{
Name: basemodel.HostsInfraMonitoring,
Active: constants.EnableHostsInfraMonitoring(),
Usage: 0,
UsageLimit: -1,
Route: "",
},
} }
var ProPlan = basemodel.FeatureSet{ var ProPlan = basemodel.FeatureSet{
@ -249,6 +257,13 @@ var ProPlan = basemodel.FeatureSet{
UsageLimit: -1, UsageLimit: -1,
Route: "", Route: "",
}, },
basemodel.Feature{
Name: basemodel.HostsInfraMonitoring,
Active: constants.EnableHostsInfraMonitoring(),
Usage: 0,
UsageLimit: -1,
Route: "",
},
} }
var EnterprisePlan = basemodel.FeatureSet{ var EnterprisePlan = basemodel.FeatureSet{
@ -378,4 +393,11 @@ var EnterprisePlan = basemodel.FeatureSet{
UsageLimit: -1, UsageLimit: -1,
Route: "", Route: "",
}, },
basemodel.Feature{
Name: basemodel.HostsInfraMonitoring,
Active: constants.EnableHostsInfraMonitoring(),
Usage: 0,
UsageLimit: -1,
Route: "",
},
} }

View File

@ -34,7 +34,7 @@
"@dnd-kit/core": "6.1.0", "@dnd-kit/core": "6.1.0",
"@dnd-kit/modifiers": "7.0.0", "@dnd-kit/modifiers": "7.0.0",
"@dnd-kit/sortable": "8.0.0", "@dnd-kit/sortable": "8.0.0",
"@grafana/data": "^9.5.2", "@grafana/data": "^11.2.3",
"@mdx-js/loader": "2.3.0", "@mdx-js/loader": "2.3.0",
"@mdx-js/react": "2.3.0", "@mdx-js/react": "2.3.0",
"@monaco-editor/react": "^4.3.1", "@monaco-editor/react": "^4.3.1",
@ -51,7 +51,7 @@
"ansi-to-html": "0.7.2", "ansi-to-html": "0.7.2",
"antd": "5.11.0", "antd": "5.11.0",
"antd-table-saveas-excel": "2.2.1", "antd-table-saveas-excel": "2.2.1",
"axios": "1.7.4", "axios": "1.7.7",
"babel-eslint": "^10.1.0", "babel-eslint": "^10.1.0",
"babel-jest": "^29.6.4", "babel-jest": "^29.6.4",
"babel-loader": "9.1.3", "babel-loader": "9.1.3",
@ -76,7 +76,7 @@
"fontfaceobserver": "2.3.0", "fontfaceobserver": "2.3.0",
"history": "4.10.1", "history": "4.10.1",
"html-webpack-plugin": "5.5.0", "html-webpack-plugin": "5.5.0",
"http-proxy-middleware": "2.0.6", "http-proxy-middleware": "2.0.7",
"i18next": "^21.6.12", "i18next": "^21.6.12",
"i18next-browser-languagedetector": "^6.1.3", "i18next-browser-languagedetector": "^6.1.3",
"i18next-http-backend": "^1.3.2", "i18next-http-backend": "^1.3.2",
@ -87,6 +87,8 @@
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"lucide-react": "0.379.0", "lucide-react": "0.379.0",
"mini-css-extract-plugin": "2.4.5", "mini-css-extract-plugin": "2.4.5",
"overlayscrollbars": "^2.8.1",
"overlayscrollbars-react": "^0.5.6",
"papaparse": "5.4.1", "papaparse": "5.4.1",
"posthog-js": "1.160.3", "posthog-js": "1.160.3",
"rc-tween-one": "3.0.6", "rc-tween-one": "3.0.6",
@ -107,11 +109,10 @@
"react-query": "3.39.3", "react-query": "3.39.3",
"react-redux": "^7.2.2", "react-redux": "^7.2.2",
"react-router-dom": "^5.2.0", "react-router-dom": "^5.2.0",
"react-router-dom-v5-compat": "6.27.0",
"react-syntax-highlighter": "15.5.0", "react-syntax-highlighter": "15.5.0",
"react-use": "^17.3.2", "react-use": "^17.3.2",
"react-virtuoso": "4.0.3", "react-virtuoso": "4.0.3",
"overlayscrollbars-react": "^0.5.6",
"overlayscrollbars": "^2.8.1",
"redux": "^4.0.5", "redux": "^4.0.5",
"redux-thunk": "^2.3.0", "redux-thunk": "^2.3.0",
"rehype-raw": "7.0.0", "rehype-raw": "7.0.0",
@ -123,10 +124,10 @@
"ts-node": "^10.2.1", "ts-node": "^10.2.1",
"tsconfig-paths-webpack-plugin": "^3.5.1", "tsconfig-paths-webpack-plugin": "^3.5.1",
"typescript": "^4.0.5", "typescript": "^4.0.5",
"uplot": "1.6.26", "uplot": "1.6.31",
"uuid": "^8.3.2", "uuid": "^8.3.2",
"web-vitals": "^0.2.4", "web-vitals": "^0.2.4",
"webpack": "5.88.2", "webpack": "5.94.0",
"webpack-dev-server": "^4.15.1", "webpack-dev-server": "^4.15.1",
"webpack-retry-chunk-load-plugin": "3.1.1", "webpack-retry-chunk-load-plugin": "3.1.1",
"xstate": "^4.31.0" "xstate": "^4.31.0"

View File

@ -21,10 +21,30 @@
"button": "Get Started" "button": "Get Started"
}, },
"summarySection": { "summarySection": {
"viewDetailsButton": "View Details" "viewDetailsButton": "View Details",
"consumer": {
"title": "Consumer lag view",
"description": "Connect and Monitor Your Data Streams"
},
"producer": {
"title": "Producer latency view",
"description": "Connect and Monitor Your Data Streams"
},
"partition": {
"title": "Partition Latency view",
"description": "Connect and Monitor Your Data Streams"
},
"dropRate": {
"title": "Drop Rate view",
"description": "Connect and Monitor Your Data Streams"
}
}, },
"confirmModal": { "confirmModal": {
"content": "Before navigating to the details page, please make sure you have configured all the required setup to ensure correct data monitoring.", "content": "Before navigating to the details page, please make sure you have configured all the required setup to ensure correct data monitoring.",
"okText": "Proceed" "okText": "Proceed"
},
"overviewSummarySection": {
"title": "Monitor Your Data Streams",
"subtitle": "Monitor key Kafka metrics like consumer lag and latency to ensure efficient data flow and troubleshoot in real time."
} }
} }

View File

@ -4,6 +4,7 @@
"SERVICE_METRICS": "SigNoz | Service Metrics", "SERVICE_METRICS": "SigNoz | Service Metrics",
"SERVICE_MAP": "SigNoz | Service Map", "SERVICE_MAP": "SigNoz | Service Map",
"GET_STARTED": "SigNoz | Get Started", "GET_STARTED": "SigNoz | Get Started",
"ONBOARDING": "SigNoz | Get Started",
"GET_STARTED_APPLICATION_MONITORING": "SigNoz | Get Started | APM", "GET_STARTED_APPLICATION_MONITORING": "SigNoz | Get Started | APM",
"GET_STARTED_LOGS_MANAGEMENT": "SigNoz | Get Started | Logs", "GET_STARTED_LOGS_MANAGEMENT": "SigNoz | Get Started | Logs",
"GET_STARTED_INFRASTRUCTURE_MONITORING": "SigNoz | Get Started | Infrastructure", "GET_STARTED_INFRASTRUCTURE_MONITORING": "SigNoz | Get Started | Infrastructure",

View File

@ -1,5 +1,6 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
import getLocalStorageApi from 'api/browser/localstorage/get'; import getLocalStorageApi from 'api/browser/localstorage/get';
import getOrgUser from 'api/user/getOrgUser';
import loginApi from 'api/user/login'; import loginApi from 'api/user/login';
import { Logout } from 'api/utils'; import { Logout } from 'api/utils';
import Spinner from 'components/Spinner'; import Spinner from 'components/Spinner';
@ -8,8 +9,10 @@ import ROUTES from 'constants/routes';
import useLicense from 'hooks/useLicense'; import useLicense from 'hooks/useLicense';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
import history from 'lib/history'; import history from 'lib/history';
import { ReactChild, useEffect, useMemo } from 'react'; import { isEmpty, isNull } from 'lodash-es';
import { ReactChild, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useQuery } from 'react-query';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { matchPath, Redirect, useLocation } from 'react-router-dom'; import { matchPath, Redirect, useLocation } from 'react-router-dom';
import { Dispatch } from 'redux'; import { Dispatch } from 'redux';
@ -17,6 +20,7 @@ import { AppState } from 'store/reducers';
import { getInitialUserTokenRefreshToken } from 'store/utils'; import { getInitialUserTokenRefreshToken } from 'store/utils';
import AppActions from 'types/actions'; import AppActions from 'types/actions';
import { UPDATE_USER_IS_FETCH } from 'types/actions/app'; import { UPDATE_USER_IS_FETCH } from 'types/actions/app';
import { Organization } from 'types/api/user/getOrganization';
import AppReducer from 'types/reducer/app'; import AppReducer from 'types/reducer/app';
import { routePermission } from 'utils/permission'; import { routePermission } from 'utils/permission';
@ -31,6 +35,19 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
const location = useLocation(); const location = useLocation();
const { pathname } = location; const { pathname } = location;
const [isLoading, setIsLoading] = useState<boolean>(true);
const {
org,
orgPreferences,
user,
role,
isUserFetching,
isUserFetchingError,
isLoggedIn: isLoggedInState,
isFetchingOrgPreferences,
} = useSelector<AppState, AppReducer>((state) => state.app);
const mapRoutes = useMemo( const mapRoutes = useMemo(
() => () =>
new Map( new Map(
@ -44,18 +61,21 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
[pathname], [pathname],
); );
const isOnboardingComplete = useMemo(
() =>
orgPreferences?.find(
(preference: Record<string, any>) => preference.key === 'ORG_ONBOARDING',
)?.value,
[orgPreferences],
);
const { const {
data: licensesData, data: licensesData,
isFetching: isFetchingLicensesData, isFetching: isFetchingLicensesData,
} = useLicense(); } = useLicense();
const {
isUserFetching,
isUserFetchingError,
isLoggedIn: isLoggedInState,
} = useSelector<AppState, AppReducer>((state) => state.app);
const { t } = useTranslation(['common']); const { t } = useTranslation(['common']);
const localStorageUserAuthToken = getInitialUserTokenRefreshToken(); const localStorageUserAuthToken = getInitialUserTokenRefreshToken();
const dispatch = useDispatch<Dispatch<AppActions>>(); const dispatch = useDispatch<Dispatch<AppActions>>();
@ -66,6 +86,8 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
const isOldRoute = oldRoutes.indexOf(pathname) > -1; const isOldRoute = oldRoutes.indexOf(pathname) > -1;
const [orgData, setOrgData] = useState<Organization | undefined>(undefined);
const isLocalStorageLoggedIn = const isLocalStorageLoggedIn =
getLocalStorageApi(LOCALSTORAGE.IS_LOGGED_IN) === 'true'; getLocalStorageApi(LOCALSTORAGE.IS_LOGGED_IN) === 'true';
@ -81,6 +103,63 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
} }
}; };
const { data: orgUsers, isLoading: isLoadingOrgUsers } = useQuery({
queryFn: () => {
if (orgData && orgData.id !== undefined) {
return getOrgUser({
orgId: orgData.id,
});
}
return undefined;
},
queryKey: ['getOrgUser'],
enabled: !isEmpty(orgData),
});
const checkFirstTimeUser = (): boolean => {
const users = orgUsers?.payload || [];
const remainingUsers = users.filter(
(user) => user.email !== 'admin@signoz.cloud',
);
return remainingUsers.length === 1;
};
// Check if the onboarding should be shown based on the org users and onboarding completion status, wait for org users and preferences to load
const shouldShowOnboarding = (): boolean => {
// Only run this effect if the org users and preferences are loaded
if (!isLoadingOrgUsers && !isFetchingOrgPreferences) {
const isFirstUser = checkFirstTimeUser();
// Redirect to get started if it's not the first user or if the onboarding is complete
return isFirstUser && !isOnboardingComplete;
}
return false;
};
const handleRedirectForOrgOnboarding = (key: string): void => {
if (
isLoggedInState &&
!isFetchingOrgPreferences &&
!isLoadingOrgUsers &&
!isEmpty(orgUsers?.payload) &&
!isNull(orgPreferences)
) {
if (key === 'ONBOARDING' && isOnboardingComplete) {
history.push(ROUTES.APPLICATION);
}
const isFirstTimeUser = checkFirstTimeUser();
if (isFirstTimeUser && !isOnboardingComplete) {
history.push(ROUTES.ONBOARDING);
}
}
};
const handleUserLoginIfTokenPresent = async ( const handleUserLoginIfTokenPresent = async (
key: keyof typeof ROUTES, key: keyof typeof ROUTES,
): Promise<void> => { ): Promise<void> => {
@ -102,6 +181,8 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
response.payload.refreshJwt, response.payload.refreshJwt,
); );
handleRedirectForOrgOnboarding(key);
if ( if (
userResponse && userResponse &&
route && route &&
@ -129,7 +210,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
) { ) {
handleUserLoginIfTokenPresent(key); handleUserLoginIfTokenPresent(key);
} else { } else {
// user does have localstorage values handleRedirectForOrgOnboarding(key);
navigateToLoginIfNotLoggedIn(isLocalStorageLoggedIn); navigateToLoginIfNotLoggedIn(isLocalStorageLoggedIn);
} }
@ -160,6 +241,45 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
} }
}, [isFetchingLicensesData]); }, [isFetchingLicensesData]);
useEffect(() => {
if (org && org.length > 0 && org[0].id !== undefined) {
setOrgData(org[0]);
}
}, [org]);
const handleRouting = (): void => {
const showOrgOnboarding = shouldShowOnboarding();
if (showOrgOnboarding && !isOnboardingComplete) {
history.push(ROUTES.ONBOARDING);
} else {
history.push(ROUTES.APPLICATION);
}
};
useEffect(() => {
const { isPrivate } = currentRoute || {
isPrivate: false,
};
if (isLoggedInState && role && role !== 'ADMIN') {
setIsLoading(false);
}
if (!isPrivate) {
setIsLoading(false);
}
if (
!isEmpty(user) &&
!isFetchingOrgPreferences &&
!isEmpty(orgUsers?.payload) &&
!isNull(orgPreferences)
) {
setIsLoading(false);
}
}, [currentRoute, user, role, orgUsers, orgPreferences]);
// eslint-disable-next-line sonarjs/cognitive-complexity // eslint-disable-next-line sonarjs/cognitive-complexity
useEffect(() => { useEffect(() => {
(async (): Promise<void> => { (async (): Promise<void> => {
@ -181,9 +301,8 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
handlePrivateRoutes(key); handlePrivateRoutes(key);
} else { } else {
// no need to fetch the user and make user fetching false // no need to fetch the user and make user fetching false
if (getLocalStorageApi(LOCALSTORAGE.IS_LOGGED_IN) === 'true') { if (getLocalStorageApi(LOCALSTORAGE.IS_LOGGED_IN) === 'true') {
history.push(ROUTES.APPLICATION); handleRouting();
} }
dispatch({ dispatch({
type: UPDATE_USER_IS_FETCH, type: UPDATE_USER_IS_FETCH,
@ -195,7 +314,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
} else if (pathname === ROUTES.HOME_PAGE) { } else if (pathname === ROUTES.HOME_PAGE) {
// routing to application page over root page // routing to application page over root page
if (isLoggedInState) { if (isLoggedInState) {
history.push(ROUTES.APPLICATION); handleRouting();
} else { } else {
navigateToLoginIfNotLoggedIn(); navigateToLoginIfNotLoggedIn();
} }
@ -208,13 +327,20 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
history.push(ROUTES.SOMETHING_WENT_WRONG); history.push(ROUTES.SOMETHING_WENT_WRONG);
} }
})(); })();
}, [dispatch, isLoggedInState, currentRoute, licensesData]); }, [
dispatch,
isLoggedInState,
currentRoute,
licensesData,
orgUsers,
orgPreferences,
]);
if (isUserFetchingError) { if (isUserFetchingError) {
return <Redirect to={ROUTES.SOMETHING_WENT_WRONG} />; return <Redirect to={ROUTES.SOMETHING_WENT_WRONG} />;
} }
if (isUserFetching) { if (isUserFetching || isLoading) {
return <Spinner tip="Loading..." />; return <Spinner tip="Loading..." />;
} }

View File

@ -2,6 +2,7 @@ import { ConfigProvider } from 'antd';
import getLocalStorageApi from 'api/browser/localstorage/get'; import getLocalStorageApi from 'api/browser/localstorage/get';
import setLocalStorageApi from 'api/browser/localstorage/set'; import setLocalStorageApi from 'api/browser/localstorage/set';
import logEvent from 'api/common/logEvent'; import logEvent from 'api/common/logEvent';
import getAllOrgPreferences from 'api/preferences/getAllOrgPreferences';
import NotFound from 'components/NotFound'; import NotFound from 'components/NotFound';
import Spinner from 'components/Spinner'; import Spinner from 'components/Spinner';
import { FeatureKeys } from 'constants/features'; import { FeatureKeys } from 'constants/features';
@ -24,13 +25,20 @@ import AlertRuleProvider from 'providers/Alert';
import { DashboardProvider } from 'providers/Dashboard/Dashboard'; import { DashboardProvider } from 'providers/Dashboard/Dashboard';
import { QueryBuilderProvider } from 'providers/QueryBuilder'; import { QueryBuilderProvider } from 'providers/QueryBuilder';
import { Suspense, useEffect, useState } from 'react'; import { Suspense, useEffect, useState } from 'react';
import { useQuery } from 'react-query';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { Route, Router, Switch } from 'react-router-dom'; import { Route, Router, Switch } from 'react-router-dom';
import { CompatRouter } from 'react-router-dom-v5-compat';
import { Dispatch } from 'redux'; import { Dispatch } from 'redux';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
import AppActions from 'types/actions'; import AppActions from 'types/actions';
import { UPDATE_FEATURE_FLAG_RESPONSE } from 'types/actions/app'; import {
UPDATE_FEATURE_FLAG_RESPONSE,
UPDATE_IS_FETCHING_ORG_PREFERENCES,
UPDATE_ORG_PREFERENCES,
} from 'types/actions/app';
import AppReducer, { User } from 'types/reducer/app'; import AppReducer, { User } from 'types/reducer/app';
import { USER_ROLES } from 'types/roles';
import { extractDomain, isCloudUser, isEECloudUser } from 'utils/app'; import { extractDomain, isCloudUser, isEECloudUser } from 'utils/app';
import PrivateRoute from './Private'; import PrivateRoute from './Private';
@ -65,6 +73,41 @@ function App(): JSX.Element {
const isPremiumSupportEnabled = const isPremiumSupportEnabled =
useFeatureFlags(FeatureKeys.PREMIUM_SUPPORT)?.active || false; useFeatureFlags(FeatureKeys.PREMIUM_SUPPORT)?.active || false;
const { data: orgPreferences, isLoading: isLoadingOrgPreferences } = useQuery({
queryFn: () => getAllOrgPreferences(),
queryKey: ['getOrgPreferences'],
enabled: isLoggedInState && role === USER_ROLES.ADMIN,
});
useEffect(() => {
if (orgPreferences && !isLoadingOrgPreferences) {
dispatch({
type: UPDATE_IS_FETCHING_ORG_PREFERENCES,
payload: {
isFetchingOrgPreferences: false,
},
});
dispatch({
type: UPDATE_ORG_PREFERENCES,
payload: {
orgPreferences: orgPreferences.payload?.data || null,
},
});
}
}, [orgPreferences, dispatch, isLoadingOrgPreferences]);
useEffect(() => {
if (isLoggedInState && role !== USER_ROLES.ADMIN) {
dispatch({
type: UPDATE_IS_FETCHING_ORG_PREFERENCES,
payload: {
isFetchingOrgPreferences: false,
},
});
}
}, [isLoggedInState, role, dispatch]);
const featureResponse = useGetFeatureFlag((allFlags) => { const featureResponse = useGetFeatureFlag((allFlags) => {
dispatch({ dispatch({
type: UPDATE_FEATURE_FLAG_RESPONSE, type: UPDATE_FEATURE_FLAG_RESPONSE,
@ -182,6 +225,16 @@ function App(): JSX.Element {
}, [isLoggedInState, isOnBasicPlan, user]); }, [isLoggedInState, isOnBasicPlan, user]);
useEffect(() => { useEffect(() => {
if (pathname === ROUTES.ONBOARDING) {
window.Intercom('update', {
hide_default_launcher: true,
});
} else {
window.Intercom('update', {
hide_default_launcher: false,
});
}
trackPageView(pathname); trackPageView(pathname);
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [pathname]); }, [pathname]);
@ -204,6 +257,7 @@ function App(): JSX.Element {
user, user,
licenseData, licenseData,
isPremiumSupportEnabled, isPremiumSupportEnabled,
pathname,
]); ]);
useEffect(() => { useEffect(() => {
@ -239,6 +293,7 @@ function App(): JSX.Element {
return ( return (
<ConfigProvider theme={themeConfig}> <ConfigProvider theme={themeConfig}>
<Router history={history}> <Router history={history}>
<CompatRouter>
<NotificationProvider> <NotificationProvider>
<PrivateRoute> <PrivateRoute>
<ResourceProvider> <ResourceProvider>
@ -269,6 +324,7 @@ function App(): JSX.Element {
</ResourceProvider> </ResourceProvider>
</PrivateRoute> </PrivateRoute>
</NotificationProvider> </NotificationProvider>
</CompatRouter>
</Router> </Router>
</ConfigProvider> </ConfigProvider>
); );

View File

@ -66,6 +66,10 @@ export const Onboarding = Loadable(
() => import(/* webpackChunkName: "Onboarding" */ 'pages/OnboardingPage'), () => import(/* webpackChunkName: "Onboarding" */ 'pages/OnboardingPage'),
); );
export const OrgOnboarding = Loadable(
() => import(/* webpackChunkName: "OrgOnboarding" */ 'pages/OrgOnboarding'),
);
export const DashboardPage = Loadable( export const DashboardPage = Loadable(
() => () =>
import(/* webpackChunkName: "DashboardPage" */ 'pages/DashboardsListPage'), import(/* webpackChunkName: "DashboardPage" */ 'pages/DashboardsListPage'),

View File

@ -32,6 +32,7 @@ import {
OldLogsExplorer, OldLogsExplorer,
Onboarding, Onboarding,
OrganizationSettings, OrganizationSettings,
OrgOnboarding,
PasswordReset, PasswordReset,
PipelinePage, PipelinePage,
ServiceMapPage, ServiceMapPage,
@ -68,6 +69,13 @@ const routes: AppRoutes[] = [
isPrivate: true, isPrivate: true,
key: 'GET_STARTED', key: 'GET_STARTED',
}, },
{
path: ROUTES.ONBOARDING,
exact: false,
component: OrgOnboarding,
isPrivate: true,
key: 'ONBOARDING',
},
{ {
component: LogsIndexToFields, component: LogsIndexToFields,
path: ROUTES.LOGS_INDEX_FIELDS, path: ROUTES.LOGS_INDEX_FIELDS,

View File

@ -4,6 +4,7 @@ export const apiV2 = '/api/v2/';
export const apiV3 = '/api/v3/'; export const apiV3 = '/api/v3/';
export const apiV4 = '/api/v4/'; export const apiV4 = '/api/v4/';
export const gatewayApiV1 = '/api/gateway/v1/'; export const gatewayApiV1 = '/api/gateway/v1/';
export const gatewayApiV2 = '/api/gateway/v2/';
export const apiAlertManager = '/api/alertmanager/'; export const apiAlertManager = '/api/alertmanager/';
export default apiV1; export default apiV1;

View File

@ -15,6 +15,7 @@ import apiV1, {
apiV3, apiV3,
apiV4, apiV4,
gatewayApiV1, gatewayApiV1,
gatewayApiV2,
} from './apiV1'; } from './apiV1';
import { Logout } from './utils'; import { Logout } from './utils';
@ -169,6 +170,19 @@ GatewayApiV1Instance.interceptors.response.use(
GatewayApiV1Instance.interceptors.request.use(interceptorsRequestResponse); GatewayApiV1Instance.interceptors.request.use(interceptorsRequestResponse);
// //
// gateway Api V2
export const GatewayApiV2Instance = axios.create({
baseURL: `${ENVIRONMENT.baseURL}${gatewayApiV2}`,
});
GatewayApiV2Instance.interceptors.response.use(
interceptorsResponse,
interceptorRejected,
);
GatewayApiV2Instance.interceptors.request.use(interceptorsRequestResponse);
//
AxiosAlertManagerInstance.interceptors.response.use( AxiosAlertManagerInstance.interceptors.response.use(
interceptorsResponse, interceptorsResponse,
interceptorRejected, interceptorRejected,

View File

@ -0,0 +1,39 @@
import { ApiBaseInstance } from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { ErrorResponse, SuccessResponse } from 'types/api';
export interface OnboardingStatusResponse {
status: string;
data: {
attribute?: string;
error_message?: string;
status?: string;
}[];
}
const getOnboardingStatus = async (props: {
start: number;
end: number;
endpointService?: string;
}): Promise<SuccessResponse<OnboardingStatusResponse> | ErrorResponse> => {
const { endpointService, ...rest } = props;
try {
const response = await ApiBaseInstance.post(
`/messaging-queues/kafka/onboarding/${endpointService || 'consumers'}`,
rest,
);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler((error as AxiosError) || SOMETHING_WENT_WRONG);
}
};
export default getOnboardingStatus;

View File

@ -0,0 +1,20 @@
import { GatewayApiV2Instance } from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { UpdateProfileProps } from 'types/api/onboarding/types';
const updateProfile = async (
props: UpdateProfileProps,
): Promise<SuccessResponse<UpdateProfileProps> | ErrorResponse> => {
const response = await GatewayApiV2Instance.put('/profiles/me', {
...props,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
};
export default updateProfile;

View File

@ -10,9 +10,12 @@ const updateOrgPreference = async (
): Promise< ): Promise<
SuccessResponse<UpdateOrgPreferenceResponseProps> | ErrorResponse SuccessResponse<UpdateOrgPreferenceResponseProps> | ErrorResponse
> => { > => {
const response = await axios.put(`/org/preferences`, { const response = await axios.put(
`/org/preferences/${preferencePayload.preferenceID}`,
{
preference_value: preferencePayload.value, preference_value: preferencePayload.value,
}); },
);
return { return {
statusCode: 200, statusCode: 200,

View File

@ -0,0 +1,18 @@
import axios from 'api';
import { SuccessResponse } from 'types/api';
import { InviteUsersResponse, UsersProps } from 'types/api/user/inviteUsers';
const inviteUsers = async (
users: UsersProps,
): Promise<SuccessResponse<InviteUsersResponse>> => {
const response = await axios.post(`/invite/bulk`, users);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
};
};
export default inviteUsers;

View File

@ -17,6 +17,7 @@ describe('getLogIndicatorType', () => {
body: 'Sample log Message', body: 'Sample log Message',
resources_string: {}, resources_string: {},
attributesString: {}, attributesString: {},
scope_string: {},
attributes_string: {}, attributes_string: {},
attributesInt: {}, attributesInt: {},
attributesFloat: {}, attributesFloat: {},
@ -40,6 +41,7 @@ describe('getLogIndicatorType', () => {
body: 'Sample log Message', body: 'Sample log Message',
resources_string: {}, resources_string: {},
attributesString: {}, attributesString: {},
scope_string: {},
attributes_string: {}, attributes_string: {},
attributesInt: {}, attributesInt: {},
attributesFloat: {}, attributesFloat: {},
@ -62,6 +64,7 @@ describe('getLogIndicatorType', () => {
body: 'Sample log Message', body: 'Sample log Message',
resources_string: {}, resources_string: {},
attributesString: {}, attributesString: {},
scope_string: {},
attributes_string: {}, attributes_string: {},
attributesInt: {}, attributesInt: {},
attributesFloat: {}, attributesFloat: {},
@ -83,6 +86,7 @@ describe('getLogIndicatorType', () => {
body: 'Sample log', body: 'Sample log',
resources_string: {}, resources_string: {},
attributesString: {}, attributesString: {},
scope_string: {},
attributes_string: { attributes_string: {
log_level: 'INFO' as never, log_level: 'INFO' as never,
}, },
@ -112,6 +116,7 @@ describe('getLogIndicatorTypeForTable', () => {
attributesString: {}, attributesString: {},
attributes_string: {}, attributes_string: {},
attributesInt: {}, attributesInt: {},
scope_string: {},
attributesFloat: {}, attributesFloat: {},
severity_text: 'WARN', severity_text: 'WARN',
}; };
@ -130,6 +135,7 @@ describe('getLogIndicatorTypeForTable', () => {
severity_number: 0, severity_number: 0,
body: 'Sample log message', body: 'Sample log message',
resources_string: {}, resources_string: {},
scope_string: {},
attributesString: {}, attributesString: {},
attributes_string: {}, attributes_string: {},
attributesInt: {}, attributesInt: {},
@ -166,6 +172,7 @@ describe('logIndicatorBySeverityNumber', () => {
body: 'Sample log Message', body: 'Sample log Message',
resources_string: {}, resources_string: {},
attributesString: {}, attributesString: {},
scope_string: {},
attributes_string: {}, attributes_string: {},
attributesInt: {}, attributesInt: {},
attributesFloat: {}, attributesFloat: {},

View File

@ -37,4 +37,8 @@ export enum QueryParams {
partition = 'partition', partition = 'partition',
selectedTimelineQuery = 'selectedTimelineQuery', selectedTimelineQuery = 'selectedTimelineQuery',
ruleType = 'ruleType', ruleType = 'ruleType',
configDetail = 'configDetail',
getStartedSource = 'getStartedSource',
getStartedSourceService = 'getStartedSourceService',
mqServiceView = 'mqServiceView',
} }

View File

@ -8,6 +8,7 @@ const ROUTES = {
TRACE_DETAIL: '/trace/:id', TRACE_DETAIL: '/trace/:id',
TRACES_EXPLORER: '/traces-explorer', TRACES_EXPLORER: '/traces-explorer',
GET_STARTED: '/get-started', GET_STARTED: '/get-started',
ONBOARDING: '/onboarding',
GET_STARTED_APPLICATION_MONITORING: '/get-started/application-monitoring', GET_STARTED_APPLICATION_MONITORING: '/get-started/application-monitoring',
GET_STARTED_LOGS_MANAGEMENT: '/get-started/logs-management', GET_STARTED_LOGS_MANAGEMENT: '/get-started/logs-management',
GET_STARTED_INFRASTRUCTURE_MONITORING: GET_STARTED_INFRASTRUCTURE_MONITORING:

View File

@ -7,6 +7,8 @@
width: calc(100% - 64px); width: calc(100% - 64px);
z-index: 0; z-index: 0;
margin: 0 auto;
.content-container { .content-container {
position: relative; position: relative;
margin: 0 1rem; margin: 0 1rem;

View File

@ -191,6 +191,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
const pageTitle = t(routeKey); const pageTitle = t(routeKey);
const renderFullScreen = const renderFullScreen =
pathname === ROUTES.GET_STARTED || pathname === ROUTES.GET_STARTED ||
pathname === ROUTES.ONBOARDING ||
pathname === ROUTES.GET_STARTED_APPLICATION_MONITORING || pathname === ROUTES.GET_STARTED_APPLICATION_MONITORING ||
pathname === ROUTES.GET_STARTED_INFRASTRUCTURE_MONITORING || pathname === ROUTES.GET_STARTED_INFRASTRUCTURE_MONITORING ||
pathname === ROUTES.GET_STARTED_LOGS_MANAGEMENT || pathname === ROUTES.GET_STARTED_LOGS_MANAGEMENT ||

View File

@ -94,6 +94,7 @@ export const anamolyAlertDefaults: AlertDef = {
matchType: defaultMatchType, matchType: defaultMatchType,
algorithm: defaultAlgorithm, algorithm: defaultAlgorithm,
seasonality: defaultSeasonality, seasonality: defaultSeasonality,
target: 3,
}, },
labels: { labels: {
severity: 'warning', severity: 'warning',

View File

@ -386,8 +386,7 @@ function RuleOptions({
renderThresholdRuleOpts()} renderThresholdRuleOpts()}
<Space direction="vertical" size="large"> <Space direction="vertical" size="large">
{queryCategory !== EQueryType.PROM && {ruleType !== AlertDetectionTypes.ANOMALY_DETECTION_ALERT && (
ruleType !== AlertDetectionTypes.ANOMALY_DETECTION_ALERT && (
<Space direction="horizontal" align="center"> <Space direction="horizontal" align="center">
<Form.Item noStyle name={['condition', 'target']}> <Form.Item noStyle name={['condition', 'target']}>
<InputNumber <InputNumber

View File

@ -73,6 +73,19 @@ export enum AlertDetectionTypes {
ANOMALY_DETECTION_ALERT = 'anomaly_rule', ANOMALY_DETECTION_ALERT = 'anomaly_rule',
} }
const ALERT_SETUP_GUIDE_URLS: Record<AlertTypes, string> = {
[AlertTypes.METRICS_BASED_ALERT]:
'https://signoz.io/docs/alerts-management/metrics-based-alerts/?utm_source=product&utm_medium=alert-creation-page',
[AlertTypes.LOGS_BASED_ALERT]:
'https://signoz.io/docs/alerts-management/log-based-alerts/?utm_source=product&utm_medium=alert-creation-page',
[AlertTypes.TRACES_BASED_ALERT]:
'https://signoz.io/docs/alerts-management/trace-based-alerts/?utm_source=product&utm_medium=alert-creation-page',
[AlertTypes.EXCEPTIONS_BASED_ALERT]:
'https://signoz.io/docs/alerts-management/exceptions-based-alerts/?utm_source=product&utm_medium=alert-creation-page',
[AlertTypes.ANOMALY_BASED_ALERT]:
'https://signoz.io/docs/alerts-management/anomaly-based-alerts/?utm_source=product&utm_medium=alert-creation-page',
};
// eslint-disable-next-line sonarjs/cognitive-complexity // eslint-disable-next-line sonarjs/cognitive-complexity
function FormAlertRules({ function FormAlertRules({
alertType, alertType,
@ -702,6 +715,29 @@ function FormAlertRules({
const isRuleCreated = !ruleId || ruleId === 0; const isRuleCreated = !ruleId || ruleId === 0;
function handleRedirection(option: AlertTypes): void {
let url;
if (
option === AlertTypes.METRICS_BASED_ALERT &&
alertTypeFromURL === AlertDetectionTypes.ANOMALY_DETECTION_ALERT
) {
url = ALERT_SETUP_GUIDE_URLS[AlertTypes.ANOMALY_BASED_ALERT];
} else {
url = ALERT_SETUP_GUIDE_URLS[option];
}
if (url) {
logEvent('Alert: Check example alert clicked', {
dataSource: ALERTS_DATA_SOURCE_MAP[alertDef?.alertType as AlertTypes],
isNewRule: !ruleId || ruleId === 0,
ruleId,
queryType: currentQuery.queryType,
link: url,
});
window.open(url, '_blank');
}
}
useEffect(() => { useEffect(() => {
if (!isRuleCreated) { if (!isRuleCreated) {
logEvent('Alert: Edit page visited', { logEvent('Alert: Edit page visited', {
@ -752,7 +788,11 @@ function FormAlertRules({
)} )}
</div> </div>
<Button className="periscope-btn" icon={<ExternalLink size={14} />}> <Button
className="periscope-btn"
onClick={(): void => handleRedirection(alertDef.alertType as AlertTypes)}
icon={<ExternalLink size={14} />}
>
Alert Setup Guide Alert Setup Guide
</Button> </Button>
</div> </div>

View File

@ -138,6 +138,9 @@ function LabelSelect({
if (e.key === 'Enter' || e.code === 'Enter' || e.key === ':') { if (e.key === 'Enter' || e.code === 'Enter' || e.key === ':') {
send('NEXT'); send('NEXT');
} }
if (state.value === 'Idle') {
send('NEXT');
}
}} }}
bordered={false} bordered={false}
value={currentVal as never} value={currentVal as never}

View File

@ -157,6 +157,11 @@ export const getFieldAttributes = (field: string): IFieldAttributes => {
const stringWithoutPrefix = field.slice('resources_'.length); const stringWithoutPrefix = field.slice('resources_'.length);
const parts = splitOnce(stringWithoutPrefix, '.'); const parts = splitOnce(stringWithoutPrefix, '.');
[dataType, newField] = parts; [dataType, newField] = parts;
} else if (field.startsWith('scope_string')) {
logType = MetricsType.Scope;
const stringWithoutPrefix = field.slice('scope_'.length);
const parts = splitOnce(stringWithoutPrefix, '.');
[dataType, newField] = parts;
} }
return { dataType, newField, logType }; return { dataType, newField, logType };
@ -187,6 +192,7 @@ export const aggregateAttributesResourcesToString = (logData: ILog): string => {
traceId: logData.traceId, traceId: logData.traceId,
attributes: {}, attributes: {},
resources: {}, resources: {},
scope: {},
severity_text: logData.severity_text, severity_text: logData.severity_text,
severity_number: logData.severity_number, severity_number: logData.severity_number,
}; };
@ -198,6 +204,9 @@ export const aggregateAttributesResourcesToString = (logData: ILog): string => {
} else if (key.startsWith('resources_')) { } else if (key.startsWith('resources_')) {
outputJson.resources = outputJson.resources || {}; outputJson.resources = outputJson.resources || {};
Object.assign(outputJson.resources, logData[key as keyof ILog]); Object.assign(outputJson.resources, logData[key as keyof ILog]);
} else if (key.startsWith('scope_string')) {
outputJson.scope = outputJson.scope || {};
Object.assign(outputJson.scope, logData[key as keyof ILog]);
} else { } else {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore

View File

@ -53,6 +53,7 @@ export enum KeyOperationTableHeader {
export enum MetricsType { export enum MetricsType {
Tag = 'tag', Tag = 'tag',
Resource = 'resource', Resource = 'resource',
Scope = 'scope',
} }
export enum WidgetKeys { export enum WidgetKeys {

View File

@ -0,0 +1,32 @@
&nbsp;
Once you are done instrumenting your Java application, you can run it using the below commands
**Note:**
- Ensure you have Java and Maven installed. Compile your Java consumer applications: Ensure your consumer apps are compiled and ready to run.
**Run Consumer App with Java Agent:**
```bash
java -javaagent:/path/to/opentelemetry-javaagent.jar \
-Dotel.service.name=consumer-svc \
-Dotel.traces.exporter=otlp \
-Dotel.metrics.exporter=otlp \
-Dotel.logs.exporter=otlp \
-Dotel.instrumentation.kafka.producer-propagation.enabled=true \
-Dotel.instrumentation.kafka.experimental-span-attributes=true \
-Dotel.instrumentation.kafka.metric-reporter.enabled=true \
-jar /path/to/your/consumer.jar
```
<path> - update it to the path where you downloaded the Java JAR agent in previous step
<my-app> - Jar file of your application
&nbsp;
**Note:**
- In case you're dockerising your application, make sure to dockerise it along with OpenTelemetry instrumentation done in previous step.
&nbsp;
If you encounter any difficulties, please consult the [troubleshooting section](https://signoz.io/docs/instrumentation/springboot/#troubleshooting-your-installation) for assistance.

View File

@ -0,0 +1,29 @@
&nbsp;
Once you are done instrumenting your Java application, you can run it using the below commands
**Note:**
- Ensure you have Java and Maven installed. Compile your Java producer applications: Ensure your producer apps are compiled and ready to run.
**Run Producer App with Java Agent:**
```bash
java -javaagent:/path/to/opentelemetry-javaagent.jar \
-Dotel.service.name=producer-svc \
-Dotel.traces.exporter=otlp \
-Dotel.metrics.exporter=otlp \
-Dotel.logs.exporter=otlp \
-jar /path/to/your/producer.jar
```
<path> - update it to the path where you downloaded the Java JAR agent in previous step
<my-app> - Jar file of your application
&nbsp;
**Note:**
- In case you're dockerising your application, make sure to dockerise it along with OpenTelemetry instrumentation done in previous step.
&nbsp;
If you encounter any difficulties, please consult the [troubleshooting section](https://signoz.io/docs/instrumentation/springboot/#troubleshooting-your-installation) for assistance.

View File

@ -312,7 +312,7 @@ export default function Onboarding(): JSX.Element {
<div <div
onClick={(): void => { onClick={(): void => {
logEvent('Onboarding V2: Skip Button Clicked', {}); logEvent('Onboarding V2: Skip Button Clicked', {});
history.push('/'); history.push(ROUTES.APPLICATION);
}} }}
className="skip-to-console" className="skip-to-console"
> >

View File

@ -6,11 +6,16 @@ import {
LoadingOutlined, LoadingOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import logEvent from 'api/common/logEvent'; import logEvent from 'api/common/logEvent';
import { QueryParams } from 'constants/query';
import Header from 'container/OnboardingContainer/common/Header/Header'; import Header from 'container/OnboardingContainer/common/Header/Header';
import { useOnboardingContext } from 'container/OnboardingContainer/context/OnboardingContext'; import { useOnboardingContext } from 'container/OnboardingContainer/context/OnboardingContext';
import { useOnboardingStatus } from 'hooks/messagingQueue / onboarding/useOnboardingStatus';
import { useQueryService } from 'hooks/useQueryService'; import { useQueryService } from 'hooks/useQueryService';
import useResourceAttribute from 'hooks/useResourceAttribute'; import useResourceAttribute from 'hooks/useResourceAttribute';
import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils'; import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils';
import useUrlQuery from 'hooks/useUrlQuery';
import MessagingQueueHealthCheck from 'pages/MessagingQueues/MessagingQueueHealthCheck/MessagingQueueHealthCheck';
import { getAttributeDataFromOnboardingStatus } from 'pages/MessagingQueues/MessagingQueuesUtils';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
@ -27,6 +32,12 @@ export default function ConnectionStatus(): JSX.Element {
GlobalReducer GlobalReducer
>((state) => state.globalTime); >((state) => state.globalTime);
const urlQuery = useUrlQuery();
const getStartedSource = urlQuery.get(QueryParams.getStartedSource);
const getStartedSourceService = urlQuery.get(
QueryParams.getStartedSourceService,
);
const { const {
serviceName, serviceName,
selectedDataSource, selectedDataSource,
@ -57,8 +68,69 @@ export default function ConnectionStatus(): JSX.Element {
maxTime, maxTime,
selectedTime, selectedTime,
selectedTags, selectedTags,
options: {
enabled: getStartedSource !== 'kafka',
},
}); });
const [pollInterval, setPollInterval] = useState<number | false>(10000);
const {
data: onbData,
error: onbErr,
isFetching: onbFetching,
} = useOnboardingStatus(
{
enabled: getStartedSource === 'kafka',
refetchInterval: pollInterval,
},
getStartedSourceService || '',
'query-key-onboarding-status',
);
const [
shouldRetryOnboardingCall,
setShouldRetryOnboardingCall,
] = useState<boolean>(false);
useEffect(() => {
// runs only when the caller is coming from 'kafka' i.e. coming from Messaging Queues - setup helper
if (getStartedSource === 'kafka') {
if (onbData?.statusCode !== 200) {
setShouldRetryOnboardingCall(true);
} else if (onbData?.payload?.status === 'success') {
const attributeData = getAttributeDataFromOnboardingStatus(
onbData?.payload,
);
if (attributeData.overallStatus === 'success') {
setLoading(false);
setIsReceivingData(true);
setPollInterval(false);
} else {
setShouldRetryOnboardingCall(true);
}
}
}
}, [
shouldRetryOnboardingCall,
onbData,
onbErr,
onbFetching,
getStartedSource,
]);
useEffect(() => {
if (retryCount < 0 && getStartedSource === 'kafka') {
setPollInterval(false);
setLoading(false);
}
}, [retryCount, getStartedSource]);
useEffect(() => {
if (getStartedSource === 'kafka' && !onbFetching) {
setRetryCount((prevCount) => prevCount - 1);
}
}, [getStartedSource, onbData, onbFetching]);
const renderDocsReference = (): JSX.Element => { const renderDocsReference = (): JSX.Element => {
switch (selectedDataSource?.name) { switch (selectedDataSource?.name) {
case 'java': case 'java':
@ -192,6 +264,7 @@ export default function ConnectionStatus(): JSX.Element {
useEffect(() => { useEffect(() => {
let pollingTimer: string | number | NodeJS.Timer | undefined; let pollingTimer: string | number | NodeJS.Timer | undefined;
if (getStartedSource !== 'kafka') {
if (loading) { if (loading) {
pollingTimer = setInterval(() => { pollingTimer = setInterval(() => {
// Trigger a refetch with the updated parameters // Trigger a refetch with the updated parameters
@ -212,6 +285,7 @@ export default function ConnectionStatus(): JSX.Element {
} else if (!loading && pollingTimer) { } else if (!loading && pollingTimer) {
clearInterval(pollingTimer); clearInterval(pollingTimer);
} }
}
// Clean up the interval when the component unmounts // Clean up the interval when the component unmounts
return (): void => { return (): void => {
@ -221,15 +295,24 @@ export default function ConnectionStatus(): JSX.Element {
}, [refetch, selectedTags, selectedTime, loading]); }, [refetch, selectedTags, selectedTime, loading]);
useEffect(() => { useEffect(() => {
if (getStartedSource !== 'kafka') {
verifyApplicationData(data); verifyApplicationData(data);
}
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [isServiceLoading, data, error, isError]); }, [isServiceLoading, data, error, isError]);
useEffect(() => { useEffect(() => {
if (getStartedSource !== 'kafka') {
refetch(); refetch();
}
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
const isQueryServiceLoading = useMemo(
() => isServiceLoading || loading || onbFetching,
[isServiceLoading, loading, onbFetching],
);
return ( return (
<div className="connection-status-container"> <div className="connection-status-container">
<div className="full-docs-link">{renderDocsReference()}</div> <div className="full-docs-link">{renderDocsReference()}</div>
@ -250,30 +333,42 @@ export default function ConnectionStatus(): JSX.Element {
<div className="label"> Status </div> <div className="label"> Status </div>
<div className="status"> <div className="status">
{(loading || isServiceLoading) && <LoadingOutlined />} {isQueryServiceLoading && <LoadingOutlined />}
{!(loading || isServiceLoading) && isReceivingData && ( {!isQueryServiceLoading &&
isReceivingData &&
(getStartedSource !== 'kafka' ? (
<> <>
<CheckCircleTwoTone twoToneColor="#52c41a" /> <CheckCircleTwoTone twoToneColor="#52c41a" />
<span> Success </span> <span> Success </span>
</> </>
)} ) : (
{!(loading || isServiceLoading) && !isReceivingData && ( <MessagingQueueHealthCheck
serviceToInclude={[getStartedSourceService || '']}
/>
))}
{!isQueryServiceLoading &&
!isReceivingData &&
(getStartedSource !== 'kafka' ? (
<> <>
<CloseCircleTwoTone twoToneColor="#e84749" /> <CloseCircleTwoTone twoToneColor="#e84749" />
<span> Failed </span> <span> Failed </span>
</> </>
)} ) : (
<MessagingQueueHealthCheck
serviceToInclude={[getStartedSourceService || '']}
/>
))}
</div> </div>
</div> </div>
<div className="details-info"> <div className="details-info">
<div className="label"> Details </div> <div className="label"> Details </div>
<div className="details"> <div className="details">
{(loading || isServiceLoading) && <div> Waiting for Update </div>} {isQueryServiceLoading && <div> Waiting for Update </div>}
{!(loading || isServiceLoading) && isReceivingData && ( {!isQueryServiceLoading && isReceivingData && (
<div> Received data from the application successfully. </div> <div> Received data from the application successfully. </div>
)} )}
{!(loading || isServiceLoading) && !isReceivingData && ( {!isQueryServiceLoading && !isReceivingData && (
<div> Could not detect the install </div> <div> Could not detect the install </div>
)} )}
</div> </div>

View File

@ -75,3 +75,10 @@ div[class*='-setup-instructions-container'] {
color: var(--bg-slate-500); color: var(--bg-slate-500);
} }
} }
.supported-languages-container {
.disabled {
cursor: not-allowed;
opacity: 0.5;
}
}

View File

@ -6,15 +6,21 @@ import { LoadingOutlined } from '@ant-design/icons';
import { Button, Card, Form, Input, Select, Space, Typography } from 'antd'; import { Button, Card, Form, Input, Select, Space, Typography } from 'antd';
import logEvent from 'api/common/logEvent'; import logEvent from 'api/common/logEvent';
import cx from 'classnames'; import cx from 'classnames';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import { useOnboardingContext } from 'container/OnboardingContainer/context/OnboardingContext'; import { useOnboardingContext } from 'container/OnboardingContainer/context/OnboardingContext';
import { useCases } from 'container/OnboardingContainer/OnboardingContainer'; import {
ModulesMap,
useCases,
} from 'container/OnboardingContainer/OnboardingContainer';
import { import {
getDataSources, getDataSources,
getSupportedFrameworks, getSupportedFrameworks,
hasFrameworks, hasFrameworks,
messagingQueueKakfaSupportedDataSources,
} from 'container/OnboardingContainer/utils/dataSourceUtils'; } from 'container/OnboardingContainer/utils/dataSourceUtils';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
import useUrlQuery from 'hooks/useUrlQuery';
import { Blocks, Check } from 'lucide-react'; import { Blocks, Check } from 'lucide-react';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -33,6 +39,8 @@ export default function DataSource(): JSX.Element {
const { t } = useTranslation(['common']); const { t } = useTranslation(['common']);
const history = useHistory(); const history = useHistory();
const getStartedSource = useUrlQuery().get(QueryParams.getStartedSource);
const { const {
serviceName, serviceName,
selectedModule, selectedModule,
@ -44,6 +52,9 @@ export default function DataSource(): JSX.Element {
updateSelectedFramework, updateSelectedFramework,
} = useOnboardingContext(); } = useOnboardingContext();
const isKafkaAPM =
getStartedSource === 'kafka' && selectedModule?.id === ModulesMap.APM;
const [supportedDataSources, setSupportedDataSources] = useState< const [supportedDataSources, setSupportedDataSources] = useState<
DataSourceType[] DataSourceType[]
>([]); >([]);
@ -150,13 +161,19 @@ export default function DataSource(): JSX.Element {
className={cx( className={cx(
'supported-language', 'supported-language',
selectedDataSource?.name === dataSource.name ? 'selected' : '', selectedDataSource?.name === dataSource.name ? 'selected' : '',
isKafkaAPM &&
!messagingQueueKakfaSupportedDataSources.includes(dataSource?.id || '')
? 'disabled'
: '',
)} )}
key={dataSource.name} key={dataSource.name}
onClick={(): void => { onClick={(): void => {
if (!isKafkaAPM) {
updateSelectedFramework(null); updateSelectedFramework(null);
updateSelectedEnvironment(null); updateSelectedEnvironment(null);
updateSelectedDataSource(dataSource); updateSelectedDataSource(dataSource);
form.setFieldsValue({ selectFramework: null }); form.setFieldsValue({ selectFramework: null });
}
}} }}
> >
<div> <div>

View File

@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */ /* eslint-disable @typescript-eslint/ban-ts-comment */
import { MarkdownRenderer } from 'components/MarkdownRenderer/MarkdownRenderer'; import { MarkdownRenderer } from 'components/MarkdownRenderer/MarkdownRenderer';
import { QueryParams } from 'constants/query';
import { ApmDocFilePaths } from 'container/OnboardingContainer/constants/apmDocFilePaths'; import { ApmDocFilePaths } from 'container/OnboardingContainer/constants/apmDocFilePaths';
import { AwsMonitoringDocFilePaths } from 'container/OnboardingContainer/constants/awsMonitoringDocFilePaths'; import { AwsMonitoringDocFilePaths } from 'container/OnboardingContainer/constants/awsMonitoringDocFilePaths';
import { AzureMonitoringDocFilePaths } from 'container/OnboardingContainer/constants/azureMonitoringDocFilePaths'; import { AzureMonitoringDocFilePaths } from 'container/OnboardingContainer/constants/azureMonitoringDocFilePaths';
@ -10,6 +11,7 @@ import {
useOnboardingContext, useOnboardingContext,
} from 'container/OnboardingContainer/context/OnboardingContext'; } from 'container/OnboardingContainer/context/OnboardingContext';
import { ModulesMap } from 'container/OnboardingContainer/OnboardingContainer'; import { ModulesMap } from 'container/OnboardingContainer/OnboardingContainer';
import useUrlQuery from 'hooks/useUrlQuery';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
export interface IngestionInfoProps { export interface IngestionInfoProps {
@ -31,6 +33,12 @@ export default function MarkdownStep(): JSX.Element {
const [markdownContent, setMarkdownContent] = useState(''); const [markdownContent, setMarkdownContent] = useState('');
const urlQuery = useUrlQuery();
const getStartedSource = urlQuery.get(QueryParams.getStartedSource);
const getStartedSourceService = urlQuery.get(
QueryParams.getStartedSourceService,
);
const { step } = activeStep; const { step } = activeStep;
const getFilePath = (): any => { const getFilePath = (): any => {
@ -54,6 +62,12 @@ export default function MarkdownStep(): JSX.Element {
path += `_${step?.id}`; path += `_${step?.id}`;
if (
getStartedSource === 'kafka' &&
path === 'APM_java_springBoot_kubernetes_recommendedSteps_runApplication' // todo: Sagar - Make this generic logic in followup PRs
) {
path += `_${getStartedSourceService}`;
}
return path; return path;
}; };

View File

@ -252,6 +252,8 @@ import APM_java_springBoot_docker_recommendedSteps_runApplication from '../Modul
import APM_java_springBoot_kubernetes_recommendedSteps_setupOtelCollector from '../Modules/APM/Java/md-docs/SpringBoot/Kubernetes/springBoot-kubernetes-installOtelCollector.md'; import APM_java_springBoot_kubernetes_recommendedSteps_setupOtelCollector from '../Modules/APM/Java/md-docs/SpringBoot/Kubernetes/springBoot-kubernetes-installOtelCollector.md';
import APM_java_springBoot_kubernetes_recommendedSteps_instrumentApplication from '../Modules/APM/Java/md-docs/SpringBoot/Kubernetes/springBoot-kubernetes-instrumentApplication.md'; import APM_java_springBoot_kubernetes_recommendedSteps_instrumentApplication from '../Modules/APM/Java/md-docs/SpringBoot/Kubernetes/springBoot-kubernetes-instrumentApplication.md';
import APM_java_springBoot_kubernetes_recommendedSteps_runApplication from '../Modules/APM/Java/md-docs/SpringBoot/Kubernetes/springBoot-kubernetes-runApplication.md'; import APM_java_springBoot_kubernetes_recommendedSteps_runApplication from '../Modules/APM/Java/md-docs/SpringBoot/Kubernetes/springBoot-kubernetes-runApplication.md';
import APM_java_springBoot_kubernetes_recommendedSteps_runApplication_consumers from '../Modules/APM/Java/md-docs/SpringBoot/Kubernetes/springBoot-kubernetes-runApplication-consumers.md';
import APM_java_springBoot_kubernetes_recommendedSteps_runApplication_producers from '../Modules/APM/Java/md-docs/SpringBoot/Kubernetes/springBoot-kubernetes-runApplication-producers.md';
// SpringBoot-LinuxAMD64-quickstart // SpringBoot-LinuxAMD64-quickstart
import APM_java_springBoot_linuxAMD64_quickStart_instrumentApplication from '../Modules/APM/Java/md-docs/SpringBoot/LinuxAMD64/QuickStart/springBoot-linuxamd64-quickStart-instrumentApplication.md'; import APM_java_springBoot_linuxAMD64_quickStart_instrumentApplication from '../Modules/APM/Java/md-docs/SpringBoot/LinuxAMD64/QuickStart/springBoot-linuxamd64-quickStart-instrumentApplication.md';
import APM_java_springBoot_linuxAMD64_quickStart_runApplication from '../Modules/APM/Java/md-docs/SpringBoot/LinuxAMD64/QuickStart/springBoot-linuxamd64-quickStart-runApplication.md'; import APM_java_springBoot_linuxAMD64_quickStart_runApplication from '../Modules/APM/Java/md-docs/SpringBoot/LinuxAMD64/QuickStart/springBoot-linuxamd64-quickStart-runApplication.md';
@ -1053,6 +1055,8 @@ export const ApmDocFilePaths = {
APM_java_springBoot_kubernetes_recommendedSteps_setupOtelCollector, APM_java_springBoot_kubernetes_recommendedSteps_setupOtelCollector,
APM_java_springBoot_kubernetes_recommendedSteps_instrumentApplication, APM_java_springBoot_kubernetes_recommendedSteps_instrumentApplication,
APM_java_springBoot_kubernetes_recommendedSteps_runApplication, APM_java_springBoot_kubernetes_recommendedSteps_runApplication,
APM_java_springBoot_kubernetes_recommendedSteps_runApplication_producers,
APM_java_springBoot_kubernetes_recommendedSteps_runApplication_consumers,
// SpringBoot-LinuxAMD64-recommended // SpringBoot-LinuxAMD64-recommended
APM_java_springBoot_linuxAMD64_recommendedSteps_setupOtelCollector, APM_java_springBoot_linuxAMD64_recommendedSteps_setupOtelCollector,

View File

@ -399,3 +399,5 @@ export const moduleRouteMap = {
[ModulesMap.AwsMonitoring]: ROUTES.GET_STARTED_AWS_MONITORING, [ModulesMap.AwsMonitoring]: ROUTES.GET_STARTED_AWS_MONITORING,
[ModulesMap.AzureMonitoring]: ROUTES.GET_STARTED_AZURE_MONITORING, [ModulesMap.AzureMonitoring]: ROUTES.GET_STARTED_AZURE_MONITORING,
}; };
export const messagingQueueKakfaSupportedDataSources = ['java'];

View File

@ -0,0 +1,235 @@
/* eslint-disable sonarjs/cognitive-complexity */
import '../OnboardingQuestionaire.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Button, Input, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import { ArrowLeft, ArrowRight, CheckCircle } from 'lucide-react';
import { useEffect, useState } from 'react';
export interface SignozDetails {
hearAboutSignoz: string | null;
interestInSignoz: string | null;
otherInterestInSignoz: string | null;
otherAboutSignoz: string | null;
}
interface AboutSigNozQuestionsProps {
signozDetails: SignozDetails;
setSignozDetails: (details: SignozDetails) => void;
onNext: () => void;
onBack: () => void;
}
const hearAboutSignozOptions: Record<string, string> = {
search: 'Google / Search',
hackerNews: 'Hacker News',
linkedin: 'LinkedIn',
twitter: 'Twitter',
reddit: 'Reddit',
colleaguesFriends: 'Colleagues / Friends',
};
const interestedInOptions: Record<string, string> = {
savingCosts: 'Saving costs',
otelNativeStack: 'Interested in Otel-native stack',
allInOne: 'All in one (Logs, Metrics & Traces)',
};
export function AboutSigNozQuestions({
signozDetails,
setSignozDetails,
onNext,
onBack,
}: AboutSigNozQuestionsProps): JSX.Element {
const [hearAboutSignoz, setHearAboutSignoz] = useState<string | null>(
signozDetails?.hearAboutSignoz || null,
);
const [otherAboutSignoz, setOtherAboutSignoz] = useState<string>(
signozDetails?.otherAboutSignoz || '',
);
const [interestInSignoz, setInterestInSignoz] = useState<string | null>(
signozDetails?.interestInSignoz || null,
);
const [otherInterestInSignoz, setOtherInterestInSignoz] = useState<string>(
signozDetails?.otherInterestInSignoz || '',
);
const [isNextDisabled, setIsNextDisabled] = useState<boolean>(true);
useEffect((): void => {
if (
hearAboutSignoz !== null &&
(hearAboutSignoz !== 'Others' || otherAboutSignoz !== '') &&
interestInSignoz !== null &&
(interestInSignoz !== 'Others' || otherInterestInSignoz !== '')
) {
setIsNextDisabled(false);
} else {
setIsNextDisabled(true);
}
}, [
hearAboutSignoz,
otherAboutSignoz,
interestInSignoz,
otherInterestInSignoz,
]);
const handleOnNext = (): void => {
setSignozDetails({
hearAboutSignoz,
otherAboutSignoz,
interestInSignoz,
otherInterestInSignoz,
});
logEvent('Org Onboarding: Answered', {
hearAboutSignoz,
otherAboutSignoz,
interestInSignoz,
otherInterestInSignoz,
});
onNext();
};
const handleOnBack = (): void => {
setSignozDetails({
hearAboutSignoz,
otherAboutSignoz,
interestInSignoz,
otherInterestInSignoz,
});
onBack();
};
return (
<div className="questions-container">
<Typography.Title level={3} className="title">
Tell Us About Your Interest in SigNoz
</Typography.Title>
<Typography.Paragraph className="sub-title">
We&apos;d love to know a little bit about you and your interest in SigNoz
</Typography.Paragraph>
<div className="questions-form-container">
<div className="questions-form">
<div className="form-group">
<div className="question">Where did you hear about SigNoz?</div>
<div className="two-column-grid">
{Object.keys(hearAboutSignozOptions).map((option: string) => (
<Button
key={option}
type="primary"
className={`onboarding-questionaire-button ${
hearAboutSignoz === option ? 'active' : ''
}`}
onClick={(): void => setHearAboutSignoz(option)}
>
{hearAboutSignozOptions[option]}
{hearAboutSignoz === option && (
<CheckCircle size={12} color={Color.BG_FOREST_500} />
)}
</Button>
))}
{hearAboutSignoz === 'Others' ? (
<Input
type="text"
className="onboarding-questionaire-other-input"
placeholder="How you got to know about us"
value={otherAboutSignoz}
autoFocus
addonAfter={
otherAboutSignoz !== '' ? (
<CheckCircle size={12} color={Color.BG_FOREST_500} />
) : (
''
)
}
onChange={(e): void => setOtherAboutSignoz(e.target.value)}
/>
) : (
<Button
type="primary"
className={`onboarding-questionaire-button ${
hearAboutSignoz === 'Others' ? 'active' : ''
}`}
onClick={(): void => setHearAboutSignoz('Others')}
>
Others
</Button>
)}
</div>
</div>
<div className="form-group">
<div className="question">What got you interested in SigNoz?</div>
<div className="two-column-grid">
{Object.keys(interestedInOptions).map((option: string) => (
<Button
key={option}
type="primary"
className={`onboarding-questionaire-button ${
interestInSignoz === option ? 'active' : ''
}`}
onClick={(): void => setInterestInSignoz(option)}
>
{interestedInOptions[option]}
{interestInSignoz === option && (
<CheckCircle size={12} color={Color.BG_FOREST_500} />
)}
</Button>
))}
{interestInSignoz === 'Others' ? (
<Input
type="text"
className="onboarding-questionaire-other-input"
placeholder="Please specify your interest"
value={otherInterestInSignoz}
autoFocus
addonAfter={
otherInterestInSignoz !== '' ? (
<CheckCircle size={12} color={Color.BG_FOREST_500} />
) : (
''
)
}
onChange={(e): void => setOtherInterestInSignoz(e.target.value)}
/>
) : (
<Button
type="primary"
className={`onboarding-questionaire-button ${
interestInSignoz === 'Others' ? 'active' : ''
}`}
onClick={(): void => setInterestInSignoz('Others')}
>
Others
</Button>
)}
</div>
</div>
</div>
<div className="next-prev-container">
<Button type="default" className="next-button" onClick={handleOnBack}>
<ArrowLeft size={14} />
Back
</Button>
<Button
type="primary"
className={`next-button ${isNextDisabled ? 'disabled' : ''}`}
onClick={handleOnNext}
disabled={isNextDisabled}
>
Next
<ArrowRight size={14} />
</Button>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,122 @@
.team-member-container {
display: flex;
align-items: center;
.team-member-role-select {
width: 20%;
.ant-select-selector {
border: 1px solid #1d212d;
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
}
}
.team-member-email-input {
width: 80%;
background-color: #121317;
border-top-right-radius: 0px;
border-bottom-right-radius: 0px;
.ant-input,
.ant-input-group-addon {
background-color: #121317 !important;
border-right: 0px;
border-top-right-radius: 0px;
border-bottom-right-radius: 0px;
}
}
}
.questions-form-container {
.error-message-container,
.success-message-container,
.partially-sent-invites-container {
border-radius: 4px;
width: 100%;
display: flex;
align-items: center;
.error-message,
.success-message {
font-size: 12px;
font-weight: 400;
display: flex;
align-items: center;
gap: 8px;
}
}
.invite-users-error-message-container,
.invite-users-success-message-container {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 8px;
.success-message {
color: var(--bg-success-500, #00b37e);
}
}
.partially-sent-invites-container {
margin-top: 16px;
padding: 8px;
border: 1px solid #1d212d;
background-color: #121317;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 8px;
.partially-sent-invites-message {
color: var(--bg-warning-500, #fbbd23);
font-size: 12px;
font-weight: 400;
display: flex;
align-items: center;
gap: 8px;
}
}
}
.lightMode {
.team-member-container {
.team-member-role-select {
.ant-select-selector {
border: 1px solid var(--bg-vanilla-300);
}
}
.team-member-email-input {
background-color: var(--bg-vanilla-100);
.ant-input,
.ant-input-group-addon {
background-color: var(--bg-vanilla-100) !important;
}
}
}
.questions-form-container {
.invite-users-error-message-container,
.invite-users-success-message-container {
.success-message {
color: var(--bg-success-500, #00b37e);
}
}
.partially-sent-invites-container {
border: 1px solid var(--bg-vanilla-300);
background-color: var(--bg-vanilla-100);
.partially-sent-invites-message {
color: var(--bg-warning-500, #fbbd23);
}
}
}
}

View File

@ -0,0 +1,450 @@
import './InviteTeamMembers.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Button, Input, Select, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import inviteUsers from 'api/user/inviteUsers';
import { AxiosError } from 'axios';
import { cloneDeep, debounce, isEmpty } from 'lodash-es';
import {
ArrowLeft,
ArrowRight,
CheckCircle,
Loader2,
Plus,
TriangleAlert,
X,
} from 'lucide-react';
import { useCallback, useEffect, useState } from 'react';
import { useMutation } from 'react-query';
import { SuccessResponse } from 'types/api';
import {
FailedInvite,
InviteUsersResponse,
SuccessfulInvite,
} from 'types/api/user/inviteUsers';
import { v4 as uuid } from 'uuid';
interface TeamMember {
email: string;
role: string;
name: string;
frontendBaseUrl: string;
id: string;
}
interface InviteTeamMembersProps {
isLoading: boolean;
teamMembers: TeamMember[] | null;
setTeamMembers: (teamMembers: TeamMember[]) => void;
onNext: () => void;
onBack: () => void;
}
function InviteTeamMembers({
isLoading,
teamMembers,
setTeamMembers,
onNext,
onBack,
}: InviteTeamMembersProps): JSX.Element {
const [teamMembersToInvite, setTeamMembersToInvite] = useState<
TeamMember[] | null
>(teamMembers);
const [emailValidity, setEmailValidity] = useState<Record<string, boolean>>(
{},
);
const [hasInvalidEmails, setHasInvalidEmails] = useState<boolean>(false);
const [hasErrors, setHasErrors] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
const [inviteUsersErrorResponse, setInviteUsersErrorResponse] = useState<
string[] | null
>(null);
const [inviteUsersSuccessResponse, setInviteUsersSuccessResponse] = useState<
string[] | null
>(null);
const [disableNextButton, setDisableNextButton] = useState<boolean>(false);
const defaultTeamMember: TeamMember = {
email: '',
role: 'EDITOR',
name: '',
frontendBaseUrl: window.location.origin,
id: '',
};
useEffect(() => {
if (isEmpty(teamMembers)) {
const teamMember = {
...defaultTeamMember,
id: uuid(),
};
setTeamMembersToInvite([teamMember]);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [teamMembers]);
const handleAddTeamMember = (): void => {
const newTeamMember = {
...defaultTeamMember,
id: uuid(),
};
setTeamMembersToInvite((prev) => [...(prev || []), newTeamMember]);
};
const handleRemoveTeamMember = (id: string): void => {
setTeamMembersToInvite((prev) => (prev || []).filter((m) => m.id !== id));
};
// Validation function to check all users
const validateAllUsers = (): boolean => {
let isValid = true;
const updatedValidity: Record<string, boolean> = {};
teamMembersToInvite?.forEach((member) => {
const emailValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(member.email);
if (!emailValid || !member.email) {
isValid = false;
setHasInvalidEmails(true);
}
updatedValidity[member.id!] = emailValid;
});
setEmailValidity(updatedValidity);
return isValid;
};
const parseInviteUsersSuccessResponse = (
response: SuccessfulInvite[],
): string[] => response.map((invite) => `${invite.email} - Invite Sent`);
const parseInviteUsersErrorResponse = (response: FailedInvite[]): string[] =>
response.map((invite) => `${invite.email} - ${invite.error}`);
const handleError = (error: AxiosError): void => {
const errorMessage = error.response?.data as InviteUsersResponse;
if (errorMessage?.status === 'failure') {
setHasErrors(true);
const failedInvitesErrorResponse = parseInviteUsersErrorResponse(
errorMessage.failed_invites,
);
setInviteUsersErrorResponse(failedInvitesErrorResponse);
}
};
const handleInviteUsersSuccess = (
response: SuccessResponse<InviteUsersResponse>,
): void => {
const inviteUsersResponse = response.payload as InviteUsersResponse;
if (inviteUsersResponse?.status === 'success') {
const successfulInvites = parseInviteUsersSuccessResponse(
inviteUsersResponse.successful_invites,
);
setDisableNextButton(true);
setError(null);
setHasErrors(false);
setInviteUsersErrorResponse(null);
setInviteUsersSuccessResponse(successfulInvites);
logEvent('Org Onboarding: Invite Team Members Success', {
teamMembers: teamMembersToInvite,
totalInvites: inviteUsersResponse.summary.total_invites,
successfulInvites: inviteUsersResponse.summary.successful_invites,
failedInvites: inviteUsersResponse.summary.failed_invites,
});
setTimeout(() => {
setDisableNextButton(false);
onNext();
}, 1000);
} else if (inviteUsersResponse?.status === 'partial_success') {
const successfulInvites = parseInviteUsersSuccessResponse(
inviteUsersResponse.successful_invites,
);
setInviteUsersSuccessResponse(successfulInvites);
logEvent('Org Onboarding: Invite Team Members Partial Success', {
teamMembers: teamMembersToInvite,
totalInvites: inviteUsersResponse.summary.total_invites,
successfulInvites: inviteUsersResponse.summary.successful_invites,
failedInvites: inviteUsersResponse.summary.failed_invites,
});
if (inviteUsersResponse.failed_invites.length > 0) {
setHasErrors(true);
setInviteUsersErrorResponse(
parseInviteUsersErrorResponse(inviteUsersResponse.failed_invites),
);
}
}
};
const { mutate: sendInvites, isLoading: isSendingInvites } = useMutation(
inviteUsers,
{
onSuccess: (response: SuccessResponse<InviteUsersResponse>): void => {
handleInviteUsersSuccess(response);
},
onError: (error: AxiosError): void => {
logEvent('Org Onboarding: Invite Team Members Failed', {
teamMembers: teamMembersToInvite,
});
handleError(error);
},
},
);
const handleNext = (): void => {
if (validateAllUsers()) {
setTeamMembers(teamMembersToInvite || []);
setHasInvalidEmails(false);
setError(null);
setHasErrors(false);
setInviteUsersErrorResponse(null);
setInviteUsersSuccessResponse(null);
sendInvites({
users: teamMembersToInvite || [],
});
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
const debouncedValidateEmail = useCallback(
debounce((email: string, memberId: string) => {
const isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
setEmailValidity((prev) => ({ ...prev, [memberId]: isValid }));
}, 500),
[],
);
const handleEmailChange = (
e: React.ChangeEvent<HTMLInputElement>,
member: TeamMember,
): void => {
const { value } = e.target;
const updatedMembers = cloneDeep(teamMembersToInvite || []);
const memberToUpdate = updatedMembers.find((m) => m.id === member.id);
if (memberToUpdate) {
memberToUpdate.email = value;
setTeamMembersToInvite(updatedMembers);
debouncedValidateEmail(value, member.id!);
}
};
const handleRoleChange = (role: string, member: TeamMember): void => {
const updatedMembers = cloneDeep(teamMembersToInvite || []);
const memberToUpdate = updatedMembers.find((m) => m.id === member.id);
if (memberToUpdate) {
memberToUpdate.role = role;
setTeamMembersToInvite(updatedMembers);
}
};
const handleDoLater = (): void => {
logEvent('Org Onboarding: Clicked Do Later', {
currentPageID: 4,
});
onNext();
};
return (
<div className="questions-container">
<Typography.Title level={3} className="title">
Invite your team members
</Typography.Title>
<Typography.Paragraph className="sub-title">
The more your team uses SigNoz, the stronger your observability. Share
dashboards, collaborate on alerts, and troubleshoot faster together.
</Typography.Paragraph>
<div className="questions-form-container">
<div className="questions-form invite-team-members-form">
<div className="form-group">
<div className="question-label">
Collaborate with your team
<div className="question-sub-label">
Invite your team to the SigNoz workspace
</div>
</div>
<div className="invite-team-members-container">
{teamMembersToInvite?.map((member) => (
<div className="team-member-container" key={member.id}>
<Input
placeholder="your-teammate@org.com"
value={member.email}
type="email"
required
autoFocus
autoComplete="off"
className="team-member-email-input"
onChange={(e: React.ChangeEvent<HTMLInputElement>): void =>
handleEmailChange(e, member)
}
addonAfter={
// eslint-disable-next-line no-nested-ternary
emailValidity[member.id!] === undefined ? null : emailValidity[
member.id!
] ? (
<CheckCircle size={14} color={Color.BG_FOREST_500} />
) : (
<TriangleAlert size={14} color={Color.BG_SIENNA_500} />
)
}
/>
<Select
defaultValue={member.role}
onChange={(value): void => handleRoleChange(value, member)}
className="team-member-role-select"
>
<Select.Option value="VIEWER">Viewer</Select.Option>
<Select.Option value="EDITOR">Editor</Select.Option>
<Select.Option value="ADMIN">Admin</Select.Option>
</Select>
{teamMembersToInvite?.length > 1 && (
<Button
type="primary"
className="remove-team-member-button"
icon={<X size={14} />}
onClick={(): void => handleRemoveTeamMember(member.id)}
/>
)}
</div>
))}
</div>
<div className="invite-team-members-add-another-member-container">
<Button
type="primary"
className="add-another-member-button"
icon={<Plus size={14} />}
onClick={handleAddTeamMember}
>
Member
</Button>
</div>
</div>
{hasInvalidEmails && (
<div className="error-message-container">
<Typography.Text className="error-message" type="danger">
<TriangleAlert size={14} /> Please enter valid emails for all team
members
</Typography.Text>
</div>
)}
{error && (
<div className="error-message-container">
<Typography.Text className="error-message" type="danger">
<TriangleAlert size={14} /> {error}
</Typography.Text>
</div>
)}
{hasErrors && (
<>
{/* show only when invites are sent successfully & partial error is present */}
{inviteUsersSuccessResponse && inviteUsersErrorResponse && (
<div className="success-message-container invite-users-success-message-container">
{inviteUsersSuccessResponse?.map((success, index) => (
<Typography.Text
className="success-message"
// eslint-disable-next-line react/no-array-index-key
key={`${success}-${index}`}
>
<CheckCircle size={14} /> {success}
</Typography.Text>
))}
</div>
)}
<div className="error-message-container invite-users-error-message-container">
{inviteUsersErrorResponse?.map((error, index) => (
<Typography.Text
className="error-message"
type="danger"
// eslint-disable-next-line react/no-array-index-key
key={`${error}-${index}`}
>
<TriangleAlert size={14} /> {error}
</Typography.Text>
))}
</div>
</>
)}
</div>
{/* Partially sent invites */}
{inviteUsersSuccessResponse && inviteUsersErrorResponse && (
<div className="partially-sent-invites-container">
<Typography.Text className="partially-sent-invites-message">
<TriangleAlert size={14} />
Some invites were sent successfully. Please fix the errors above and
resend invites.
</Typography.Text>
<Typography.Text className="partially-sent-invites-message">
You can click on I&apos;ll do this later to go to next step.
</Typography.Text>
</div>
)}
<div className="next-prev-container">
<Button type="default" className="next-button" onClick={onBack}>
<ArrowLeft size={14} />
Back
</Button>
<Button
type="primary"
className="next-button"
onClick={handleNext}
loading={isSendingInvites || isLoading || disableNextButton}
>
Send Invites
<ArrowRight size={14} />
</Button>
</div>
<div className="do-later-container">
<Button
type="link"
className="do-later-button"
onClick={handleDoLater}
disabled={isSendingInvites || disableNextButton}
>
{isLoading && <Loader2 className="animate-spin" size={16} />}
<span>I&apos;ll do this later</span>
</Button>
</div>
</div>
</div>
);
}
export default InviteTeamMembers;

View File

@ -0,0 +1,49 @@
.footer-main-container {
display: flex;
justify-content: center;
box-sizing: border-box;
}
.footer-container {
display: inline-flex;
height: 36px;
padding: 12px;
justify-content: center;
align-items: center;
gap: 32px;
flex-shrink: 0;
border-radius: 4px;
border: 1px solid var(--Greyscale-Slate-500, #161922);
background: var(--Ink-400, #121317);
width: 100%;
max-width: 600px;
}
.footer-container .footer-content {
display: flex;
align-items: center;
gap: 10px;
}
.footer-container .footer-link {
display: flex;
align-items: center;
gap: 6px;
color: #c0c1c3;
}
.footer-container .footer-text {
color: var(--Vanilla-400, var(--Greyscale-Vanilla-400, #c0c1c3));
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: normal;
letter-spacing: 0.2px;
}
.footer-container .footer-dot {
width: 4px;
height: 4px;
fill: var(--Greyscale-Slate-200, #2c3140);
}

View File

@ -0,0 +1,31 @@
import './OnboardingFooter.styles.scss';
import { Dot } from 'lucide-react';
export function OnboardingFooter(): JSX.Element {
return (
<section className="footer-main-container">
<div className="footer-container">
<a
href="https://trust.signoz.io/"
target="_blank"
className="footer-content"
rel="noreferrer"
>
<img src="/logos/hippa.svg" alt="HIPPA" className="footer-logo" />
<span className="footer-text">HIPPA</span>
</a>
<Dot size={24} color="#2C3140" />
<a
href="https://trust.signoz.io/"
target="_blank"
className="footer-content"
rel="noreferrer"
>
<img src="/logos/soc2.svg" alt="SOC2" className="footer-logo" />
<span className="footer-text">SOC2</span>
</a>
</div>
</section>
);
}

View File

@ -0,0 +1 @@
export { OnboardingFooter } from './OnboardingFooter';

View File

@ -0,0 +1,65 @@
.header-container {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0px;
box-sizing: border-box;
}
.header-container .logo-container {
display: flex;
align-items: center;
gap: 10px;
}
.header-container .logo-container img {
height: 17.5px;
width: 17.5px;
}
.header-container .logo-text {
font-family: 'Work Sans', sans-serif;
color: var(--bg-vanilla-100);
font-size: 15.4px;
font-style: normal;
font-weight: 500;
line-height: 17.5px;
}
.header-container .get-help-container {
display: flex;
width: 113px;
height: 32px;
padding: 6px;
justify-content: center;
align-items: center;
gap: 6px;
flex-shrink: 0;
border-radius: 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
box-shadow: none;
}
.header-container .get-help-container img {
width: 12px;
height: 12px;
flex-shrink: 0;
}
.header-container .get-help-text {
color: var(--bg-vanilla-400);
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 10px;
letter-spacing: 0.12px;
}
.lightMode {
.header-container .logo-text {
color: var(--bg-slate-300);
}
}

View File

@ -0,0 +1,12 @@
import './OnboardingHeader.styles.scss';
export function OnboardingHeader(): JSX.Element {
return (
<div className="header-container">
<div className="logo-container">
<img src="/Logos/signoz-brand-logo.svg" alt="SigNoz" />
<span className="logo-text">SigNoz</span>
</div>
</div>
);
}

View File

@ -0,0 +1 @@
export { OnboardingHeader } from './OnboardingHeader';

View File

@ -0,0 +1,597 @@
.onboarding-questionaire-container {
width: 100%;
display: flex;
margin: 0 auto;
align-items: center;
flex-direction: column;
height: 100vh;
max-width: 1176px;
.onboarding-questionaire-header {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
height: 56px;
}
.onboarding-questionaire-content {
height: calc(100vh - 56px - 60px);
width: 100%;
display: flex;
flex-direction: column;
overflow-y: auto;
.questions-container {
color: var(--bg-vanilla-100, #fff);
font-family: Inter;
font-size: 24px;
font-style: normal;
font-weight: 600;
line-height: 32px;
max-width: 600px;
margin: 0 auto;
border-radius: 8px;
max-height: 100%;
}
.title {
color: var(--bg-vanilla-100) !important;
font-size: 24px !important;
line-height: 32px !important;
margin-bottom: 8px !important;
}
.sub-title {
color: var(--bg-vanilla-400) !important;
font-size: 14px !important;
font-style: normal;
font-weight: 400 !important;
line-height: 24px !important;
margin-top: 0px !important;
margin-bottom: 24px !important;
}
.questions-form-container {
max-width: 600px;
width: 600px;
margin: 0 auto;
}
.questions-form {
width: 100%;
display: flex;
min-height: 420px;
padding: 20px 24px 24px 24px;
flex-direction: column;
align-items: center;
gap: 24px;
border-radius: 4px;
border: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400);
.ant-form-item {
margin-bottom: 0px !important;
.ant-form-item-label {
label {
color: var(--bg-vanilla-100) !important;
font-size: 13px !important;
font-weight: 500;
line-height: 20px;
}
}
}
&.invite-team-members-form {
min-height: calc(420px - 24px);
max-height: calc(420px - 24px);
.invite-team-members-container {
max-height: 260px;
overflow-y: auto;
&::-webkit-scrollbar {
width: 0.1rem;
}
&::-webkit-scrollbar-corner {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: rgb(136, 136, 136);
border-radius: 0.625rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
}
}
}
.invite-team-members-container {
display: flex;
width: 100%;
flex-direction: column;
gap: 12px;
.ant-input-group {
width: 100%;
.ant-input {
font-size: 12px;
height: 32px;
background: var(--Ink-300, #16181d);
border: 1px solid var(--bg-slate-400);
color: var(--bg-vanilla-400);
}
.ant-input-group-addon {
font-size: 11px;
height: 32px;
min-width: 80px;
background: var(--Ink-300, #16181d);
border: 1px solid var(--Greyscale-Slate-400, #1d212d);
border-left: 0px;
color: var(--bg-vanilla-400);
}
}
}
.question-label {
color: var(--bg-vanilla-100);
font-size: 13px;
font-style: normal;
font-weight: 500;
line-height: 20px;
}
.question-sub-label {
color: var(--bg-vanilla-400);
font-size: 11px;
font-style: normal;
font-weight: 400;
line-height: 16px;
}
.next-prev-container {
display: flex;
justify-content: space-between;
align-items: center;
gap: 10px;
margin-bottom: 24px;
.ant-btn {
flex: 1;
}
}
.form-group {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 8px;
align-self: stretch;
}
.slider-container {
width: 100%;
.ant-slider .ant-slider-mark {
font-size: 10px;
}
}
.do-later-container {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
margin-top: 24px;
.do-later-button {
font-size: 12px;
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
}
}
.question {
color: var(--bg-vanilla-100);
font-size: 14px;
font-style: normal;
font-weight: 500;
line-height: 20px;
display: flex;
align-items: center;
gap: 8px;
}
input[type='text'] {
width: 100%;
padding: 12px;
border-radius: 2px;
font-size: 14px;
height: 40px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
color: var(--bg-vanilla-100);
&:focus-visible {
outline: none;
}
}
.radio-group,
.grid,
.tool-grid {
display: flex;
align-items: flex-start;
align-content: flex-start;
gap: 10px;
align-self: stretch;
flex-wrap: wrap;
}
.radio-button,
.grid-button,
.tool-button {
border-radius: 4px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
padding: 12px;
color: var(--bg-vanilla-400);
font-size: 14px;
font-style: normal;
text-align: left;
font-weight: 400;
transition: background-color 0.3s ease;
min-width: 258px;
cursor: pointer;
box-sizing: border-box;
}
.radio-button.active,
.grid-button.active,
.tool-button.active,
.radio-button:hover,
.grid-button:hover,
.tool-button:hover {
border: 1px solid rgba(78, 116, 248, 0.4);
background: rgba(78, 116, 248, 0.2);
}
.two-column-grid {
width: 100%;
display: grid;
grid-template-columns: 1fr 1fr; /* Two equal columns */
gap: 10px;
}
.onboarding-questionaire-button,
.add-another-member-button,
.remove-team-member-button {
display: flex;
align-items: center;
justify-content: space-between;
border-radius: 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
color: var(--bg-vanilla-400);
box-shadow: none;
font-size: 14px;
font-style: normal;
text-align: left;
font-weight: 400;
transition: background-color 0.3s ease;
cursor: pointer;
height: 40px;
box-sizing: border-box;
&:hover {
border: 1px solid rgba(78, 116, 248, 0.4);
background: rgba(78, 116, 248, 0.2);
}
&:focus-visible {
outline: none;
}
&.active {
border: 1px solid rgba(78, 116, 248, 0.4);
background: rgba(78, 116, 248, 0.2);
}
}
.add-another-member-button,
.remove-team-member-button {
font-size: 12px;
height: 32px;
}
.remove-team-member-button {
display: flex;
align-items: center;
justify-content: center;
border: 1px solid var(--bg-slate-400);
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
background-color: var(--bg-ink-300);
border-left: 0px;
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
}
.onboarding-questionaire-other-input {
.ant-input-group {
.ant-input {
border-top-right-radius: 0px !important;
border-bottom-right-radius: 0px !important;
}
}
}
.tool-grid {
grid-template-columns: repeat(4, 1fr);
}
.input-field {
flex: 0;
padding: 12px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
color: var(--bg-vanilla-100);
border-radius: 4px;
font-size: 14px;
min-width: 258px;
}
.next-button {
display: flex;
height: 40px;
padding: 8px 12px 8px 16px;
justify-content: center;
align-items: center;
gap: 6px;
align-self: stretch;
border: 0px;
border-radius: 50px;
margin-top: 24px;
cursor: pointer;
}
.next-button.disabled {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
.arrow {
font-size: 18px;
color: var(--bg-vanilla-100);
}
}
.onboarding-questionaire-footer {
width: 100%;
height: 60px;
padding: 12px 24px;
box-sizing: border-box;
}
.invite-team-members-add-another-member-container {
width: 100%;
display: flex;
justify-content: flex-end;
align-items: center;
margin-top: 12px;
}
}
.onboarding-questionaire-loading-container {
width: 100%;
display: flex;
height: 100vh;
max-width: 600px;
justify-content: center;
align-items: center;
margin: 0 auto;
}
.lightMode {
.onboarding-questionaire-container {
.onboarding-questionaire-content {
.questions-container {
color: var(--bg-slate-300);
}
.title {
color: var(--bg-slate-300) !important;
}
.sub-title {
color: var(--bg-slate-400) !important;
}
.questions-form {
width: 100%;
display: flex;
min-height: 420px;
padding: 20px 24px 24px 24px;
flex-direction: column;
align-items: center;
gap: 24px;
border-radius: 4px;
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
.ant-form-item {
margin-bottom: 0px !important;
.ant-form-item-label {
label {
color: var(--bg-slate-300) !important;
font-size: 13px;
font-weight: 500;
line-height: 20px;
}
}
}
&.invite-team-members-form {
.invite-team-members-container {
max-height: 260px;
overflow-y: auto;
&::-webkit-scrollbar {
width: 0.1rem;
}
&::-webkit-scrollbar-corner {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: rgb(136, 136, 136);
border-radius: 0.625rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
}
}
}
.invite-team-members-container {
.ant-input-group {
.ant-input {
background: var(--bg-vanilla-100);
border: 1px solid var(--bg-vanilla-300);
color: var(--bg-slate-300);
}
.ant-input-group-addon {
font-size: 11px;
height: 32px;
min-width: 80px;
background: var(--bg-vanilla-100);
border: 1px solid var(--bg-vanilla-300);
border-left: 0px;
color: var(--bg-slate-300);
}
}
}
.question-label {
color: var(--bg-slate-300);
}
.question-sub-label {
color: var(--bg-slate-400);
}
.question {
color: var(--bg-slate-300);
}
input[type='text'] {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
color: var(--text-ink-300);
}
.radio-button,
.grid-button,
.tool-button {
border-radius: 4px;
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
padding: 12px;
color: var(--bg-slate-300);
font-size: 14px;
font-style: normal;
text-align: left;
font-weight: 400;
transition: background-color 0.3s ease;
min-width: 258px;
cursor: pointer;
box-sizing: border-box;
}
.radio-button.active,
.grid-button.active,
.tool-button.active,
.radio-button:hover,
.grid-button:hover,
.tool-button:hover {
border: 1px solid rgba(78, 116, 248, 0.4);
background: rgba(78, 116, 248, 0.2);
}
.onboarding-questionaire-button,
.add-another-member-button,
.remove-team-member-button {
display: flex;
align-items: center;
justify-content: space-between;
border-radius: 2px;
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
color: var(--bg-ink-300);
box-shadow: none;
font-size: 14px;
font-style: normal;
text-align: left;
font-weight: 400;
transition: background-color 0.3s ease;
cursor: pointer;
height: 40px;
box-sizing: border-box;
&:hover {
border: 1px solid rgba(78, 116, 248, 0.4);
background: rgba(78, 116, 248, 0.2);
}
&:focus-visible {
outline: none;
}
&.active {
border: 1px solid rgba(78, 116, 248, 0.4);
background: rgba(78, 116, 248, 0.2);
}
}
.remove-team-member-button {
display: flex;
align-items: center;
justify-content: center;
border: 1px solid var(--bg-vanilla-300);
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
background-color: var(--bg-vanilla-100);
border-left: 0px;
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
}
.input-field {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
color: var(--text-ink-300);
}
.arrow {
color: var(--bg-slate-300);
}
}
}
}

View File

@ -0,0 +1,323 @@
import { Button, Slider, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import { ArrowLeft, ArrowRight, Loader2, Minus } from 'lucide-react';
import { useEffect, useState } from 'react';
export interface OptimiseSignozDetails {
logsPerDay: number;
hostsPerDay: number;
services: number;
}
// Define exponential range
const logsMin = 1; // Set to your minimum value in the exponential range
const logsMax = 10000; // Set to your maximum value in the exponential range
const hostsMin = 1;
const hostsMax = 10000;
const servicesMin = 1;
const servicesMax = 5000;
// Function to convert linear slider value to exponential scale
const linearToExponential = (
value: number,
min: number,
max: number,
): number => {
const expMin = Math.log10(min);
const expMax = Math.log10(max);
const expValue = 10 ** (expMin + ((expMax - expMin) * value) / 100);
return Math.round(expValue);
};
const exponentialToLinear = (
expValue: number,
min: number,
max: number,
): number => {
const expMin = Math.log10(min);
const expMax = Math.log10(max);
const linearValue =
((Math.log10(expValue) - expMin) / (expMax - expMin)) * 100;
return Math.round(linearValue); // Round to get a whole number within the 0-100 range
};
interface OptimiseSignozNeedsProps {
optimiseSignozDetails: OptimiseSignozDetails;
setOptimiseSignozDetails: (details: OptimiseSignozDetails) => void;
onNext: () => void;
onBack: () => void;
onWillDoLater: () => void;
isUpdatingProfile: boolean;
isNextDisabled: boolean;
}
const marks = {
0: `${linearToExponential(0, logsMin, logsMax).toLocaleString()} GB`,
25: `${linearToExponential(25, logsMin, logsMax).toLocaleString()} GB`,
50: `${linearToExponential(50, logsMin, logsMax).toLocaleString()} GB`,
75: `${linearToExponential(75, logsMin, logsMax).toLocaleString()} GB`,
100: `${linearToExponential(100, logsMin, logsMax).toLocaleString()} GB`,
};
const hostMarks = {
0: `${linearToExponential(0, hostsMin, hostsMax).toLocaleString()}`,
25: `${linearToExponential(25, hostsMin, hostsMax).toLocaleString()}`,
50: `${linearToExponential(50, hostsMin, hostsMax).toLocaleString()}`,
75: `${linearToExponential(75, hostsMin, hostsMax).toLocaleString()}`,
100: `${linearToExponential(100, hostsMin, hostsMax).toLocaleString()}`,
};
const serviceMarks = {
0: `${linearToExponential(0, servicesMin, servicesMax).toLocaleString()}`,
25: `${linearToExponential(25, servicesMin, servicesMax).toLocaleString()}`,
50: `${linearToExponential(50, servicesMin, servicesMax).toLocaleString()}`,
75: `${linearToExponential(75, servicesMin, servicesMax).toLocaleString()}`,
100: `${linearToExponential(100, servicesMin, servicesMax).toLocaleString()}`,
};
function OptimiseSignozNeeds({
isUpdatingProfile,
optimiseSignozDetails,
setOptimiseSignozDetails,
onNext,
onBack,
onWillDoLater,
isNextDisabled,
}: OptimiseSignozNeedsProps): JSX.Element {
const [logsPerDay, setLogsPerDay] = useState<number>(
optimiseSignozDetails?.logsPerDay || 0,
);
const [hostsPerDay, setHostsPerDay] = useState<number>(
optimiseSignozDetails?.hostsPerDay || 0,
);
const [services, setServices] = useState<number>(
optimiseSignozDetails?.services || 0,
);
// Internal state for the linear slider
const [sliderValues, setSliderValues] = useState({
logsPerDay: 0,
hostsPerDay: 0,
services: 0,
});
useEffect(() => {
setSliderValues({
logsPerDay: exponentialToLinear(logsPerDay, logsMin, logsMax),
hostsPerDay: exponentialToLinear(hostsPerDay, hostsMin, hostsMax),
services: exponentialToLinear(services, servicesMin, servicesMax),
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
setOptimiseSignozDetails({
logsPerDay,
hostsPerDay,
services,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [services, hostsPerDay, logsPerDay]);
const handleOnNext = (): void => {
logEvent('Org Onboarding: Answered', {
logsPerDay,
hostsPerDay,
services,
});
onNext();
};
const handleOnBack = (): void => {
onBack();
};
const handleWillDoLater = (): void => {
setOptimiseSignozDetails({
logsPerDay: 0,
hostsPerDay: 0,
services: 0,
});
onWillDoLater();
logEvent('Org Onboarding: Clicked Do Later', {
currentPageID: 3,
});
};
const handleSliderChange = (key: string, value: number): void => {
setSliderValues({
...sliderValues,
[key]: value,
});
switch (key) {
case 'logsPerDay':
setLogsPerDay(linearToExponential(value, logsMin, logsMax));
break;
case 'hostsPerDay':
setHostsPerDay(linearToExponential(value, hostsMin, hostsMax));
break;
case 'services':
setServices(linearToExponential(value, servicesMin, servicesMax));
break;
default:
break;
}
};
// Calculate the exponential value based on the current slider position
const logsPerDayValue = linearToExponential(
sliderValues.logsPerDay,
logsMin,
logsMax,
);
const hostsPerDayValue = linearToExponential(
sliderValues.hostsPerDay,
hostsMin,
hostsMax,
);
const servicesValue = linearToExponential(
sliderValues.services,
servicesMin,
servicesMax,
);
return (
<div className="questions-container">
<Typography.Title level={3} className="title">
Optimize SigNoz for Your Needs
</Typography.Title>
<Typography.Paragraph className="sub-title">
Give us a quick sense of your scale so SigNoz can keep up!
</Typography.Paragraph>
<div className="questions-form-container">
<div className="questions-form">
<Typography.Paragraph className="question">
What does your scale approximately look like?
</Typography.Paragraph>
<div className="form-group">
<label className="question" htmlFor="organisationName">
Logs / Day
</label>
<div className="slider-container">
<div>
<Slider
min={0}
max={100}
value={sliderValues.logsPerDay}
marks={marks}
onChange={(value: number): void =>
handleSliderChange('logsPerDay', value)
}
styles={{
track: {
background: '#4E74F8',
},
}}
tooltip={{
formatter: (): string => `${logsPerDayValue.toLocaleString()} GB`, // Show whole number
}}
/>
</div>
</div>
</div>
<div className="form-group">
<label className="question" htmlFor="organisationName">
Metrics <Minus size={14} /> Number of Hosts
</label>
<div className="slider-container">
<div>
<Slider
min={0}
max={100}
value={sliderValues.hostsPerDay}
marks={hostMarks}
onChange={(value: number): void =>
handleSliderChange('hostsPerDay', value)
}
styles={{
track: {
background: '#4E74F8',
},
}}
tooltip={{
formatter: (): string => `${hostsPerDayValue.toLocaleString()}`, // Show whole number
}}
/>
</div>
</div>
</div>
<div className="form-group">
<label className="question" htmlFor="organisationName">
Number of services
</label>
<div className="slider-container">
<div>
<Slider
min={0}
max={100}
value={sliderValues.services}
marks={serviceMarks}
onChange={(value: number): void =>
handleSliderChange('services', value)
}
styles={{
track: {
background: '#4E74F8',
},
}}
tooltip={{
formatter: (): string => `${servicesValue.toLocaleString()}`, // Show whole number
}}
/>
</div>
</div>
</div>
</div>
<div className="next-prev-container">
<Button
type="default"
className="next-button"
onClick={handleOnBack}
disabled={isUpdatingProfile}
>
<ArrowLeft size={14} />
Back
</Button>
<Button
type="primary"
className="next-button"
onClick={handleOnNext}
disabled={isUpdatingProfile || isNextDisabled}
>
Next{' '}
{isUpdatingProfile ? (
<Loader2 className="animate-spin" />
) : (
<ArrowRight size={14} />
)}
</Button>
</div>
<div className="do-later-container">
<Button type="link" onClick={handleWillDoLater}>
I&apos;ll do this later
</Button>
</div>
</div>
</div>
);
}
export default OptimiseSignozNeeds;

View File

@ -0,0 +1,376 @@
/* eslint-disable sonarjs/cognitive-complexity */
import '../OnboardingQuestionaire.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Button, Input, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import editOrg from 'api/user/editOrg';
import { useNotifications } from 'hooks/useNotifications';
import { ArrowRight, CheckCircle, Loader2 } from 'lucide-react';
import { Dispatch, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import { UPDATE_ORG_NAME } from 'types/actions/app';
import AppReducer from 'types/reducer/app';
export interface OrgData {
id: string;
isAnonymous: boolean;
name: string;
}
export interface OrgDetails {
organisationName: string;
usesObservability: boolean | null;
observabilityTool: string | null;
otherTool: string | null;
familiarity: string | null;
}
interface OrgQuestionsProps {
currentOrgData: OrgData | null;
orgDetails: OrgDetails;
onNext: (details: OrgDetails) => void;
}
const observabilityTools = {
AWSCloudwatch: 'AWS Cloudwatch',
DataDog: 'DataDog',
NewRelic: 'New Relic',
GrafanaPrometheus: 'Grafana / Prometheus',
AzureAppMonitor: 'Azure App Monitor',
GCPNativeO11yTools: 'GCP-native o11y tools',
Honeycomb: 'Honeycomb',
};
const o11yFamiliarityOptions: Record<string, string> = {
beginner: 'Beginner',
intermediate: 'Intermediate',
expert: 'Expert',
notFamiliar: "I'm not familiar with it",
};
function OrgQuestions({
currentOrgData,
orgDetails,
onNext,
}: OrgQuestionsProps): JSX.Element {
const { user } = useSelector<AppState, AppReducer>((state) => state.app);
const { notifications } = useNotifications();
const dispatch = useDispatch<Dispatch<AppActions>>();
const { t } = useTranslation(['organizationsettings', 'common']);
const [organisationName, setOrganisationName] = useState<string>(
orgDetails?.organisationName || '',
);
const [usesObservability, setUsesObservability] = useState<boolean | null>(
orgDetails?.usesObservability || null,
);
const [observabilityTool, setObservabilityTool] = useState<string | null>(
orgDetails?.observabilityTool || null,
);
const [otherTool, setOtherTool] = useState<string>(
orgDetails?.otherTool || '',
);
const [familiarity, setFamiliarity] = useState<string | null>(
orgDetails?.familiarity || null,
);
const [isNextDisabled, setIsNextDisabled] = useState<boolean>(true);
useEffect(() => {
setOrganisationName(orgDetails.organisationName);
}, [orgDetails.organisationName]);
const [isLoading, setIsLoading] = useState<boolean>(false);
const handleOrgNameUpdate = async (): Promise<void> => {
/* Early bailout if orgData is not set or if the organisation name is not set or if the organisation name is empty or if the organisation name is the same as the one in the orgData */
if (
!currentOrgData ||
!organisationName ||
organisationName === '' ||
orgDetails.organisationName === organisationName
) {
logEvent('Org Onboarding: Answered', {
usesObservability,
observabilityTool,
otherTool,
familiarity,
});
onNext({
organisationName,
usesObservability,
observabilityTool,
otherTool,
familiarity,
});
return;
}
try {
setIsLoading(true);
const { statusCode, error } = await editOrg({
isAnonymous: currentOrgData.isAnonymous,
name: organisationName,
orgId: currentOrgData.id,
});
if (statusCode === 200) {
dispatch({
type: UPDATE_ORG_NAME,
payload: {
orgId: currentOrgData?.id,
name: orgDetails.organisationName,
},
});
logEvent('Org Onboarding: Org Name Updated', {
organisationName: orgDetails.organisationName,
});
logEvent('Org Onboarding: Answered', {
usesObservability,
observabilityTool,
otherTool,
familiarity,
});
onNext({
organisationName,
usesObservability,
observabilityTool,
otherTool,
familiarity,
});
} else {
logEvent('Org Onboarding: Org Name Update Failed', {
organisationName: orgDetails.organisationName,
});
notifications.error({
message:
error ||
t('something_went_wrong', {
ns: 'common',
}),
});
}
setIsLoading(false);
} catch (error) {
setIsLoading(false);
notifications.error({
message: t('something_went_wrong', {
ns: 'common',
}),
});
}
};
const isValidUsesObservability = (): boolean => {
if (usesObservability === null) {
return false;
}
if (usesObservability && (!observabilityTool || observabilityTool === '')) {
return false;
}
// eslint-disable-next-line sonarjs/prefer-single-boolean-return
if (usesObservability && observabilityTool === 'Others' && otherTool === '') {
return false;
}
return true;
};
useEffect(() => {
const isValidObservability = isValidUsesObservability();
if (organisationName !== '' && familiarity !== null && isValidObservability) {
setIsNextDisabled(false);
} else {
setIsNextDisabled(true);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
organisationName,
usesObservability,
familiarity,
observabilityTool,
otherTool,
]);
const handleOnNext = (): void => {
handleOrgNameUpdate();
};
return (
<div className="questions-container">
<Typography.Title level={3} className="title">
Welcome, {user?.name}!
</Typography.Title>
<Typography.Paragraph className="sub-title">
We&apos;ll help you get the most out of SigNoz, whether you&apos;re new to
observability or a seasoned pro.
</Typography.Paragraph>
<div className="questions-form-container">
<div className="questions-form">
<div className="form-group">
<label className="question" htmlFor="organisationName">
Your Organisation Name
</label>
<input
type="text"
name="organisationName"
id="organisationName"
placeholder="For eg. Simpsonville..."
autoComplete="off"
value={organisationName}
onChange={(e): void => setOrganisationName(e.target.value)}
/>
</div>
<div className="form-group">
<label className="question" htmlFor="usesObservability">
Do you currently use any observability/monitoring tool?
</label>
<div className="two-column-grid">
<Button
type="primary"
name="usesObservability"
className={`onboarding-questionaire-button ${
usesObservability === true ? 'active' : ''
}`}
onClick={(): void => {
setUsesObservability(true);
}}
>
Yes{' '}
{usesObservability === true && (
<CheckCircle size={12} color={Color.BG_FOREST_500} />
)}
</Button>
<Button
type="primary"
className={`onboarding-questionaire-button ${
usesObservability === false ? 'active' : ''
}`}
onClick={(): void => {
setUsesObservability(false);
setObservabilityTool(null);
setOtherTool('');
}}
>
No{' '}
{usesObservability === false && (
<CheckCircle size={12} color={Color.BG_FOREST_500} />
)}
</Button>
</div>
</div>
{usesObservability && (
<div className="form-group">
<label className="question" htmlFor="observabilityTool">
Which observability tool do you currently use?
</label>
<div className="two-column-grid">
{Object.keys(observabilityTools).map((tool) => (
<Button
key={tool}
type="primary"
className={`onboarding-questionaire-button ${
observabilityTool === tool ? 'active' : ''
}`}
onClick={(): void => setObservabilityTool(tool)}
>
{observabilityTools[tool as keyof typeof observabilityTools]}
{observabilityTool === tool && (
<CheckCircle size={12} color={Color.BG_FOREST_500} />
)}
</Button>
))}
{observabilityTool === 'Others' ? (
<Input
type="text"
className="onboarding-questionaire-other-input"
placeholder="Please specify the tool"
value={otherTool || ''}
autoFocus
addonAfter={
otherTool && otherTool !== '' ? (
<CheckCircle size={12} color={Color.BG_FOREST_500} />
) : (
''
)
}
onChange={(e): void => setOtherTool(e.target.value)}
/>
) : (
<button
type="button"
className={`onboarding-questionaire-button ${
observabilityTool === 'Others' ? 'active' : ''
}`}
onClick={(): void => setObservabilityTool('Others')}
>
Others
</button>
)}
</div>
</div>
)}
<div className="form-group">
<div className="question">
Are you familiar with setting up observability (o11y)?
</div>
<div className="two-column-grid">
{Object.keys(o11yFamiliarityOptions).map((option: string) => (
<Button
key={option}
type="primary"
className={`onboarding-questionaire-button ${
familiarity === option ? 'active' : ''
}`}
onClick={(): void => setFamiliarity(option)}
>
{o11yFamiliarityOptions[option]}
{familiarity === option && (
<CheckCircle size={12} color={Color.BG_FOREST_500} />
)}
</Button>
))}
</div>
</div>
</div>
<div className="next-prev-container">
<Button
type="primary"
className={`next-button ${isNextDisabled ? 'disabled' : ''}`}
onClick={handleOnNext}
disabled={isNextDisabled}
>
Next
{isLoading ? (
<Loader2 className="animate-spin" />
) : (
<ArrowRight size={14} />
)}
</Button>
</div>
</div>
</div>
);
}
export default OrgQuestions;

View File

@ -0,0 +1,295 @@
import './OnboardingQuestionaire.styles.scss';
import { NotificationInstance } from 'antd/es/notification/interface';
import logEvent from 'api/common/logEvent';
import updateProfileAPI from 'api/onboarding/updateProfile';
import getAllOrgPreferences from 'api/preferences/getAllOrgPreferences';
import updateOrgPreferenceAPI from 'api/preferences/updateOrgPreference';
import { AxiosError } from 'axios';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import ROUTES from 'constants/routes';
import { InviteTeamMembersProps } from 'container/OrganizationSettings/PendingInvitesContainer';
import { useNotifications } from 'hooks/useNotifications';
import history from 'lib/history';
import { useEffect, useState } from 'react';
import { useMutation, useQuery } from 'react-query';
import { useDispatch, useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import {
UPDATE_IS_FETCHING_ORG_PREFERENCES,
UPDATE_ORG_PREFERENCES,
} from 'types/actions/app';
import AppReducer from 'types/reducer/app';
import {
AboutSigNozQuestions,
SignozDetails,
} from './AboutSigNozQuestions/AboutSigNozQuestions';
import InviteTeamMembers from './InviteTeamMembers/InviteTeamMembers';
import { OnboardingHeader } from './OnboardingHeader/OnboardingHeader';
import OptimiseSignozNeeds, {
OptimiseSignozDetails,
} from './OptimiseSignozNeeds/OptimiseSignozNeeds';
import OrgQuestions, { OrgData, OrgDetails } from './OrgQuestions/OrgQuestions';
export const showErrorNotification = (
notifications: NotificationInstance,
err: Error,
): void => {
notifications.error({
message: err.message || SOMETHING_WENT_WRONG,
});
};
const INITIAL_ORG_DETAILS: OrgDetails = {
organisationName: '',
usesObservability: true,
observabilityTool: '',
otherTool: '',
familiarity: '',
};
const INITIAL_SIGNOZ_DETAILS: SignozDetails = {
hearAboutSignoz: '',
interestInSignoz: '',
otherInterestInSignoz: '',
otherAboutSignoz: '',
};
const INITIAL_OPTIMISE_SIGNOZ_DETAILS: OptimiseSignozDetails = {
logsPerDay: 0,
hostsPerDay: 0,
services: 0,
};
const BACK_BUTTON_EVENT_NAME = 'Org Onboarding: Back Button Clicked';
const NEXT_BUTTON_EVENT_NAME = 'Org Onboarding: Next Button Clicked';
const ONBOARDING_COMPLETE_EVENT_NAME = 'Org Onboarding: Complete';
function OnboardingQuestionaire(): JSX.Element {
const { notifications } = useNotifications();
const { org } = useSelector<AppState, AppReducer>((state) => state.app);
const dispatch = useDispatch();
const [currentStep, setCurrentStep] = useState<number>(1);
const [orgDetails, setOrgDetails] = useState<OrgDetails>(INITIAL_ORG_DETAILS);
const [signozDetails, setSignozDetails] = useState<SignozDetails>(
INITIAL_SIGNOZ_DETAILS,
);
const [
optimiseSignozDetails,
setOptimiseSignozDetails,
] = useState<OptimiseSignozDetails>(INITIAL_OPTIMISE_SIGNOZ_DETAILS);
const [teamMembers, setTeamMembers] = useState<
InviteTeamMembersProps[] | null
>(null);
const [currentOrgData, setCurrentOrgData] = useState<OrgData | null>(null);
const [
updatingOrgOnboardingStatus,
setUpdatingOrgOnboardingStatus,
] = useState<boolean>(false);
useEffect(() => {
if (org) {
setCurrentOrgData(org[0]);
setOrgDetails({
...orgDetails,
organisationName: org[0].name,
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [org]);
useEffect(() => {
logEvent('Org Onboarding: Started', {
org_id: org?.[0]?.id,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const { refetch: refetchOrgPreferences } = useQuery({
queryFn: () => getAllOrgPreferences(),
queryKey: ['getOrgPreferences'],
enabled: false,
refetchOnWindowFocus: false,
onSuccess: (response) => {
dispatch({
type: UPDATE_IS_FETCHING_ORG_PREFERENCES,
payload: {
isFetchingOrgPreferences: false,
},
});
dispatch({
type: UPDATE_ORG_PREFERENCES,
payload: {
orgPreferences: response.payload?.data || null,
},
});
setUpdatingOrgOnboardingStatus(false);
logEvent('Org Onboarding: Redirecting to Get Started', {});
history.push(ROUTES.GET_STARTED);
},
onError: () => {
setUpdatingOrgOnboardingStatus(false);
},
});
const isNextDisabled =
optimiseSignozDetails.logsPerDay === 0 &&
optimiseSignozDetails.hostsPerDay === 0 &&
optimiseSignozDetails.services === 0;
const { mutate: updateProfile, isLoading: isUpdatingProfile } = useMutation(
updateProfileAPI,
{
onSuccess: () => {
setCurrentStep(4);
},
onError: (error) => {
showErrorNotification(notifications, error as AxiosError);
},
},
);
const { mutate: updateOrgPreference } = useMutation(updateOrgPreferenceAPI, {
onSuccess: () => {
refetchOrgPreferences();
},
onError: (error) => {
showErrorNotification(notifications, error as AxiosError);
setUpdatingOrgOnboardingStatus(false);
},
});
const handleUpdateProfile = (): void => {
logEvent(NEXT_BUTTON_EVENT_NAME, {
currentPageID: 3,
nextPageID: 4,
});
updateProfile({
familiarity_with_observability: orgDetails?.familiarity as string,
has_existing_observability_tool: orgDetails?.usesObservability as boolean,
existing_observability_tool:
orgDetails?.observabilityTool === 'Others'
? (orgDetails?.otherTool as string)
: (orgDetails?.observabilityTool as string),
reasons_for_interest_in_signoz:
signozDetails?.interestInSignoz === 'Others'
? (signozDetails?.otherInterestInSignoz as string)
: (signozDetails?.interestInSignoz as string),
where_did_you_hear_about_signoz:
signozDetails?.hearAboutSignoz === 'Others'
? (signozDetails?.otherAboutSignoz as string)
: (signozDetails?.hearAboutSignoz as string),
logs_scale_per_day_in_gb: optimiseSignozDetails?.logsPerDay as number,
number_of_hosts: optimiseSignozDetails?.hostsPerDay as number,
number_of_services: optimiseSignozDetails?.services as number,
});
};
const handleOnboardingComplete = (): void => {
logEvent(ONBOARDING_COMPLETE_EVENT_NAME, {
currentPageID: 4,
});
setUpdatingOrgOnboardingStatus(true);
updateOrgPreference({
preferenceID: 'ORG_ONBOARDING',
value: true,
});
};
return (
<div className="onboarding-questionaire-container">
<div className="onboarding-questionaire-header">
<OnboardingHeader />
</div>
<div className="onboarding-questionaire-content">
{currentStep === 1 && (
<OrgQuestions
currentOrgData={currentOrgData}
orgDetails={orgDetails}
onNext={(orgDetails: OrgDetails): void => {
logEvent(NEXT_BUTTON_EVENT_NAME, {
currentPageID: 1,
nextPageID: 2,
});
setOrgDetails(orgDetails);
setCurrentStep(2);
}}
/>
)}
{currentStep === 2 && (
<AboutSigNozQuestions
signozDetails={signozDetails}
setSignozDetails={setSignozDetails}
onBack={(): void => {
logEvent(BACK_BUTTON_EVENT_NAME, {
currentPageID: 2,
prevPageID: 1,
});
setCurrentStep(1);
}}
onNext={(): void => {
logEvent(NEXT_BUTTON_EVENT_NAME, {
currentPageID: 2,
nextPageID: 3,
});
setCurrentStep(3);
}}
/>
)}
{currentStep === 3 && (
<OptimiseSignozNeeds
isNextDisabled={isNextDisabled}
isUpdatingProfile={isUpdatingProfile}
optimiseSignozDetails={optimiseSignozDetails}
setOptimiseSignozDetails={setOptimiseSignozDetails}
onBack={(): void => {
logEvent(BACK_BUTTON_EVENT_NAME, {
currentPageID: 3,
prevPageID: 2,
});
setCurrentStep(2);
}}
onNext={handleUpdateProfile}
onWillDoLater={(): void => setCurrentStep(4)}
/>
)}
{currentStep === 4 && (
<InviteTeamMembers
isLoading={updatingOrgOnboardingStatus}
teamMembers={teamMembers}
setTeamMembers={setTeamMembers}
onBack={(): void => {
logEvent(BACK_BUTTON_EVENT_NAME, {
currentPageID: 4,
prevPageID: 3,
});
setCurrentStep(3);
}}
onNext={handleOnboardingComplete}
/>
)}
</div>
</div>
);
}
export default OnboardingQuestionaire;

View File

@ -236,7 +236,9 @@ function PendingInvitesContainer(): JSX.Element {
export interface InviteTeamMembersProps { export interface InviteTeamMembersProps {
email: string; email: string;
name: string; name: string;
role: ROLES; role: string;
id: string;
frontendBaseUrl: string;
} }
interface DataProps { interface DataProps {

View File

@ -81,8 +81,10 @@ export const AggregatorFilter = memo(function AggregatorFilter({
prefix: item.type || '', prefix: item.type || '',
condition: !item.isColumn, condition: !item.isColumn,
}), }),
!item.isColumn && item.type ? item.type : '',
)} )}
dataType={item.dataType} dataType={item.dataType}
type={item.type || ''}
/> />
), ),
value: `${item.key}${selectValueDivider}${createIdFromObjectFields( value: `${item.key}${selectValueDivider}${createIdFromObjectFields(
@ -187,6 +189,9 @@ export const AggregatorFilter = memo(function AggregatorFilter({
prefix: query.aggregateAttribute.type || '', prefix: query.aggregateAttribute.type || '',
condition: !query.aggregateAttribute.isColumn, condition: !query.aggregateAttribute.isColumn,
}), }),
!query.aggregateAttribute.isColumn && query.aggregateAttribute.type
? query.aggregateAttribute.type
: '',
); );
return ( return (

View File

@ -75,8 +75,10 @@ export const GroupByFilter = memo(function GroupByFilter({
prefix: item.type || '', prefix: item.type || '',
condition: !item.isColumn, condition: !item.isColumn,
}), }),
!item.isColumn && item.type ? item.type : '',
)} )}
dataType={item.dataType || ''} dataType={item.dataType || ''}
type={item.type || ''}
/> />
), ),
value: `${item.id}`, value: `${item.id}`,
@ -166,6 +168,7 @@ export const GroupByFilter = memo(function GroupByFilter({
prefix: item.type || '', prefix: item.type || '',
condition: !item.isColumn, condition: !item.isColumn,
}), }),
!item.isColumn && item.type ? item.type : '',
)}`, )}`,
value: `${item.id}`, value: `${item.id}`,
}), }),

View File

@ -1,8 +1,9 @@
import { MetricsType } from 'container/MetricsApplication/constant'; import { MetricsType } from 'container/MetricsApplication/constant';
export function removePrefix(str: string): string { export function removePrefix(str: string, type: string): string {
const tagPrefix = `${MetricsType.Tag}_`; const tagPrefix = `${MetricsType.Tag}_`;
const resourcePrefix = `${MetricsType.Resource}_`; const resourcePrefix = `${MetricsType.Resource}_`;
const scopePrefix = `${MetricsType.Scope}_`;
if (str.startsWith(tagPrefix)) { if (str.startsWith(tagPrefix)) {
return str.slice(tagPrefix.length); return str.slice(tagPrefix.length);
@ -10,5 +11,9 @@ export function removePrefix(str: string): string {
if (str.startsWith(resourcePrefix)) { if (str.startsWith(resourcePrefix)) {
return str.slice(resourcePrefix.length); return str.slice(resourcePrefix.length);
} }
if (str.startsWith(scopePrefix) && type === MetricsType.Scope) {
return str.slice(scopePrefix.length);
}
return str; return str;
} }

View File

@ -3,25 +3,23 @@ import './QueryBuilderSearch.styles.scss';
import { Tooltip } from 'antd'; import { Tooltip } from 'antd';
import { TagContainer, TagLabel, TagValue } from './style'; import { TagContainer, TagLabel, TagValue } from './style';
import { getOptionType } from './utils';
function OptionRenderer({ function OptionRenderer({
label, label,
value, value,
dataType, dataType,
type,
}: OptionRendererProps): JSX.Element { }: OptionRendererProps): JSX.Element {
const optionType = getOptionType(label);
return ( return (
<span className="option"> <span className="option">
{optionType ? ( {type ? (
<Tooltip title={`${value}`} placement="topLeft"> <Tooltip title={`${value}`} placement="topLeft">
<div className="selectOptionContainer"> <div className="selectOptionContainer">
<div className="option-value">{value}</div> <div className="option-value">{value}</div>
<div className="option-meta-data-container"> <div className="option-meta-data-container">
<TagContainer> <TagContainer>
<TagLabel>Type: </TagLabel> <TagLabel>Type: </TagLabel>
<TagValue>{optionType}</TagValue> <TagValue>{type}</TagValue>
</TagContainer> </TagContainer>
<TagContainer> <TagContainer>
<TagLabel>Data type: </TagLabel> <TagLabel>Data type: </TagLabel>
@ -43,6 +41,7 @@ interface OptionRendererProps {
label: string; label: string;
value: string; value: string;
dataType: string; dataType: string;
type: string;
} }
export default OptionRenderer; export default OptionRenderer;

View File

@ -410,6 +410,7 @@ function QueryBuilderSearch({
label={option.label} label={option.label}
value={option.value} value={option.value}
dataType={option.dataType || ''} dataType={option.dataType || ''}
type={option.type || ''}
/> />
{option.selected && <StyledCheckOutlined />} {option.selected && <StyledCheckOutlined />}
</Select.Option> </Select.Option>

View File

@ -260,6 +260,20 @@
background: rgba(189, 153, 121, 0.1); background: rgba(189, 153, 121, 0.1);
} }
} }
&.scope {
border: 1px solid rgba(113, 144, 249, 0.2);
.ant-typography {
color: var(--bg-robin-400);
background: rgba(113, 144, 249, 0.1);
font-size: 14px;
}
.ant-tag-close-icon {
background: rgba(113, 144, 249, 0.1);
}
}
} }
} }
} }

View File

@ -94,6 +94,25 @@
letter-spacing: -0.06px; letter-spacing: -0.06px;
} }
} }
&.scope {
border-radius: 50px;
background: rgba(113, 144, 249, 0.1) !important;
color: var(--bg-robin-400) !important;
.dot {
background-color: var(--bg-robin-400);
}
.text {
color: var(--bg-robin-400);
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 150% */
letter-spacing: -0.06px;
}
}
} }
} }
.option-meta-data-container { .option-meta-data-container {

View File

@ -16,4 +16,5 @@ export type Option = {
selected?: boolean; selected?: boolean;
dataType?: string; dataType?: string;
isIndexed?: boolean; isIndexed?: boolean;
type?: string;
}; };

View File

@ -113,7 +113,9 @@ function SideNav({
if (!isOnboardingEnabled || !isCloudUser()) { if (!isOnboardingEnabled || !isCloudUser()) {
let items = [...menuItems]; let items = [...menuItems];
items = items.filter((item) => item.key !== ROUTES.GET_STARTED); items = items.filter(
(item) => item.key !== ROUTES.GET_STARTED && item.key !== ROUTES.ONBOARDING,
);
setMenuItems(items); setMenuItems(items);
} }

View File

@ -27,6 +27,7 @@ export const routeConfig: Record<string, QueryParams[]> = {
[ROUTES.ERROR_DETAIL]: [QueryParams.resourceAttributes], [ROUTES.ERROR_DETAIL]: [QueryParams.resourceAttributes],
[ROUTES.HOME_PAGE]: [QueryParams.resourceAttributes], [ROUTES.HOME_PAGE]: [QueryParams.resourceAttributes],
[ROUTES.GET_STARTED]: [QueryParams.resourceAttributes], [ROUTES.GET_STARTED]: [QueryParams.resourceAttributes],
[ROUTES.ONBOARDING]: [QueryParams.resourceAttributes],
[ROUTES.LIST_ALL_ALERT]: [QueryParams.resourceAttributes], [ROUTES.LIST_ALL_ALERT]: [QueryParams.resourceAttributes],
[ROUTES.LIST_LICENSES]: [QueryParams.resourceAttributes], [ROUTES.LIST_LICENSES]: [QueryParams.resourceAttributes],
[ROUTES.LOGIN]: [QueryParams.resourceAttributes], [ROUTES.LOGIN]: [QueryParams.resourceAttributes],

View File

@ -0,0 +1,29 @@
import getOnboardingStatus, {
OnboardingStatusResponse,
} from 'api/messagingQueues/onboarding/getOnboardingStatus';
import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query';
import { ErrorResponse, SuccessResponse } from 'types/api';
type UseOnboardingStatus = (
options?: UseQueryOptions<
SuccessResponse<OnboardingStatusResponse> | ErrorResponse
>,
endpointService?: string,
queryKey?: string,
) => UseQueryResult<SuccessResponse<OnboardingStatusResponse> | ErrorResponse>;
export const useOnboardingStatus: UseOnboardingStatus = (
options,
endpointService,
queryKey,
) =>
useQuery<SuccessResponse<OnboardingStatusResponse> | ErrorResponse>({
queryKey: [queryKey || `onboardingStatus-${endpointService}`],
queryFn: () =>
getOnboardingStatus({
start: (Date.now() - 15 * 60 * 1000) * 1_000_000,
end: Date.now() * 1_000_000,
endpointService,
}),
...options,
});

View File

@ -46,6 +46,7 @@ export const useOptions = (
value: item.key, value: item.key,
dataType: item.dataType, dataType: item.dataType,
isIndexed: item?.isIndexed, isIndexed: item?.isIndexed,
type: item?.type || '',
})), })),
[getLabel], [getLabel],
); );

View File

@ -163,7 +163,8 @@ export const getUPlotChartOptions = ({
const stackBarChart = stackChart && isUndefined(hiddenGraph); const stackBarChart = stackChart && isUndefined(hiddenGraph);
const isAnomalyRule = apiResponse?.data?.newResult?.data?.result[0].isAnomaly; const isAnomalyRule =
apiResponse?.data?.newResult?.data?.result[0]?.isAnomaly || false;
const series = getStackedSeries(apiResponse?.data?.result || []); const series = getStackedSeries(apiResponse?.data?.result || []);

View File

@ -57,8 +57,11 @@ export const useAlertHistoryQueryParams = (): {
const startTime = params.get(QueryParams.startTime); const startTime = params.get(QueryParams.startTime);
const endTime = params.get(QueryParams.endTime); const endTime = params.get(QueryParams.endTime);
const relativeTimeParam = params.get(QueryParams.relativeTime);
const relativeTime = const relativeTime =
params.get(QueryParams.relativeTime) ?? RelativeTimeMap['6hr']; (relativeTimeParam === 'null' ? null : relativeTimeParam) ??
RelativeTimeMap['6hr'];
const intStartTime = parseInt(startTime || '0', 10); const intStartTime = parseInt(startTime || '0', 10);
const intEndTime = parseInt(endTime || '0', 10); const intEndTime = parseInt(endTime || '0', 10);

View File

@ -66,9 +66,9 @@ export const LogsQuickFiltersConfig: IQuickFiltersConfig[] = [
type: FiltersType.CHECKBOX, type: FiltersType.CHECKBOX,
title: 'Hostname', title: 'Hostname',
attributeKey: { attributeKey: {
key: 'hostname', key: 'host.name',
dataType: DataTypes.String, dataType: DataTypes.String,
type: 'tag', type: 'resource',
isColumn: false, isColumn: false,
isJSON: false, isJSON: false,
}, },

View File

@ -1,26 +1,65 @@
/* eslint-disable no-nested-ternary */
import '../MessagingQueues.styles.scss'; import '../MessagingQueues.styles.scss';
import { Select, Typography } from 'antd'; import { Select, Typography } from 'antd';
import logEvent from 'api/common/logEvent'; import logEvent from 'api/common/logEvent';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2'; import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import useUrlQuery from 'hooks/useUrlQuery';
import { ListMinus } from 'lucide-react'; import { ListMinus } from 'lucide-react';
import { useEffect } from 'react'; import { useEffect, useState } from 'react';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { MessagingQueuesViewType } from '../MessagingQueuesUtils'; import {
import { SelectLabelWithComingSoon } from '../MQCommon/MQCommon'; MessagingQueuesViewType,
MessagingQueuesViewTypeOptions,
ProducerLatencyOptions,
} from '../MessagingQueuesUtils';
import DropRateView from '../MQDetails/DropRateView/DropRateView';
import MessagingQueueOverview from '../MQDetails/MessagingQueueOverview';
import MessagingQueuesDetails from '../MQDetails/MQDetails'; import MessagingQueuesDetails from '../MQDetails/MQDetails';
import MessagingQueuesConfigOptions from '../MQGraph/MQConfigOptions'; import MessagingQueuesConfigOptions from '../MQGraph/MQConfigOptions';
import MessagingQueuesGraph from '../MQGraph/MQGraph'; import MessagingQueuesGraph from '../MQGraph/MQGraph';
function MQDetailPage(): JSX.Element { function MQDetailPage(): JSX.Element {
const history = useHistory(); const history = useHistory();
const [
selectedView,
setSelectedView,
] = useState<MessagingQueuesViewTypeOptions>(
MessagingQueuesViewType.consumerLag.value,
);
const [
producerLatencyOption,
setproducerLatencyOption,
] = useState<ProducerLatencyOptions>(ProducerLatencyOptions.Producers);
const mqServiceView = useUrlQuery().get(
QueryParams.mqServiceView,
) as MessagingQueuesViewTypeOptions;
useEffect(() => { useEffect(() => {
logEvent('Messaging Queues: Detail page visited', {}); logEvent('Messaging Queues: Detail page visited', {});
}, []); }, []);
useEffect(() => {
if (mqServiceView) {
setSelectedView(mqServiceView);
}
}, [mqServiceView]);
const updateUrlQuery = (query: Record<string, string | number>): void => {
const searchParams = new URLSearchParams(history.location.search);
Object.keys(query).forEach((key) => {
searchParams.set(key, query[key].toString());
});
history.push({
search: searchParams.toString(),
});
};
return ( return (
<div className="messaging-queue-container"> <div className="messaging-queue-container">
<div className="messaging-breadcrumb"> <div className="messaging-breadcrumb">
@ -39,50 +78,55 @@ function MQDetailPage(): JSX.Element {
className="messaging-queue-options" className="messaging-queue-options"
defaultValue={MessagingQueuesViewType.consumerLag.value} defaultValue={MessagingQueuesViewType.consumerLag.value}
popupClassName="messaging-queue-options-popup" popupClassName="messaging-queue-options-popup"
onChange={(value): void => {
setSelectedView(value);
updateUrlQuery({ [QueryParams.mqServiceView]: value });
}}
value={mqServiceView}
options={[ options={[
{ {
label: MessagingQueuesViewType.consumerLag.label, label: MessagingQueuesViewType.consumerLag.label,
value: MessagingQueuesViewType.consumerLag.value, value: MessagingQueuesViewType.consumerLag.value,
}, },
{ {
label: ( label: MessagingQueuesViewType.partitionLatency.label,
<SelectLabelWithComingSoon
label={MessagingQueuesViewType.partitionLatency.label}
/>
),
value: MessagingQueuesViewType.partitionLatency.value, value: MessagingQueuesViewType.partitionLatency.value,
disabled: true,
}, },
{ {
label: ( label: MessagingQueuesViewType.producerLatency.label,
<SelectLabelWithComingSoon
label={MessagingQueuesViewType.producerLatency.label}
/>
),
value: MessagingQueuesViewType.producerLatency.value, value: MessagingQueuesViewType.producerLatency.value,
disabled: true,
}, },
{ {
label: ( label: MessagingQueuesViewType.dropRate.label,
<SelectLabelWithComingSoon value: MessagingQueuesViewType.dropRate.value,
label={MessagingQueuesViewType.consumerLatency.label}
/>
),
value: MessagingQueuesViewType.consumerLatency.value,
disabled: true,
}, },
]} ]}
/> />
</div> </div>
<DateTimeSelectionV2 showAutoRefresh={false} hideShareModal /> <DateTimeSelectionV2 showAutoRefresh={false} hideShareModal />
</div> </div>
{selectedView === MessagingQueuesViewType.consumerLag.value ? (
<div className="messaging-queue-main-graph"> <div className="messaging-queue-main-graph">
<MessagingQueuesConfigOptions /> <MessagingQueuesConfigOptions />
<MessagingQueuesGraph /> <MessagingQueuesGraph />
</div> </div>
) : selectedView === MessagingQueuesViewType.dropRate.value ? (
<DropRateView />
) : (
<MessagingQueueOverview
selectedView={selectedView}
option={producerLatencyOption}
setOption={setproducerLatencyOption}
/>
)}
{selectedView !== MessagingQueuesViewType.dropRate.value && (
<div className="messaging-queue-details"> <div className="messaging-queue-details">
<MessagingQueuesDetails /> <MessagingQueuesDetails
selectedView={selectedView}
producerLatencyOption={producerLatencyOption}
/>
</div> </div>
)}
</div> </div>
); );
} }

View File

@ -0,0 +1,30 @@
.evaluation-time-selector {
display: flex;
align-items: center;
gap: 8px;
.eval-title {
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 500;
line-height: 28px;
color: var(--bg-vanilla-200);
}
.ant-selector {
background-color: var(--bg-ink-400);
border-radius: 4px;
border: 1px solid var(--bg-slate-400);
box-shadow: none;
}
}
.select-dropdown-render {
padding: 8px;
display: flex;
justify-content: center;
align-items: center;
width: 200px;
margin: 6px;
}

View File

@ -0,0 +1,251 @@
/* eslint-disable sonarjs/no-duplicate-string */
import '../MQDetails.style.scss';
import { Table, Typography } from 'antd';
import axios from 'axios';
import cx from 'classnames';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import ROUTES from 'constants/routes';
import { useNotifications } from 'hooks/useNotifications';
import { isNumber } from 'lodash-es';
import {
convertToTitleCase,
MessagingQueuesViewType,
RowData,
} from 'pages/MessagingQueues/MessagingQueuesUtils';
import { useEffect, useMemo, useState } from 'react';
import { useMutation } from 'react-query';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { MessagingQueueServicePayload } from '../MQTables/getConsumerLagDetails';
import { getKafkaSpanEval } from '../MQTables/getKafkaSpanEval';
import {
convertToMilliseconds,
DropRateAPIResponse,
DropRateResponse,
} from './dropRateViewUtils';
import EvaluationTimeSelector from './EvaluationTimeSelector';
export function getTableData(data: DropRateResponse[]): RowData[] {
if (data?.length === 0) {
return [];
}
const tableData: RowData[] =
data?.map(
(row: DropRateResponse, index: number): RowData => ({
...(row.data as any), // todo-sagar
key: index,
}),
) || [];
return tableData;
}
// eslint-disable-next-line sonarjs/cognitive-complexity
export function getColumns(
data: DropRateResponse[],
visibleCounts: Record<number, number>,
handleShowMore: (index: number) => void,
): any[] {
if (data?.length === 0) {
return [];
}
const columnsOrder = [
'producer_service',
'consumer_service',
'breach_percentage',
'top_traceIDs',
'breached_spans',
'total_spans',
];
const columns: {
title: string;
dataIndex: string;
key: string;
}[] = columnsOrder.map((column) => ({
title: convertToTitleCase(column),
dataIndex: column,
key: column,
render: (
text: string | string[],
_record: any,
index: number,
): JSX.Element => {
if (Array.isArray(text)) {
const visibleCount = visibleCounts[index] || 4;
const visibleItems = text.slice(0, visibleCount);
const remainingCount = (text || []).length - visibleCount;
return (
<div>
<div className="trace-id-list">
{visibleItems.map((item, idx) => {
const shouldShowMore = remainingCount > 0 && idx === visibleCount - 1;
return (
<div key={item} className="traceid-style">
<Typography.Text
key={item}
className="traceid-text"
onClick={(): void => {
window.open(`${ROUTES.TRACE}/${item}`, '_blank');
}}
>
{item}
</Typography.Text>
{shouldShowMore && (
<Typography
onClick={(): void => handleShowMore(index)}
className="remaing-count"
>
+ {remainingCount} more
</Typography>
)}
</div>
);
})}
</div>
</div>
);
}
if (column === 'consumer_service' || column === 'producer_service') {
return (
<Typography.Link
onClick={(e): void => {
e.preventDefault();
e.stopPropagation();
window.open(`/services/${encodeURIComponent(text)}`, '_blank');
}}
>
{text}
</Typography.Link>
);
}
if (column === 'breach_percentage' && text) {
if (!isNumber(text))
return <Typography.Text>{text.toString()}</Typography.Text>;
return (
<Typography.Text>
{(typeof text === 'string' ? parseFloat(text) : text).toFixed(2)} %
</Typography.Text>
);
}
return <Typography.Text>{text}</Typography.Text>;
},
}));
return columns;
}
const showPaginationItem = (total: number, range: number[]): JSX.Element => (
<>
<Typography.Text className="numbers">
{range[0]} &#8212; {range[1]}
</Typography.Text>
<Typography.Text className="total"> of {total}</Typography.Text>
</>
);
function DropRateView(): JSX.Element {
const [columns, setColumns] = useState<any[]>([]);
const [tableData, setTableData] = useState<any[]>([]);
const { notifications } = useNotifications();
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const [data, setData] = useState<
DropRateAPIResponse['data']['result'][0]['list']
>([]);
const [interval, setInterval] = useState<string>('');
const [visibleCounts, setVisibleCounts] = useState<Record<number, number>>({});
const paginationConfig = useMemo(
() =>
tableData?.length > 10 && {
pageSize: 10,
showTotal: showPaginationItem,
showSizeChanger: false,
hideOnSinglePage: true,
},
[tableData],
);
const evaluationTime = useMemo(() => convertToMilliseconds(interval), [
interval,
]);
const tableApiPayload: MessagingQueueServicePayload = useMemo(
() => ({
start: minTime,
end: maxTime,
evalTime: evaluationTime * 1e6,
}),
[evaluationTime, maxTime, minTime],
);
const handleOnError = (error: Error): void => {
notifications.error({
message: axios.isAxiosError(error) ? error?.message : SOMETHING_WENT_WRONG,
});
};
const handleShowMore = (index: number): void => {
setVisibleCounts((prevCounts) => ({
...prevCounts,
[index]: (prevCounts[index] || 4) + 4,
}));
};
const { mutate: getViewDetails, isLoading } = useMutation(getKafkaSpanEval, {
onSuccess: (data) => {
if (data.payload) {
setData(data.payload.result[0].list);
}
},
onError: handleOnError,
});
useEffect(() => {
if (data?.length > 0) {
setColumns(getColumns(data, visibleCounts, handleShowMore));
setTableData(getTableData(data));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data, visibleCounts]);
useEffect(() => {
if (evaluationTime) {
getViewDetails(tableApiPayload);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [minTime, maxTime, evaluationTime]);
return (
<div className={cx('mq-overview-container', 'droprate-view')}>
<div className="mq-overview-title">
<div className="drop-rat-title">
{MessagingQueuesViewType.dropRate.label}
</div>
<EvaluationTimeSelector setInterval={setInterval} />
</div>
<Table
className={cx('mq-table', 'pagination-left')}
pagination={paginationConfig}
size="middle"
columns={columns}
dataSource={tableData}
bordered={false}
loading={isLoading}
/>
</div>
);
}
export default DropRateView;

View File

@ -0,0 +1,111 @@
import './DropRateView.styles.scss';
import { Input, Select, Typography } from 'antd';
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
const { Option } = Select;
interface SelectDropdownRenderProps {
menu: React.ReactNode;
inputValue: string;
handleInputChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
handleKeyDown: (e: React.KeyboardEvent<HTMLInputElement>) => void;
handleAddCustomValue: () => void;
}
function SelectDropdownRender({
menu,
inputValue,
handleInputChange,
handleAddCustomValue,
handleKeyDown,
}: SelectDropdownRenderProps): JSX.Element {
return (
<>
{menu}
<Input
placeholder="Enter custom time (ms)"
value={inputValue}
onChange={handleInputChange}
onKeyDown={handleKeyDown}
onBlur={handleAddCustomValue}
className="select-dropdown-render"
/>
</>
);
}
function EvaluationTimeSelector({
setInterval,
}: {
setInterval: Dispatch<SetStateAction<string>>;
}): JSX.Element {
const [inputValue, setInputValue] = useState<string>('');
const [selectedInterval, setSelectedInterval] = useState<string | null>('5ms');
const [dropdownOpen, setDropdownOpen] = useState<boolean>(false);
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
setInputValue(e.target.value);
};
const handleSelectChange = (value: string): void => {
setSelectedInterval(value);
setInputValue('');
setDropdownOpen(false);
};
const handleAddCustomValue = (): void => {
setSelectedInterval(inputValue);
setInputValue(inputValue);
setDropdownOpen(false);
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>): void => {
if (e.key === 'Enter') {
e.preventDefault();
e.stopPropagation();
handleAddCustomValue();
}
};
const renderDropdown = (menu: React.ReactNode): JSX.Element => (
<SelectDropdownRender
menu={menu}
inputValue={inputValue}
handleInputChange={handleInputChange}
handleAddCustomValue={handleAddCustomValue}
handleKeyDown={handleKeyDown}
/>
);
useEffect(() => {
if (selectedInterval) {
setInterval(() => selectedInterval);
}
}, [selectedInterval, setInterval]);
return (
<div className="evaluation-time-selector">
<Typography.Text className="eval-title">
Evaluation Interval:
</Typography.Text>
<Select
style={{ width: 220 }}
placeholder="Select time interval (ms)"
value={selectedInterval}
onChange={handleSelectChange}
open={dropdownOpen}
onDropdownVisibleChange={setDropdownOpen}
dropdownRender={renderDropdown}
>
<Option value="1ms">1ms</Option>
<Option value="2ms">2ms</Option>
<Option value="5ms">5ms</Option>
<Option value="10ms">10ms</Option>
<Option value="15ms">15ms</Option>
</Select>
</div>
);
}
export default EvaluationTimeSelector;

View File

@ -0,0 +1,46 @@
export function convertToMilliseconds(timeInput: string): number {
if (!timeInput.trim()) {
return 0;
}
const match = timeInput.match(/^(\d+)(ms|s|ns)?$/); // Match number and optional unit
if (!match) {
throw new Error(`Invalid time format: ${timeInput}`);
}
const value = parseInt(match[1], 10);
const unit = match[2] || 'ms'; // Default to 'ms' if no unit is provided
switch (unit) {
case 's':
return value * 1e3;
case 'ms':
return value;
case 'ns':
return value / 1e6;
default:
throw new Error('Invalid time format');
}
}
export interface DropRateResponse {
timestamp: string;
data: {
breach_percentage: number;
breached_spans: number;
consumer_service: string;
producer_service: string;
top_traceIDs: string[];
total_spans: number;
};
}
export interface DropRateAPIResponse {
status: string;
data: {
resultType: string;
result: {
queryName: string;
list: DropRateResponse[];
}[];
};
}

View File

@ -4,3 +4,115 @@
flex-direction: column; flex-direction: column;
gap: 24px; gap: 24px;
} }
.mq-overview-container {
display: flex;
padding: 24px;
flex-direction: column;
align-items: start;
gap: 16px;
border-radius: 6px;
border: 1px solid var(--bg-slate-500);
background: var(--bg-ink-500);
.mq-overview-title {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
.drop-rat-title {
color: var(--bg-vanilla-200);
font-family: Inter;
font-size: 18px;
font-style: normal;
font-weight: 500;
line-height: 28px;
}
}
.mq-details-options {
letter-spacing: -0.06px;
cursor: pointer;
.ant-radio-button-wrapper {
border-color: var(--bg-slate-400);
color: var(--bg-vanilla-400);
}
.ant-radio-button-wrapper-checked {
background: var(--bg-slate-400);
color: var(--bg-vanilla-100);
}
.ant-radio-button-wrapper::before {
width: 0px;
}
}
}
.droprate-view {
.mq-table {
width: 100%;
.ant-table-content {
border-radius: 6px;
border: 1px solid var(--bg-slate-500);
box-shadow: 0px 4px 12px 0px rgba(0, 0, 0, 0.1);
}
.ant-table-tbody {
.ant-table-cell {
max-width: 250px;
border-bottom: none;
}
}
.ant-table-thead {
.ant-table-cell {
background-color: var(--bg-ink-500);
border-bottom: 1px solid var(--bg-slate-500);
}
}
}
.trace-id-list {
display: flex;
flex-direction: column;
gap: 4px;
width: max-content;
.traceid-style {
display: flex;
gap: 8px;
align-items: center;
.traceid-text {
border-radius: 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-slate-400);
padding: 2px;
cursor: pointer;
}
.remaing-count {
cursor: pointer;
color: var(--bg-vanilla-100);
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: normal;
letter-spacing: -0.06px;
}
}
}
}
.pagination-left {
&.mq-table {
.ant-pagination {
justify-content: flex-start;
}
}
}

View File

@ -1,65 +1,222 @@
import './MQDetails.style.scss'; import './MQDetails.style.scss';
import { Radio } from 'antd'; import { Radio } from 'antd';
import { Dispatch, SetStateAction, useState } from 'react'; import { QueryParams } from 'constants/query';
import useUrlQuery from 'hooks/useUrlQuery';
import { isEmpty } from 'lodash-es';
import { Dispatch, SetStateAction, useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { import {
ConsumerLagDetailTitle, ConsumerLagDetailTitle,
ConsumerLagDetailType, getMetaDataAndAPIPerView,
MessagingQueueServiceDetailType,
MessagingQueuesViewType,
MessagingQueuesViewTypeOptions,
ProducerLatencyOptions,
SelectedTimelineQuery,
} from '../MessagingQueuesUtils'; } from '../MessagingQueuesUtils';
import { ComingSoon } from '../MQCommon/MQCommon'; import { ComingSoon } from '../MQCommon/MQCommon';
import MessagingQueuesTable from './MQTables/MQTables'; import MessagingQueuesTable from './MQTables/MQTables';
const MQServiceDetailTypePerView = (
producerLatencyOption: ProducerLatencyOptions,
): Record<string, MessagingQueueServiceDetailType[]> => ({
[MessagingQueuesViewType.consumerLag.value]: [
MessagingQueueServiceDetailType.ConsumerDetails,
MessagingQueueServiceDetailType.ProducerDetails,
MessagingQueueServiceDetailType.NetworkLatency,
MessagingQueueServiceDetailType.PartitionHostMetrics,
],
[MessagingQueuesViewType.partitionLatency.value]: [
MessagingQueueServiceDetailType.ConsumerDetails,
MessagingQueueServiceDetailType.ProducerDetails,
],
[MessagingQueuesViewType.producerLatency.value]: [
producerLatencyOption === ProducerLatencyOptions.Consumers
? MessagingQueueServiceDetailType.ConsumerDetails
: MessagingQueueServiceDetailType.ProducerDetails,
],
});
interface MessagingQueuesOptionsProps {
currentTab: MessagingQueueServiceDetailType;
setCurrentTab: Dispatch<SetStateAction<MessagingQueueServiceDetailType>>;
selectedView: MessagingQueuesViewTypeOptions;
producerLatencyOption: ProducerLatencyOptions;
}
function MessagingQueuesOptions({ function MessagingQueuesOptions({
currentTab, currentTab,
setCurrentTab, setCurrentTab,
}: { selectedView,
currentTab: ConsumerLagDetailType; producerLatencyOption,
setCurrentTab: Dispatch<SetStateAction<ConsumerLagDetailType>>; }: MessagingQueuesOptionsProps): JSX.Element {
}): JSX.Element { const handleChange = (value: MessagingQueueServiceDetailType): void => {
const [option, setOption] = useState<ConsumerLagDetailType>(currentTab); setCurrentTab(value);
};
const renderRadioButtons = (): JSX.Element[] => {
const detailTypes =
MQServiceDetailTypePerView(producerLatencyOption)[selectedView] || [];
return detailTypes.map((detailType) => (
<Radio.Button
key={detailType}
value={detailType}
disabled={
detailType === MessagingQueueServiceDetailType.PartitionHostMetrics
}
className={
detailType === MessagingQueueServiceDetailType.PartitionHostMetrics
? 'disabled-option'
: ''
}
>
{ConsumerLagDetailTitle[detailType]}
{detailType === MessagingQueueServiceDetailType.PartitionHostMetrics && (
<ComingSoon />
)}
</Radio.Button>
));
};
return ( return (
<Radio.Group <Radio.Group
onChange={(value): void => { onChange={(e): void => handleChange(e.target.value)}
setOption(value.target.value); value={currentTab}
setCurrentTab(value.target.value);
}}
value={option}
className="mq-details-options" className="mq-details-options"
> >
<Radio.Button value={ConsumerLagDetailType.ConsumerDetails} checked> {renderRadioButtons()}
{ConsumerLagDetailTitle[ConsumerLagDetailType.ConsumerDetails]}
</Radio.Button>
<Radio.Button value={ConsumerLagDetailType.ProducerDetails}>
{ConsumerLagDetailTitle[ConsumerLagDetailType.ProducerDetails]}
</Radio.Button>
<Radio.Button value={ConsumerLagDetailType.NetworkLatency}>
{ConsumerLagDetailTitle[ConsumerLagDetailType.NetworkLatency]}
</Radio.Button>
<Radio.Button
value={ConsumerLagDetailType.PartitionHostMetrics}
disabled
className="disabled-option"
>
{ConsumerLagDetailTitle[ConsumerLagDetailType.PartitionHostMetrics]}
<ComingSoon />
</Radio.Button>
</Radio.Group> </Radio.Group>
); );
} }
function MessagingQueuesDetails(): JSX.Element { const checkValidityOfDetailConfigs = (
const [currentTab, setCurrentTab] = useState<ConsumerLagDetailType>( selectedTimelineQuery: SelectedTimelineQuery,
ConsumerLagDetailType.ConsumerDetails, selectedView: MessagingQueuesViewTypeOptions,
currentTab: MessagingQueueServiceDetailType,
configDetails?: {
[key: string]: string;
},
// eslint-disable-next-line sonarjs/cognitive-complexity
): boolean => {
if (selectedView === MessagingQueuesViewType.consumerLag.value) {
return !(
isEmpty(selectedTimelineQuery) ||
(!selectedTimelineQuery?.group &&
!selectedTimelineQuery?.topic &&
!selectedTimelineQuery?.partition)
); );
}
if (selectedView === MessagingQueuesViewType.partitionLatency.value) {
if (isEmpty(configDetails)) {
return false;
}
return Boolean(configDetails?.topic && configDetails?.partition);
}
if (selectedView === MessagingQueuesViewType.producerLatency.value) {
if (isEmpty(configDetails)) {
return false;
}
if (currentTab === MessagingQueueServiceDetailType.ProducerDetails) {
return Boolean(
configDetails?.topic &&
configDetails?.partition &&
configDetails?.service_name,
);
}
return Boolean(configDetails?.topic && configDetails?.service_name);
}
return selectedView === MessagingQueuesViewType.dropRate.value;
};
function MessagingQueuesDetails({
selectedView,
producerLatencyOption,
}: {
selectedView: MessagingQueuesViewTypeOptions;
producerLatencyOption: ProducerLatencyOptions;
}): JSX.Element {
const [currentTab, setCurrentTab] = useState<MessagingQueueServiceDetailType>(
MessagingQueueServiceDetailType.ConsumerDetails,
);
useEffect(() => {
if (
producerLatencyOption &&
selectedView === MessagingQueuesViewType.producerLatency.value
) {
setCurrentTab(
producerLatencyOption === ProducerLatencyOptions.Consumers
? MessagingQueueServiceDetailType.ConsumerDetails
: MessagingQueueServiceDetailType.ProducerDetails,
);
}
}, [selectedView, producerLatencyOption]);
const urlQuery = useUrlQuery();
const timelineQuery = decodeURIComponent(
urlQuery.get(QueryParams.selectedTimelineQuery) || '',
);
const timelineQueryData: SelectedTimelineQuery = useMemo(
() => (timelineQuery ? JSON.parse(timelineQuery) : {}),
[timelineQuery],
);
const configDetails = decodeURIComponent(
urlQuery.get(QueryParams.configDetail) || '',
);
const configDetailQueryData: {
[key: string]: string;
} = useMemo(() => (configDetails ? JSON.parse(configDetails) : {}), [
configDetails,
]);
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const serviceConfigDetails = useMemo(
() =>
getMetaDataAndAPIPerView({
detailType: currentTab,
minTime,
maxTime,
selectedTimelineQuery: timelineQueryData,
configDetails: configDetailQueryData,
}),
[configDetailQueryData, currentTab, maxTime, minTime, timelineQueryData],
);
return ( return (
<div className="mq-details"> <div className="mq-details">
<MessagingQueuesOptions <MessagingQueuesOptions
currentTab={currentTab} currentTab={currentTab}
setCurrentTab={setCurrentTab} setCurrentTab={setCurrentTab}
selectedView={selectedView}
producerLatencyOption={producerLatencyOption}
/>
<MessagingQueuesTable
currentTab={currentTab}
selectedView={selectedView}
tableApi={serviceConfigDetails[selectedView]?.tableApi}
validConfigPresent={checkValidityOfDetailConfigs(
timelineQueryData,
selectedView,
currentTab,
configDetailQueryData,
)}
tableApiPayload={serviceConfigDetails[selectedView]?.tableApiPayload}
/> />
<MessagingQueuesTable currentTab={currentTab} />
</div> </div>
); );
} }

View File

@ -1,4 +1,7 @@
.mq-tables-container { .mq-tables-container {
width: 100%;
height: 100%;
.mq-table-title { .mq-table-title {
display: flex; display: flex;
align-items: center; align-items: center;
@ -31,9 +34,6 @@
.ant-table-tbody { .ant-table-tbody {
.ant-table-cell { .ant-table-cell {
max-width: 250px; max-width: 250px;
background-color: var(--bg-ink-400);
border-bottom: none; border-bottom: none;
} }
} }
@ -63,6 +63,21 @@
} }
} }
.mq-table {
&.mq-overview-row-clickable {
.ant-table-row {
background-color: var(--bg-ink-400);
&:hover {
cursor: pointer;
background-color: var(--bg-slate-400) !important;
color: var(--bg-vanilla-400);
transition: background-color 0.3s ease, color 0.3s ease;
}
}
}
}
.lightMode { .lightMode {
.mq-tables-container { .mq-tables-container {
.mq-table-title { .mq-table-title {

View File

@ -1,9 +1,11 @@
/* eslint-disable no-nested-ternary */
/* eslint-disable react/require-default-props */
import './MQTables.styles.scss'; import './MQTables.styles.scss';
import { Skeleton, Table, Typography } from 'antd'; import { Skeleton, Table, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import axios from 'axios'; import axios from 'axios';
import { isNumber } from 'chart.js/helpers'; import { isNumber } from 'chart.js/helpers';
import cx from 'classnames';
import { ColumnTypeRender } from 'components/Logs/TableView/types'; import { ColumnTypeRender } from 'components/Logs/TableView/types';
import { SOMETHING_WENT_WRONG } from 'constants/api'; import { SOMETHING_WENT_WRONG } from 'constants/api';
import { QueryParams } from 'constants/query'; import { QueryParams } from 'constants/query';
@ -13,27 +15,31 @@ import useUrlQuery from 'hooks/useUrlQuery';
import { isEmpty } from 'lodash-es'; import { isEmpty } from 'lodash-es';
import { import {
ConsumerLagDetailTitle, ConsumerLagDetailTitle,
ConsumerLagDetailType,
convertToTitleCase, convertToTitleCase,
MessagingQueueServiceDetailType,
MessagingQueuesViewType,
MessagingQueuesViewTypeOptions,
RowData, RowData,
SelectedTimelineQuery, SelectedTimelineQuery,
setConfigDetail,
} from 'pages/MessagingQueues/MessagingQueuesUtils'; } from 'pages/MessagingQueues/MessagingQueuesUtils';
import { useEffect, useMemo, useRef, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { useMutation } from 'react-query'; import { useMutation } from 'react-query';
import { useHistory } from 'react-router-dom'; import { useHistory, useLocation } from 'react-router-dom';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { import {
ConsumerLagPayload, MessagingQueueServicePayload,
getConsumerLagDetails,
MessagingQueuesPayloadProps, MessagingQueuesPayloadProps,
} from './getConsumerLagDetails'; } from './getConsumerLagDetails';
const INITIAL_PAGE_SIZE = 10;
// eslint-disable-next-line sonarjs/cognitive-complexity // eslint-disable-next-line sonarjs/cognitive-complexity
export function getColumns( export function getColumns(
data: MessagingQueuesPayloadProps['payload'], data: MessagingQueuesPayloadProps['payload'],
history: History<unknown>, history: History<unknown>,
): RowData[] { ): RowData[] {
console.log(data);
if (data?.result?.length === 0) { if (data?.result?.length === 0) {
return []; return [];
} }
@ -105,10 +111,25 @@ const showPaginationItem = (total: number, range: number[]): JSX.Element => (
</> </>
); );
// eslint-disable-next-line sonarjs/cognitive-complexity
function MessagingQueuesTable({ function MessagingQueuesTable({
currentTab, currentTab,
selectedView,
tableApiPayload,
tableApi,
validConfigPresent = false,
type = 'Detail',
}: { }: {
currentTab: ConsumerLagDetailType; currentTab?: MessagingQueueServiceDetailType;
selectedView: MessagingQueuesViewTypeOptions;
tableApiPayload?: MessagingQueueServicePayload;
tableApi: (
props: MessagingQueueServicePayload,
) => Promise<
SuccessResponse<MessagingQueuesPayloadProps['payload']> | ErrorResponse
>;
validConfigPresent?: boolean;
type?: 'Detail' | 'Overview';
}): JSX.Element { }): JSX.Element {
const [columns, setColumns] = useState<any[]>([]); const [columns, setColumns] = useState<any[]>([]);
const [tableData, setTableData] = useState<any[]>([]); const [tableData, setTableData] = useState<any[]>([]);
@ -118,15 +139,26 @@ function MessagingQueuesTable({
const timelineQuery = decodeURIComponent( const timelineQuery = decodeURIComponent(
urlQuery.get(QueryParams.selectedTimelineQuery) || '', urlQuery.get(QueryParams.selectedTimelineQuery) || '',
); );
const timelineQueryData: SelectedTimelineQuery = useMemo( const timelineQueryData: SelectedTimelineQuery = useMemo(
() => (timelineQuery ? JSON.parse(timelineQuery) : {}), () => (timelineQuery ? JSON.parse(timelineQuery) : {}),
[timelineQuery], [timelineQuery],
); );
const configDetails = decodeURIComponent(
urlQuery.get(QueryParams.configDetail) || '',
);
const configDetailQueryData: {
[key: string]: string;
} = useMemo(() => (configDetails ? JSON.parse(configDetails) : {}), [
configDetails,
]);
const paginationConfig = useMemo( const paginationConfig = useMemo(
() => () =>
tableData?.length > 20 && { tableData?.length > INITIAL_PAGE_SIZE && {
pageSize: 20, pageSize: INITIAL_PAGE_SIZE,
showTotal: showPaginationItem, showTotal: showPaginationItem,
showSizeChanger: false, showSizeChanger: false,
hideOnSinglePage: true, hideOnSinglePage: true,
@ -134,28 +166,14 @@ function MessagingQueuesTable({
[tableData], [tableData],
); );
const props: ConsumerLagPayload = useMemo(
() => ({
start: (timelineQueryData?.start || 0) * 1e9,
end: (timelineQueryData?.end || 0) * 1e9,
variables: {
partition: timelineQueryData?.partition,
topic: timelineQueryData?.topic,
consumer_group: timelineQueryData?.group,
},
detailType: currentTab,
}),
[currentTab, timelineQueryData],
);
const handleConsumerDetailsOnError = (error: Error): void => { const handleConsumerDetailsOnError = (error: Error): void => {
notifications.error({ notifications.error({
message: axios.isAxiosError(error) ? error?.message : SOMETHING_WENT_WRONG, message: axios.isAxiosError(error) ? error?.message : SOMETHING_WENT_WRONG,
}); });
}; };
const { mutate: getConsumerDetails, isLoading } = useMutation( const { mutate: getViewDetails, isLoading, error, isError } = useMutation(
getConsumerLagDetails, tableApi,
{ {
onSuccess: (data) => { onSuccess: (data) => {
if (data.payload) { if (data.payload) {
@ -167,57 +185,92 @@ function MessagingQueuesTable({
}, },
); );
// eslint-disable-next-line react-hooks/exhaustive-deps useEffect(
useEffect(() => getConsumerDetails(props), [currentTab, props]); () => {
if (validConfigPresent && tableApiPayload) {
const isLogEventCalled = useRef<boolean>(false); getViewDetails(tableApiPayload);
}
const isEmptyDetails = (timelineQueryData: SelectedTimelineQuery): boolean => { },
const isEmptyDetail = // eslint-disable-next-line react-hooks/exhaustive-deps
isEmpty(timelineQueryData) || [currentTab, selectedView, tableApiPayload],
(!timelineQueryData?.group && );
!timelineQueryData?.topic &&
!timelineQueryData?.partition); const [selectedRowKey, setSelectedRowKey] = useState<React.Key>();
const [, setSelectedRows] = useState<any>();
if (!isEmptyDetail && !isLogEventCalled.current) { const location = useLocation();
logEvent('Messaging Queues: More details viewed', {
'tab-option': ConsumerLagDetailTitle[currentTab], const onRowClick = (record: { [key: string]: string }): void => {
variables: { const selectedKey = record.key;
group: timelineQueryData?.group,
topic: timelineQueryData?.topic, if (`${selectedKey}_${selectedView}` === selectedRowKey) {
partition: timelineQueryData?.partition, setSelectedRowKey(undefined);
}, setSelectedRows({});
}); setConfigDetail(urlQuery, location, history, {});
isLogEventCalled.current = true; } else {
setSelectedRowKey(`${selectedKey}_${selectedView}`);
setSelectedRows(record);
if (!isEmpty(record)) {
setConfigDetail(urlQuery, location, history, record);
}
} }
return isEmptyDetail;
}; };
const subtitle =
selectedView === MessagingQueuesViewType.consumerLag.value
? `${timelineQueryData?.group || ''} ${timelineQueryData?.topic || ''} ${
timelineQueryData?.partition || ''
}`
: `${configDetailQueryData?.service_name || ''} ${
configDetailQueryData?.topic || ''
} ${configDetailQueryData?.partition || ''}`;
return ( return (
<div className="mq-tables-container"> <div className="mq-tables-container">
{isEmptyDetails(timelineQueryData) ? ( {!validConfigPresent ? (
<div className="no-data-style"> <div className="no-data-style">
<Typography.Text> <Typography.Text>
Click on a co-ordinate above to see the details {selectedView === MessagingQueuesViewType.consumerLag.value
? 'Click on a co-ordinate above to see the details'
: 'Click on a row above to see the details'}
</Typography.Text> </Typography.Text>
<Skeleton /> <Skeleton />
</div> </div>
) : isError ? (
<div className="no-data-style">
<Typography.Text>{error?.message || SOMETHING_WENT_WRONG}</Typography.Text>
</div>
) : ( ) : (
<> <>
{currentTab && (
<div className="mq-table-title"> <div className="mq-table-title">
{ConsumerLagDetailTitle[currentTab]} {ConsumerLagDetailTitle[currentTab]}
<div className="mq-table-subtitle">{`${timelineQueryData?.group || ''} ${ <div className="mq-table-subtitle">{subtitle}</div>
timelineQueryData?.topic || ''
} ${timelineQueryData?.partition || ''}`}</div>
</div> </div>
)}
<Table <Table
className="mq-table" className={cx(
'mq-table',
type !== 'Detail' ? 'mq-overview-row-clickable' : 'pagination-left',
)}
pagination={paginationConfig} pagination={paginationConfig}
size="middle" size="middle"
columns={columns} columns={columns}
dataSource={tableData} dataSource={tableData}
bordered={false} bordered={false}
loading={isLoading} loading={isLoading}
onRow={(record): any =>
type !== 'Detail'
? {
onClick: (): void => onRowClick(record),
}
: {}
}
rowClassName={(record): any =>
`${record.key}_${selectedView}` === selectedRowKey
? 'ant-table-row-selected'
: ''
}
/> />
</> </>
)} )}

View File

@ -1,19 +1,18 @@
import axios from 'api'; import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; import { MessagingQueueServiceDetailType } from 'pages/MessagingQueues/MessagingQueuesUtils';
import { AxiosError } from 'axios';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { ConsumerLagDetailType } from 'pages/MessagingQueues/MessagingQueuesUtils';
import { ErrorResponse, SuccessResponse } from 'types/api'; import { ErrorResponse, SuccessResponse } from 'types/api';
export interface ConsumerLagPayload { export interface MessagingQueueServicePayload {
start?: number | string; start?: number | string;
end?: number | string; end?: number | string;
variables: { variables?: {
partition?: string; partition?: string;
topic?: string; topic?: string;
consumer_group?: string; consumer_group?: string;
service_name?: string;
}; };
detailType: ConsumerLagDetailType; detailType?: MessagingQueueServiceDetailType | 'producer' | 'consumer';
evalTime?: number;
} }
export interface MessagingQueuesPayloadProps { export interface MessagingQueuesPayloadProps {
@ -36,12 +35,11 @@ export interface MessagingQueuesPayloadProps {
} }
export const getConsumerLagDetails = async ( export const getConsumerLagDetails = async (
props: ConsumerLagPayload, props: MessagingQueueServicePayload,
): Promise< ): Promise<
SuccessResponse<MessagingQueuesPayloadProps['payload']> | ErrorResponse SuccessResponse<MessagingQueuesPayloadProps['payload']> | ErrorResponse
> => { > => {
const { detailType, ...restProps } = props; const { detailType, ...restProps } = props;
try {
const response = await axios.post( const response = await axios.post(
`/messaging-queues/kafka/consumer-lag/${props.detailType}`, `/messaging-queues/kafka/consumer-lag/${props.detailType}`,
{ {
@ -55,7 +53,4 @@ export const getConsumerLagDetails = async (
message: response.data.status, message: response.data.status,
payload: response.data.data, payload: response.data.data,
}; };
} catch (error) {
return ErrorResponseHandler((error as AxiosError) || SOMETHING_WENT_WRONG);
}
}; };

View File

@ -0,0 +1,23 @@
import axios from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { DropRateAPIResponse } from '../DropRateView/dropRateViewUtils';
import { MessagingQueueServicePayload } from './getConsumerLagDetails';
export const getKafkaSpanEval = async (
props: Omit<MessagingQueueServicePayload, 'detailType' | 'variables'>,
): Promise<SuccessResponse<DropRateAPIResponse['data']> | ErrorResponse> => {
const { start, end, evalTime } = props;
const response = await axios.post(`messaging-queues/kafka/span/evaluation`, {
start,
end,
eval_time: evalTime,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
};

View File

@ -0,0 +1,33 @@
import axios from 'api';
import { MessagingQueueServiceDetailType } from 'pages/MessagingQueues/MessagingQueuesUtils';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
MessagingQueueServicePayload,
MessagingQueuesPayloadProps,
} from './getConsumerLagDetails';
export const getPartitionLatencyDetails = async (
props: MessagingQueueServicePayload,
): Promise<
SuccessResponse<MessagingQueuesPayloadProps['payload']> | ErrorResponse
> => {
const { detailType, ...rest } = props;
let endpoint = '';
if (detailType === MessagingQueueServiceDetailType.ConsumerDetails) {
endpoint = `/messaging-queues/kafka/partition-latency/consumer`;
} else {
endpoint = `/messaging-queues/kafka/consumer-lag/producer-details`;
}
const response = await axios.post(endpoint, {
...rest,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
};

View File

@ -0,0 +1,27 @@
import axios from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
MessagingQueueServicePayload,
MessagingQueuesPayloadProps,
} from './getConsumerLagDetails';
export const getPartitionLatencyOverview = async (
props: Omit<MessagingQueueServicePayload, 'detailType' | 'variables'>,
): Promise<
SuccessResponse<MessagingQueuesPayloadProps['payload']> | ErrorResponse
> => {
const response = await axios.post(
`/messaging-queues/kafka/partition-latency/overview`,
{
...props,
},
);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
};

View File

@ -0,0 +1,26 @@
import axios from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
MessagingQueueServicePayload,
MessagingQueuesPayloadProps,
} from './getConsumerLagDetails';
export const getTopicThroughputDetails = async (
props: MessagingQueueServicePayload,
): Promise<
SuccessResponse<MessagingQueuesPayloadProps['payload']> | ErrorResponse
> => {
const { detailType, ...rest } = props;
const endpoint = `/messaging-queues/kafka/topic-throughput/${detailType}`;
const response = await axios.post(endpoint, {
...rest,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
};

View File

@ -0,0 +1,29 @@
import axios from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
MessagingQueueServicePayload,
MessagingQueuesPayloadProps,
} from './getConsumerLagDetails';
export const getTopicThroughputOverview = async (
props: Omit<MessagingQueueServicePayload, 'variables'>,
): Promise<
SuccessResponse<MessagingQueuesPayloadProps['payload']> | ErrorResponse
> => {
const { detailType, start, end } = props;
const response = await axios.post(
`messaging-queues/kafka/topic-throughput/${detailType}`,
{
start,
end,
},
);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
};

View File

@ -0,0 +1,110 @@
import './MQDetails.style.scss';
import { Radio } from 'antd';
import { Dispatch, SetStateAction } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import {
MessagingQueuesViewType,
MessagingQueuesViewTypeOptions,
ProducerLatencyOptions,
} from '../MessagingQueuesUtils';
import { MessagingQueueServicePayload } from './MQTables/getConsumerLagDetails';
import { getKafkaSpanEval } from './MQTables/getKafkaSpanEval';
import { getPartitionLatencyOverview } from './MQTables/getPartitionLatencyOverview';
import { getTopicThroughputOverview } from './MQTables/getTopicThroughputOverview';
import MessagingQueuesTable from './MQTables/MQTables';
type SelectedViewType = keyof typeof MessagingQueuesViewType;
function PartitionLatencyTabs({
option,
setOption,
}: {
option: ProducerLatencyOptions;
setOption: Dispatch<SetStateAction<ProducerLatencyOptions>>;
}): JSX.Element {
return (
<Radio.Group
onChange={(e): void => setOption(e.target.value)}
value={option}
className="mq-details-options"
>
<Radio.Button
value={ProducerLatencyOptions.Producers}
key={ProducerLatencyOptions.Producers}
>
{ProducerLatencyOptions.Producers}
</Radio.Button>
<Radio.Button
value={ProducerLatencyOptions.Consumers}
key={ProducerLatencyOptions.Consumers}
>
{ProducerLatencyOptions.Consumers}
</Radio.Button>
</Radio.Group>
);
}
const getTableApi = (selectedView: MessagingQueuesViewTypeOptions): any => {
if (selectedView === MessagingQueuesViewType.producerLatency.value) {
return getTopicThroughputOverview;
}
if (selectedView === MessagingQueuesViewType.dropRate.value) {
return getKafkaSpanEval;
}
return getPartitionLatencyOverview;
};
function MessagingQueueOverview({
selectedView,
option,
setOption,
}: {
selectedView: MessagingQueuesViewTypeOptions;
option: ProducerLatencyOptions;
setOption: Dispatch<SetStateAction<ProducerLatencyOptions>>;
}): JSX.Element {
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const tableApiPayload: MessagingQueueServicePayload = {
variables: {},
start: minTime,
end: maxTime,
detailType:
// eslint-disable-next-line no-nested-ternary
selectedView === MessagingQueuesViewType.producerLatency.value
? option === ProducerLatencyOptions.Producers
? 'producer'
: 'consumer'
: undefined,
evalTime:
selectedView === MessagingQueuesViewType.dropRate.value
? 2363404
: undefined,
};
return (
<div className="mq-overview-container">
{selectedView === MessagingQueuesViewType.producerLatency.value ? (
<PartitionLatencyTabs option={option} setOption={setOption} />
) : (
<div className="mq-overview-title">
{MessagingQueuesViewType[selectedView as SelectedViewType].label}
</div>
)}
<MessagingQueuesTable
selectedView={selectedView}
tableApiPayload={tableApiPayload}
tableApi={getTableApi(selectedView)}
validConfigPresent
type="Overview"
/>
</div>
);
}
export default MessagingQueueOverview;

View File

@ -0,0 +1,270 @@
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable jsx-a11y/click-events-have-key-events */
import './MessagingQueueHealthCheck.styles.scss';
import { CaretDownOutlined, LoadingOutlined } from '@ant-design/icons';
import {
Modal,
Select,
Spin,
Tooltip,
Tree,
TreeDataNode,
Typography,
} from 'antd';
import { OnboardingStatusResponse } from 'api/messagingQueues/onboarding/getOnboardingStatus';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import { History } from 'history';
import { Bolt, Check, OctagonAlert, X } from 'lucide-react';
import { ReactNode, useEffect, useState } from 'react';
import { useHistory } from 'react-router-dom';
import { isCloudUser } from 'utils/app';
import { v4 as uuid } from 'uuid';
import {
KAFKA_SETUP_DOC_LINK,
MessagingQueueHealthCheckService,
} from '../MessagingQueuesUtils';
interface AttributeCheckListProps {
visible: boolean;
onClose: () => void;
onboardingStatusResponses: {
title: string;
data: OnboardingStatusResponse['data'];
errorMsg?: string;
}[];
loading: boolean;
}
export enum AttributesFilters {
ALL = 'all',
SUCCESS = 'success',
ERROR = 'error',
}
function ErrorTitleAndKey({
title,
parentTitle,
history,
isCloudUserVal,
errorMsg,
isLeaf,
}: {
title: string;
parentTitle: string;
isCloudUserVal: boolean;
history: History<unknown>;
errorMsg?: string;
isLeaf?: boolean;
}): TreeDataNode {
const handleRedirection = (): void => {
let link = '';
switch (parentTitle) {
case 'Consumers':
link = `${ROUTES.GET_STARTED_APPLICATION_MONITORING}?${QueryParams.getStartedSource}=kafka&${QueryParams.getStartedSourceService}=${MessagingQueueHealthCheckService.Consumers}`;
break;
case 'Producers':
link = `${ROUTES.GET_STARTED_APPLICATION_MONITORING}?${QueryParams.getStartedSource}=kafka&${QueryParams.getStartedSourceService}=${MessagingQueueHealthCheckService.Producers}`;
break;
case 'Kafka':
link = `${ROUTES.GET_STARTED_INFRASTRUCTURE_MONITORING}?${QueryParams.getStartedSource}=kafka&${QueryParams.getStartedSourceService}=${MessagingQueueHealthCheckService.Kafka}`;
break;
default:
link = '';
}
if (isCloudUserVal && !!link) {
history.push(link);
} else {
window.open(KAFKA_SETUP_DOC_LINK, '_blank');
}
};
return {
key: `${title}-key-${uuid()}`,
title: (
<div className="attribute-error-title">
<Typography.Text className="tree-text" ellipsis={{ tooltip: title }}>
{title}
</Typography.Text>
<Tooltip title={errorMsg}>
<div
className="attribute-error-warning"
onClick={(e): void => {
e.preventDefault();
handleRedirection();
}}
>
<OctagonAlert size={14} />
Fix
</div>
</Tooltip>
</div>
),
isLeaf,
};
}
function AttributeLabels({ title }: { title: ReactNode }): JSX.Element {
return (
<div className="attribute-label">
<Bolt size={14} />
{title}
</div>
);
}
function treeTitleAndKey({
title,
isLeaf,
}: {
title: string;
isLeaf?: boolean;
}): TreeDataNode {
return {
key: `${title}-key-${uuid()}`,
title: (
<div className="attribute-success-title">
<Typography.Text className="tree-text" ellipsis={{ tooltip: title }}>
{title}
</Typography.Text>
{isLeaf && (
<div className="success-attribute-icon">
<Tooltip title="Success">
<Check size={14} />
</Tooltip>
</div>
)}
</div>
),
isLeaf,
};
}
function generateTreeDataNodes(
response: OnboardingStatusResponse['data'],
parentTitle: string,
isCloudUserVal: boolean,
history: History<unknown>,
): TreeDataNode[] {
return response
.map((item) => {
if (item.attribute) {
if (item.status === '1') {
return treeTitleAndKey({ title: item.attribute, isLeaf: true });
}
if (item.status === '0') {
return ErrorTitleAndKey({
title: item.attribute,
errorMsg: item.error_message || '',
parentTitle,
history,
isCloudUserVal,
});
}
}
return null;
})
.filter(Boolean) as TreeDataNode[];
}
function AttributeCheckList({
visible,
onClose,
onboardingStatusResponses,
loading,
}: AttributeCheckListProps): JSX.Element {
const [filter, setFilter] = useState<AttributesFilters>(AttributesFilters.ALL);
const [treeData, setTreeData] = useState<TreeDataNode[]>([]);
const handleFilterChange = (value: AttributesFilters): void => {
setFilter(value);
};
const isCloudUserVal = isCloudUser();
const history = useHistory();
useEffect(() => {
const filteredData = onboardingStatusResponses.map((response) => {
if (response.errorMsg) {
return ErrorTitleAndKey({
title: response.title,
errorMsg: response.errorMsg,
isLeaf: true,
parentTitle: response.title,
history,
isCloudUserVal,
});
}
let filteredData = response.data;
if (filter === AttributesFilters.SUCCESS) {
filteredData = response.data.filter((item) => item.status === '1');
} else if (filter === AttributesFilters.ERROR) {
filteredData = response.data.filter((item) => item.status === '0');
}
return {
...treeTitleAndKey({ title: response.title }),
children: generateTreeDataNodes(
filteredData,
response.title,
isCloudUserVal,
history,
),
};
});
setTreeData(filteredData);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [filter, onboardingStatusResponses]);
return (
<Modal
title="Kafka Service Attributes"
open={visible}
onCancel={onClose}
footer={false}
className="mq-health-check-modal"
closeIcon={<X size={14} />}
>
{loading ? (
<div className="loader-container">
<Spin indicator={<LoadingOutlined spin />} size="large" />
</div>
) : (
<div className="modal-content">
<Select
defaultValue={AttributesFilters.ALL}
className="attribute-select"
onChange={handleFilterChange}
options={[
{
value: AttributesFilters.ALL,
label: AttributeLabels({ title: 'Attributes: All' }),
},
{
value: AttributesFilters.SUCCESS,
label: AttributeLabels({ title: 'Attributes: Success' }),
},
{
value: AttributesFilters.ERROR,
label: AttributeLabels({ title: 'Attributes: Error' }),
},
]}
/>
<Tree
showLine
switcherIcon={<CaretDownOutlined />}
treeData={treeData}
height={450}
className="attribute-tree"
/>
</div>
)}
</Modal>
);
}
export default AttributeCheckList;

View File

@ -0,0 +1,168 @@
.mq-health-check-modal {
.ant-modal-content {
border-radius: 4px;
border: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400);
box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2);
.ant-modal-close {
margin-top: 4px;
}
.ant-modal-header {
border-bottom: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400);
margin-bottom: 16px;
padding-bottom: 4px;
.ant-modal-title {
color: var(--bg-vanilla-100);
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 600;
line-height: 22px;
letter-spacing: 0.52px;
}
}
.modal-content {
display: flex;
flex-direction: column;
gap: 8px;
padding: 8px;
background: var(--bg-ink-300);
.attribute-select {
align-items: center;
display: flex;
gap: 8px;
width: 170px;
.ant-select-selector {
display: flex;
height: 28px !important;
padding: 8px;
align-items: center;
gap: 4px;
border-radius: 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
}
}
.attribute-tree {
padding: 8px;
}
.tree-text {
color: var(--bg-vanilla-400);
font-family: 'Geist Mono';
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px;
width: 328px;
}
.ant-tree {
.ant-tree-title {
cursor: default;
.attribute-error-title {
display: flex;
align-items: center;
color: var(--bg-amber-400);
height: 24px;
.tree-text {
color: var(--bg-amber-400);
}
.attribute-error-warning {
display: flex;
align-items: center;
gap: 6px;
font-family: 'Geist Mono';
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px;
cursor: pointer;
}
}
.attribute-success-title {
display: flex;
align-items: center;
height: 24px;
.success-attribute-icon {
width: 44px;
color: var(--bg-vanilla-400);
display: flex;
> svg {
margin-left: auto;
}
}
}
}
.ant-tree-treenode {
width: 100%;
.ant-tree-node-content-wrapper {
width: 100%;
max-width: 380px;
}
}
}
}
.loader-container {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
padding: 8px;
background: var(--bg-ink-300);
height: 156px;
}
}
}
.attribute-label {
display: flex;
align-items: center;
gap: 8px;
}
.config-btn {
display: flex;
align-items: center;
height: 28px;
border-radius: 2px;
border: none;
box-shadow: none;
background: var(--bg-slate-500);
&.missing-config-btn {
background: rgba(255, 205, 86, 0.1);
color: var(--bg-amber-400);
&:hover {
color: var(--bg-amber-300) !important;
}
}
.config-btn-content {
display: flex;
align-items: center;
margin-right: 8px;
border-right: 1px solid rgba(255, 215, 120, 0.1);
padding-right: 8px;
}
}

View File

@ -0,0 +1,133 @@
/* eslint-disable sonarjs/no-duplicate-string */
import './MessagingQueueHealthCheck.styles.scss';
import { Button } from 'antd';
import cx from 'classnames';
import { useOnboardingStatus } from 'hooks/messagingQueue / onboarding/useOnboardingStatus';
import { Bolt, FolderTree } from 'lucide-react';
import { useEffect, useMemo, useState } from 'react';
import { MessagingQueueHealthCheckService } from '../MessagingQueuesUtils';
import AttributeCheckList from './AttributeCheckList';
interface MessagingQueueHealthCheckProps {
serviceToInclude: string[];
}
function MessagingQueueHealthCheck({
serviceToInclude,
}: MessagingQueueHealthCheckProps): JSX.Element {
const [loading, setLoading] = useState(false);
const [checkListOpen, setCheckListOpen] = useState(false);
// Consumer Data
const {
data: consumerData,
error: consumerError,
isFetching: consumerLoading,
} = useOnboardingStatus(
{
enabled: !!serviceToInclude.filter(
(service) => service === MessagingQueueHealthCheckService.Consumers,
).length,
},
MessagingQueueHealthCheckService.Consumers,
);
// Producer Data
const {
data: producerData,
error: producerError,
isFetching: producerLoading,
} = useOnboardingStatus(
{
enabled: !!serviceToInclude.filter(
(service) => service === MessagingQueueHealthCheckService.Producers,
).length,
},
MessagingQueueHealthCheckService.Producers,
);
// Kafka Data
const {
data: kafkaData,
error: kafkaError,
isFetching: kafkaLoading,
} = useOnboardingStatus(
{
enabled: !!serviceToInclude.filter(
(service) => service === MessagingQueueHealthCheckService.Kafka,
).length,
},
MessagingQueueHealthCheckService.Kafka,
);
// combined loading and update state
useEffect(() => {
setLoading(consumerLoading || producerLoading || kafkaLoading);
}, [consumerLoading, producerLoading, kafkaLoading]);
const missingConfiguration = useMemo(() => {
const consumerMissing =
(serviceToInclude.includes(MessagingQueueHealthCheckService.Consumers) &&
consumerData?.payload?.data?.filter((item) => item.status === '0')
.length) ||
0;
const producerMissing =
(serviceToInclude.includes(MessagingQueueHealthCheckService.Producers) &&
producerData?.payload?.data?.filter((item) => item.status === '0')
.length) ||
0;
const kafkaMissing =
(serviceToInclude.includes(MessagingQueueHealthCheckService.Kafka) &&
kafkaData?.payload?.data?.filter((item) => item.status === '0').length) ||
0;
return consumerMissing + producerMissing + kafkaMissing;
}, [consumerData, producerData, kafkaData, serviceToInclude]);
return (
<div>
<Button
onClick={(): void => setCheckListOpen(true)}
loading={loading}
className={cx(
'config-btn',
missingConfiguration ? 'missing-config-btn' : '',
)}
icon={<Bolt size={12} />}
>
<div className="config-btn-content">
{missingConfiguration
? `Missing Configuration (${missingConfiguration})`
: 'Configuration'}
</div>
<FolderTree size={14} />
</Button>
<AttributeCheckList
visible={checkListOpen}
onClose={(): void => setCheckListOpen(false)}
onboardingStatusResponses={[
{
title: 'Consumers',
data: consumerData?.payload?.data || [],
errorMsg: (consumerError || consumerData?.error) as string,
},
{
title: 'Producers',
data: producerData?.payload?.data || [],
errorMsg: (producerError || producerData?.error) as string,
},
{
title: 'Kafka',
data: kafkaData?.payload?.data || [],
errorMsg: (kafkaError || kafkaData?.error) as string,
},
].filter((item) => serviceToInclude.includes(item.title.toLowerCase()))}
loading={loading}
/>
</div>
);
}
export default MessagingQueueHealthCheck;

View File

@ -47,7 +47,7 @@
.header-config { .header-config {
display: flex; display: flex;
gap: 10px; gap: 12px;
align-items: center; align-items: center;
.messaging-queue-options { .messaging-queue-options {
@ -106,6 +106,8 @@
.mq-details-options { .mq-details-options {
letter-spacing: -0.06px; letter-spacing: -0.06px;
cursor: pointer;
.ant-radio-button-wrapper { .ant-radio-button-wrapper {
border-color: var(--bg-slate-400); border-color: var(--bg-slate-400);
color: var(--bg-vanilla-400); color: var(--bg-vanilla-400);
@ -219,6 +221,23 @@
} }
} }
} }
&.summary-section {
.overview-info-card {
min-height: 144px;
.card-title {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 500;
line-height: 22px;
letter-spacing: 0.52px;
text-transform: uppercase;
}
}
}
} }
.summary-section { .summary-section {

View File

@ -1,47 +1,38 @@
/* eslint-disable sonarjs/no-duplicate-string */
import './MessagingQueues.styles.scss'; import './MessagingQueues.styles.scss';
import { ExclamationCircleFilled } from '@ant-design/icons'; import { Button } from 'antd';
import { Color } from '@signozhq/design-tokens';
import { Button, Modal } from 'antd';
import logEvent from 'api/common/logEvent'; import logEvent from 'api/common/logEvent';
import cx from 'classnames';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2'; import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import { Calendar, ListMinus } from 'lucide-react'; import { ListMinus } from 'lucide-react';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { isCloudUser } from 'utils/app'; import { isCloudUser } from 'utils/app';
import MessagingQueueHealthCheck from './MessagingQueueHealthCheck/MessagingQueueHealthCheck';
import { import {
KAFKA_SETUP_DOC_LINK, KAFKA_SETUP_DOC_LINK,
MessagingQueueHealthCheckService,
MessagingQueuesViewType, MessagingQueuesViewType,
} from './MessagingQueuesUtils'; } from './MessagingQueuesUtils';
import { ComingSoon } from './MQCommon/MQCommon';
function MessagingQueues(): JSX.Element { function MessagingQueues(): JSX.Element {
const history = useHistory(); const history = useHistory();
const { t } = useTranslation('messagingQueuesKafkaOverview'); const { t } = useTranslation('messagingQueuesKafkaOverview');
const { confirm } = Modal; const redirectToDetailsPage = (callerView?: string): void => {
const showConfirm = (): void => {
logEvent('Messaging Queues: View details clicked', { logEvent('Messaging Queues: View details clicked', {
page: 'Messaging Queues Overview', page: 'Messaging Queues Overview',
source: 'Consumer Latency view', source: callerView,
}); });
confirm({ history.push(
icon: <ExclamationCircleFilled />, `${ROUTES.MESSAGING_QUEUES_DETAIL}?${QueryParams.mqServiceView}=${callerView}`,
content: t('confirmModal.content'), );
className: 'overview-confirm-modal',
onOk() {
logEvent('Messaging Queues: Proceed button clicked', {
page: 'Messaging Queues Overview',
});
history.push(ROUTES.MESSAGING_QUEUES_DETAIL);
},
okText: t('confirmModal.okText'),
});
}; };
const isCloudUserVal = isCloudUser(); const isCloudUserVal = isCloudUser();
@ -69,7 +60,16 @@ function MessagingQueues(): JSX.Element {
{t('breadcrumb')} {t('breadcrumb')}
</div> </div>
<div className="messaging-header"> <div className="messaging-header">
<div className="header-config">{t('header')}</div> <div className="header-config">
{t('header')} /
<MessagingQueueHealthCheck
serviceToInclude={[
MessagingQueueHealthCheckService.Consumers,
MessagingQueueHealthCheckService.Producers,
MessagingQueueHealthCheckService.Kafka,
]}
/>
</div>
<DateTimeSelectionV2 showAutoRefresh={false} hideShareModal /> <DateTimeSelectionV2 showAutoRefresh={false} hideShareModal />
</div> </div>
<div className="messaging-overview"> <div className="messaging-overview">
@ -86,7 +86,7 @@ function MessagingQueues(): JSX.Element {
type="default" type="default"
onClick={(): void => onClick={(): void =>
getStartedRedirect( getStartedRedirect(
ROUTES.GET_STARTED_APPLICATION_MONITORING, `${ROUTES.GET_STARTED_APPLICATION_MONITORING}?${QueryParams.getStartedSource}=kafka&${QueryParams.getStartedSourceService}=${MessagingQueueHealthCheckService.Consumers}`,
'Configure Consumer', 'Configure Consumer',
) )
} }
@ -105,7 +105,7 @@ function MessagingQueues(): JSX.Element {
type="default" type="default"
onClick={(): void => onClick={(): void =>
getStartedRedirect( getStartedRedirect(
ROUTES.GET_STARTED_APPLICATION_MONITORING, `${ROUTES.GET_STARTED_APPLICATION_MONITORING}?${QueryParams.getStartedSource}=kafka&${QueryParams.getStartedSourceService}=${MessagingQueueHealthCheckService.Producers}`,
'Configure Producer', 'Configure Producer',
) )
} }
@ -124,7 +124,7 @@ function MessagingQueues(): JSX.Element {
type="default" type="default"
onClick={(): void => onClick={(): void =>
getStartedRedirect( getStartedRedirect(
ROUTES.GET_STARTED_INFRASTRUCTURE_MONITORING, `${ROUTES.GET_STARTED_INFRASTRUCTURE_MONITORING}?${QueryParams.getStartedSource}=kafka&${QueryParams.getStartedSourceService}=${MessagingQueueHealthCheckService.Kafka}`,
'Monitor kafka', 'Monitor kafka',
) )
} }
@ -134,55 +134,80 @@ function MessagingQueues(): JSX.Element {
</div> </div>
</div> </div>
</div> </div>
<div className="summary-section">
<div className="summary-card"> <p className="overview-text">{t('overviewSummarySection.title')}</p>
<div className="summary-title"> <p className="overview-subtext">{t('overviewSummarySection.subtitle')}</p>
<p>{MessagingQueuesViewType.consumerLag.label}</p> <div className={cx('overview-doc-area', 'summary-section')}>
<div className="time-value"> <div className="overview-info-card">
<Calendar size={14} color={Color.BG_SLATE_200} /> <div>
<p className="time-value">1D</p> <p className="card-title">{t('summarySection.consumer.title')}</p>
<p className="card-info-text">
{t('summarySection.consumer.description')}
</p>
</div> </div>
</div> <div className="button-grp">
<div className="view-detail-btn"> <Button
<Button type="primary" onClick={showConfirm}> type="default"
onClick={(): void =>
redirectToDetailsPage(MessagingQueuesViewType.consumerLag.value)
}
>
{t('summarySection.viewDetailsButton')} {t('summarySection.viewDetailsButton')}
</Button> </Button>
</div> </div>
</div> </div>
<div className="summary-card coming-soon-card"> <div className="overview-info-card middle-card">
<div className="summary-title"> <div>
<p>{MessagingQueuesViewType.partitionLatency.label}</p> <p className="card-title">{t('summarySection.producer.title')}</p>
<div className="time-value"> <p className="card-info-text">
<Calendar size={14} color={Color.BG_SLATE_200} /> {t('summarySection.producer.description')}
<p className="time-value">1D</p> </p>
</div>
<div className="button-grp">
<Button
type="default"
onClick={(): void =>
redirectToDetailsPage(MessagingQueuesViewType.producerLatency.value)
}
>
{t('summarySection.viewDetailsButton')}
</Button>
</div> </div>
</div> </div>
<div className="view-detail-btn"> <div className="overview-info-card middle-card">
<ComingSoon /> <div>
<p className="card-title">{t('summarySection.partition.title')}</p>
<p className="card-info-text">
{t('summarySection.partition.description')}
</p>
</div>
<div className="button-grp">
<Button
type="default"
onClick={(): void =>
redirectToDetailsPage(MessagingQueuesViewType.partitionLatency.value)
}
>
{t('summarySection.viewDetailsButton')}
</Button>
</div> </div>
</div> </div>
<div className="summary-card coming-soon-card"> <div className="overview-info-card">
<div className="summary-title"> <div>
<p>{MessagingQueuesViewType.producerLatency.label}</p> <p className="card-title">{t('summarySection.dropRate.title')}</p>
<div className="time-value"> <p className="card-info-text">
<Calendar size={14} color={Color.BG_SLATE_200} /> {t('summarySection.dropRate.description')}
<p className="time-value">1D</p> </p>
</div> </div>
</div> <div className="button-grp">
<div className="view-detail-btn"> <Button
<ComingSoon /> type="default"
</div> onClick={(): void =>
</div> redirectToDetailsPage(MessagingQueuesViewType.dropRate.value)
<div className="summary-card coming-soon-card"> }
<div className="summary-title"> >
<p>{MessagingQueuesViewType.consumerLatency.label}</p> {t('summarySection.viewDetailsButton')}
<div className="time-value"> </Button>
<Calendar size={14} color={Color.BG_SLATE_200} />
<p className="time-value">1D</p>
</div>
</div>
<div className="view-detail-btn">
<ComingSoon />
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,14 +1,24 @@
import { OnboardingStatusResponse } from 'api/messagingQueues/onboarding/getOnboardingStatus';
import { QueryParams } from 'constants/query'; import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder'; import { PANEL_TYPES } from 'constants/queryBuilder';
import { GetWidgetQueryBuilderProps } from 'container/MetricsApplication/types'; import { GetWidgetQueryBuilderProps } from 'container/MetricsApplication/types';
import { History, Location } from 'history'; import { History, Location } from 'history';
import { isEmpty } from 'lodash-es'; import { isEmpty } from 'lodash-es';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse'; import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData'; import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard'; import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder'; import { DataSource } from 'types/common/queryBuilder';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import {
getConsumerLagDetails,
MessagingQueueServicePayload,
MessagingQueuesPayloadProps,
} from './MQDetails/MQTables/getConsumerLagDetails';
import { getPartitionLatencyDetails } from './MQDetails/MQTables/getPartitionLatencyDetails';
import { getTopicThroughputDetails } from './MQDetails/MQTables/getTopicThroughputDetails';
export const KAFKA_SETUP_DOC_LINK = export const KAFKA_SETUP_DOC_LINK =
'https://signoz.io/docs/messaging-queues/kafka?utm_source=product&utm_medium=kafka-get-started'; 'https://signoz.io/docs/messaging-queues/kafka?utm_source=product&utm_medium=kafka-get-started';
@ -24,14 +34,17 @@ export type RowData = {
[key: string]: string | number; [key: string]: string | number;
}; };
export enum ConsumerLagDetailType { export enum MessagingQueueServiceDetailType {
ConsumerDetails = 'consumer-details', ConsumerDetails = 'consumer-details',
ProducerDetails = 'producer-details', ProducerDetails = 'producer-details',
NetworkLatency = 'network-latency', NetworkLatency = 'network-latency',
PartitionHostMetrics = 'partition-host-metric', PartitionHostMetrics = 'partition-host-metric',
} }
export const ConsumerLagDetailTitle: Record<ConsumerLagDetailType, string> = { export const ConsumerLagDetailTitle: Record<
MessagingQueueServiceDetailType,
string
> = {
'consumer-details': 'Consumer Groups Details', 'consumer-details': 'Consumer Groups Details',
'producer-details': 'Producer Details', 'producer-details': 'Producer Details',
'network-latency': 'Network Latency', 'network-latency': 'Network Latency',
@ -205,21 +218,172 @@ export function setSelectedTimelineQuery(
history.replace(generatedUrl); history.replace(generatedUrl);
} }
export enum MessagingQueuesViewTypeOptions {
ConsumerLag = 'consumerLag',
PartitionLatency = 'partitionLatency',
ProducerLatency = 'producerLatency',
ConsumerLatency = 'consumerLatency',
}
export const MessagingQueuesViewType = { export const MessagingQueuesViewType = {
consumerLag: { consumerLag: {
label: 'Consumer Lag view', label: 'Consumer Lag view',
value: 'consumerLag', value: MessagingQueuesViewTypeOptions.ConsumerLag,
}, },
partitionLatency: { partitionLatency: {
label: 'Partition Latency view', label: 'Partition Latency view',
value: 'partitionLatency', value: MessagingQueuesViewTypeOptions.PartitionLatency,
}, },
producerLatency: { producerLatency: {
label: 'Producer Latency view', label: 'Producer Latency view',
value: 'producerLatency', value: MessagingQueuesViewTypeOptions.ProducerLatency,
}, },
consumerLatency: { dropRate: {
label: 'Consumer latency view', label: 'Drop Rate view',
value: 'consumerLatency', value: 'dropRate',
}, },
}; };
export function setConfigDetail(
urlQuery: URLSearchParams,
location: Location<unknown>,
history: History<unknown>,
paramsToSet?: {
[key: string]: string;
},
): void {
// remove "key" and its value from the paramsToSet object
const { key, ...restParamsToSet } = paramsToSet || {};
if (!isEmpty(restParamsToSet)) {
const configDetail = {
...restParamsToSet,
};
urlQuery.set(
QueryParams.configDetail,
encodeURIComponent(JSON.stringify(configDetail)),
);
} else {
urlQuery.delete(QueryParams.configDetail);
}
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
history.replace(generatedUrl);
}
export enum ProducerLatencyOptions {
Producers = 'Producers',
Consumers = 'Consumers',
}
interface MetaDataAndAPI {
tableApiPayload: MessagingQueueServicePayload;
tableApi: (
props: MessagingQueueServicePayload,
) => Promise<
SuccessResponse<MessagingQueuesPayloadProps['payload']> | ErrorResponse
>;
}
interface MetaDataAndAPIPerView {
detailType: MessagingQueueServiceDetailType;
selectedTimelineQuery: SelectedTimelineQuery;
configDetails?: {
[key: string]: string;
};
minTime: number;
maxTime: number;
}
export const getMetaDataAndAPIPerView = (
metaDataProps: MetaDataAndAPIPerView,
): Record<string, MetaDataAndAPI> => {
const {
detailType,
minTime,
maxTime,
selectedTimelineQuery,
configDetails,
} = metaDataProps;
return {
[MessagingQueuesViewType.consumerLag.value]: {
tableApiPayload: {
start: (selectedTimelineQuery?.start || 0) * 1e9,
end: (selectedTimelineQuery?.end || 0) * 1e9,
variables: {
partition: selectedTimelineQuery?.partition,
topic: selectedTimelineQuery?.topic,
consumer_group: selectedTimelineQuery?.group,
},
detailType,
},
tableApi: getConsumerLagDetails,
},
[MessagingQueuesViewType.partitionLatency.value]: {
tableApiPayload: {
start: minTime,
end: maxTime,
variables: {
partition: configDetails?.partition,
topic: configDetails?.topic,
consumer_group: configDetails?.group,
},
detailType,
},
tableApi: getPartitionLatencyDetails,
},
[MessagingQueuesViewType.producerLatency.value]: {
tableApiPayload: {
start: minTime,
end: maxTime,
variables: {
partition: configDetails?.partition,
topic: configDetails?.topic,
service_name: configDetails?.service_name,
},
detailType,
},
tableApi: getTopicThroughputDetails,
},
};
};
interface OnboardingStatusAttributeData {
overallStatus: string;
allAvailableAttributes: string[];
attributeDataWithError: { attributeName: string; errorMsg: string }[];
}
export const getAttributeDataFromOnboardingStatus = (
onboardingStatus?: OnboardingStatusResponse | null,
): OnboardingStatusAttributeData => {
const allAvailableAttributes: string[] = [];
const attributeDataWithError: {
attributeName: string;
errorMsg: string;
}[] = [];
if (onboardingStatus?.data && !isEmpty(onboardingStatus?.data)) {
onboardingStatus.data.forEach((status) => {
if (status.attribute) {
allAvailableAttributes.push(status.attribute);
if (status.status === '0') {
attributeDataWithError.push({
attributeName: status.attribute,
errorMsg: status.error_message || '',
});
}
}
});
}
return {
overallStatus: attributeDataWithError.length ? 'error' : 'success',
allAvailableAttributes,
attributeDataWithError,
};
};
export enum MessagingQueueHealthCheckService {
Consumers = 'consumers',
Producers = 'producers',
Kafka = 'kafka',
}

View File

@ -0,0 +1,11 @@
import OnboardingQuestionaire from 'container/OnboardingQuestionaire';
function OrgOnboarding(): JSX.Element {
return (
<div className="onboarding-v2">
<OnboardingQuestionaire />
</div>
);
}
export default OrgOnboarding;

View File

@ -0,0 +1,3 @@
import OnboardingPage from './OrgOnboarding';
export default OnboardingPage;

View File

@ -8,10 +8,12 @@ import {
UPDATE_CURRENT_ERROR, UPDATE_CURRENT_ERROR,
UPDATE_CURRENT_VERSION, UPDATE_CURRENT_VERSION,
UPDATE_FEATURE_FLAG_RESPONSE, UPDATE_FEATURE_FLAG_RESPONSE,
UPDATE_IS_FETCHING_ORG_PREFERENCES,
UPDATE_LATEST_VERSION, UPDATE_LATEST_VERSION,
UPDATE_LATEST_VERSION_ERROR, UPDATE_LATEST_VERSION_ERROR,
UPDATE_ORG, UPDATE_ORG,
UPDATE_ORG_NAME, UPDATE_ORG_NAME,
UPDATE_ORG_PREFERENCES,
UPDATE_USER, UPDATE_USER,
UPDATE_USER_ACCESS_REFRESH_ACCESS_TOKEN, UPDATE_USER_ACCESS_REFRESH_ACCESS_TOKEN,
UPDATE_USER_FLAG, UPDATE_USER_FLAG,
@ -59,6 +61,8 @@ const InitialValue: InitialValueTypes = {
userFlags: {}, userFlags: {},
ee: 'Y', ee: 'Y',
setupCompleted: true, setupCompleted: true,
orgPreferences: null,
isFetchingOrgPreferences: true,
}; };
const appReducer = ( const appReducer = (
@ -73,6 +77,17 @@ const appReducer = (
}; };
} }
case UPDATE_ORG_PREFERENCES: {
return { ...state, orgPreferences: action.payload.orgPreferences };
}
case UPDATE_IS_FETCHING_ORG_PREFERENCES: {
return {
...state,
isFetchingOrgPreferences: action.payload.isFetchingOrgPreferences,
};
}
case UPDATE_FEATURE_FLAG_RESPONSE: { case UPDATE_FEATURE_FLAG_RESPONSE: {
return { return {
...state, ...state,

View File

@ -299,3 +299,16 @@ notifications - 2050
font-weight: 300 700; font-weight: 300 700;
font-style: normal; font-style: normal;
} }
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.animate-spin {
animation: spin 1s linear infinite;
}

View File

@ -25,7 +25,10 @@ export const UPDATE_ORG_NAME = 'UPDATE_ORG_NAME';
export const UPDATE_ORG = 'UPDATE_ORG'; export const UPDATE_ORG = 'UPDATE_ORG';
export const UPDATE_CONFIGS = 'UPDATE_CONFIGS'; export const UPDATE_CONFIGS = 'UPDATE_CONFIGS';
export const UPDATE_USER_FLAG = 'UPDATE_USER_FLAG'; export const UPDATE_USER_FLAG = 'UPDATE_USER_FLAG';
export const UPDATE_ORG_PREFERENCES = 'UPDATE_ORG_PREFERENCES';
export const UPDATE_FEATURE_FLAG_RESPONSE = 'UPDATE_FEATURE_FLAG_RESPONSE'; export const UPDATE_FEATURE_FLAG_RESPONSE = 'UPDATE_FEATURE_FLAG_RESPONSE';
export const UPDATE_IS_FETCHING_ORG_PREFERENCES =
'UPDATE_IS_FETCHING_ORG_PREFERENCES';
export interface LoggedInUser { export interface LoggedInUser {
type: typeof LOGGED_IN; type: typeof LOGGED_IN;
@ -130,6 +133,20 @@ export interface UpdateFeatureFlag {
}; };
} }
export interface UpdateOrgPreferences {
type: typeof UPDATE_ORG_PREFERENCES;
payload: {
orgPreferences: AppReducer['orgPreferences'];
};
}
export interface UpdateIsFetchingOrgPreferences {
type: typeof UPDATE_IS_FETCHING_ORG_PREFERENCES;
payload: {
isFetchingOrgPreferences: AppReducer['isFetchingOrgPreferences'];
};
}
export type AppAction = export type AppAction =
| LoggedInUser | LoggedInUser
| UpdateAppVersion | UpdateAppVersion
@ -143,4 +160,6 @@ export type AppAction =
| UpdateOrg | UpdateOrg
| UpdateConfigs | UpdateConfigs
| UpdateUserFlag | UpdateUserFlag
| UpdateFeatureFlag; | UpdateFeatureFlag
| UpdateOrgPreferences
| UpdateIsFetchingOrgPreferences;

View File

@ -9,6 +9,7 @@ export interface ILog {
severityNumber: number; severityNumber: number;
body: string; body: string;
resources_string: Record<string, never>; resources_string: Record<string, never>;
scope_string: Record<string, never>;
attributesString: Record<string, never>; attributesString: Record<string, never>;
attributes_string: Record<string, never>; attributes_string: Record<string, never>;
attributesInt: Record<string, never>; attributesInt: Record<string, never>;
@ -22,6 +23,7 @@ type OmitAttributesResources = Pick<
Exclude< Exclude<
keyof ILog, keyof ILog,
| 'resources_string' | 'resources_string'
| 'scope_string'
| 'attributesString' | 'attributesString'
| 'attributes_string' | 'attributes_string'
| 'attributesInt' | 'attributesInt'
@ -32,4 +34,5 @@ type OmitAttributesResources = Pick<
export type ILogAggregateAttributesResources = OmitAttributesResources & { export type ILogAggregateAttributesResources = OmitAttributesResources & {
attributes: Record<string, never>; attributes: Record<string, never>;
resources: Record<string, never>; resources: Record<string, never>;
scope: Record<string, never>;
}; };

View File

@ -0,0 +1,10 @@
export interface UpdateProfileProps {
reasons_for_interest_in_signoz: string;
familiarity_with_observability: string;
has_existing_observability_tool: boolean;
existing_observability_tool: string;
logs_scale_per_day_in_gb: number;
number_of_services: number;
number_of_hosts: number;
where_did_you_hear_about_signoz: string;
}

View File

@ -1,3 +1,5 @@
import { OrgPreference } from 'types/reducer/app';
export interface GetOrgPreferenceResponseProps { export interface GetOrgPreferenceResponseProps {
status: string; status: string;
data: Record<string, unknown>; data: Record<string, unknown>;
@ -10,7 +12,7 @@ export interface GetUserPreferenceResponseProps {
export interface GetAllOrgPreferencesResponseProps { export interface GetAllOrgPreferencesResponseProps {
status: string; status: string;
data: Record<string, unknown>; data: OrgPreference[];
} }
export interface GetAllUserPreferencesResponseProps { export interface GetAllUserPreferencesResponseProps {
@ -19,12 +21,12 @@ export interface GetAllUserPreferencesResponseProps {
} }
export interface UpdateOrgPreferenceProps { export interface UpdateOrgPreferenceProps {
key: string; preferenceID: string;
value: unknown; value: unknown;
} }
export interface UpdateUserPreferenceProps { export interface UpdateUserPreferenceProps {
key: string; preferenceID: string;
value: unknown; value: unknown;
} }

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