Compare commits

...

27 Commits

Author SHA1 Message Date
Yunus M
ee7f6a1aec feat: update background for tr, td on hover 2024-07-25 18:51:45 +05:30
Yunus M
7c97ea83de feat: add a min max limit for log body width in column view 2024-07-25 15:57:11 +05:30
Vikrant Gupta
d60daef171 fix: handle the super set query reset state when changing widgets (#5539) 2024-07-23 20:21:25 +05:30
Vikrant Gupta
d50530f58c fix: retain the step interval while creating alerts from the dashboard panel (#5455)
* fix: use the same step interval as in the dashboard query while creating alerts from panel

* chore: added extra safety checks

* chore: add test cases for the mapQueryDataFromAPI utils

* chore: added functions test cases as well

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2024-07-23 17:20:31 +05:30
Yunus M
6957bd71ca chore: move from trackEvent to logEvent (#5530)
* chore: move from trackEvent to logEvent

* feat: update test cases
2024-07-23 16:32:45 +05:30
Vikrant Gupta
ef8b50c19e chore: addition of jest test cases for dashboards panels (#5506)
* chore: added value panel wrapper jest tests

* chore: added column units and legends test for table panel wrapper
2024-07-22 21:10:48 +05:30
Nityananda Gohain
1585065fff fix: use proper indexes for full text search (#4787)
* fix: use proper indexes for full text search

* fix: tests updated

* feat: lower support only for body and not attributes

* fix: remove default tolower

* fix: add comment for json key split

* fix: remove ilike only for body searches

* fix: minor fixes

* fix: minor fixes
2024-07-22 17:46:35 +05:30
Yunus M
99c68ddbcd feat: add learn more urls to ingestion settings page (#5526)
* feat: add learn more urls to ingestion settings page

* feat: enable multi ingestion settins for editors, add basic test cases
2024-07-22 16:29:00 +05:30
Vikrant Gupta
b08e859426 fix: do not add select columns when the datasource is logs (#5515)
* fix: do not add select columns when the datasource is logs

* chore: added data test id
2024-07-22 13:43:47 +05:30
SagarRajput-7
89fd3e4f55 chore: added trace filter test cases (#5451)
* feat: added trace filter test cases

* feat: added trace filter test cases - initial render

* feat: added test cases - query sync, filter section behaviour etc

* feat: deleted mock-data files

* feat: added test cases of undefined filters and items

* feat: deleted tsconfig

* feat: added clear and rest btn test cases for traces filters

* feat: added collapse and uncollapse test for traces filters
2024-07-22 11:05:20 +05:30
Vibhu Pandey
a2492b0135 ci(github): change to beta (#5524)
* ci(github): change to beta

* Update testing-deployment.yaml

* ci(staging): bump to beta
2024-07-19 11:59:40 +05:30
Nityananda Gohain
eb8ca5a7ca fix: ignore offset if timestamp is selected in order by (#5520)
* fix: ignore offset if timestamp is selected in order by

* fix: tests updated
2024-07-18 18:03:39 +05:30
Pranay Prateek
80133240ca fix: update community link (#5516)
* update community link

* Update copyright year
2024-07-18 17:25:32 +05:30
Vikrant Gupta
7d7d112f40 fix: the dashboard locked bar should be sticky at the bottom (#5512) 2024-07-18 13:56:18 +05:30
SagarRajput-7
add2d19614 fix: fixed logEvent breaking page due to lack of null checks (#5511)
* fix: fixed logEvent breaking page due to lack of null checks

* fix: fixed logEvent breaking page due to lack of null checks
2024-07-18 13:54:05 +05:30
Vikrant Gupta
adfe20e88a fix: url params should not propagate across pages (#5417)
* fix: dashboards list url query params isolation

* feat: order query param old logs explorer isolation

* feat: added extra checks in place

* fix: refactor the dashboards list page for better performance

* chore: add test cases for the dashboards list page

* fix: added test cases for dashboards list page

* fix: added code comments

* fix: added empty state for dashboards and no search state
2024-07-18 12:25:31 +05:30
Vishal Sharma
d3b83f5a41 chore: update heartbeat interval logic (#5507)
* chore: update heartbeat interval logic

* chore: address review comment
2024-07-17 17:05:15 +05:30
B Kevin Anderson
77eba9a558 fix: list panel not querying selected columns (#5452)
Added log and traces columns to query for list panels
  Closes #5064

Co-authored-by: Vikrant Gupta <vikrant.thomso@gmail.com>
2024-07-17 12:19:00 +05:30
Vikrant Gupta
43e73e06fe chore: helpers required for dashboards e2e test cases (#5496)
* chore: helpers required for dashboards e2e test cases

* chore: helpers required for dashboards e2e test cases

* chore: helpers required for dashboards e2e test cases
2024-07-16 18:35:59 +05:30
Vikrant Gupta
840d8b2e49 fix: 404 not found when intercepting the ingestion key calls (#5490) 2024-07-16 14:30:03 +05:30
Vishal Sharma
df751c7f38 chore: add activation events (#5474) 2024-07-16 14:18:59 +05:30
Shaheer Kochai
cd07c743b6 Implement OverlayScrollbars throughout the app for MacOS-like scrolling experience (#5423)
* feat: build overlay scrollbar component for Virtuoso elements

* feat: apply overlay scroll to Virtuoso components

* feat: build overlay scrollbar component for normal scrollable sections

* feat: apply overlay scrollbar to normal scrollable sections

* feat: add dark mode UI support to overlay scrollbars

* chore: rename OverlayScrollbar to OverlayScrollbarForTypicalChildren

* chore: move inline style to scss file

* chore: rename VirtuosoOverlayScrollbar to OverlayScrollbarForVirtuosoChildren

* chore: move OverlayScrollbarForTypicalChildren to components folder

* chore: create a common component for handling Virtuoso and Typical scroll sections

* chore: rename Virtuoso and Typical Overlay Scrollbar components

* fix: fix the overlay scrollbar initialization flickering

* fix: remove calculated height from typical overlay scrollbar + remove the explicit height: 100%
2024-07-16 14:16:13 +05:30
Shaheer Kochai
46e6c34e51 fix: block alert creation if query_range API fails (#5441) 2024-07-16 14:13:25 +05:30
Yunus M
42f7905b3b feat: show status message, status code string, span kind in trace det… (#5428)
* feat: show status message, status code string, span kind in trace details

* chore: update tests

* chore: update snapshots
2024-07-16 11:00:29 +05:30
Vikrant Gupta
a6e68c6519 fix: issue with table sorting when column contains both string and numbers (#5458) 2024-07-15 21:15:37 +05:30
Vikrant Gupta
c7e3e6dc4e fix: retain legends while changing panel types (#5447) 2024-07-15 21:04:49 +05:30
Srikanth Chekuri
9194ab08b6 fix: incorrect response for promql value type panels (#5497) 2024-07-15 18:06:39 +05:30
145 changed files with 5793 additions and 591 deletions

View File

@@ -30,6 +30,7 @@ jobs:
GCP_PROJECT: ${{ secrets.GCP_PROJECT }}
GCP_ZONE: ${{ secrets.GCP_ZONE }}
GCP_INSTANCE: ${{ secrets.GCP_INSTANCE }}
CLOUDSDK_CORE_DISABLE_PROMPTS: 1
run: |
read -r -d '' COMMAND <<EOF || true
echo "GITHUB_BRANCH: ${GITHUB_BRANCH}"
@@ -51,4 +52,4 @@ jobs:
make build-frontend-amd64
make run-testing
EOF
gcloud compute ssh ${GCP_INSTANCE} --zone ${GCP_ZONE} --ssh-key-expire-after=15m --tunnel-through-iap --project ${GCP_PROJECT} --command "${COMMAND}"
gcloud beta compute ssh ${GCP_INSTANCE} --zone ${GCP_ZONE} --ssh-key-expire-after=15m --tunnel-through-iap --project ${GCP_PROJECT} --command "${COMMAND}"

View File

@@ -30,6 +30,7 @@ jobs:
GCP_PROJECT: ${{ secrets.GCP_PROJECT }}
GCP_ZONE: ${{ secrets.GCP_ZONE }}
GCP_INSTANCE: ${{ secrets.GCP_INSTANCE }}
CLOUDSDK_CORE_DISABLE_PROMPTS: 1
run: |
read -r -d '' COMMAND <<EOF || true
echo "GITHUB_BRANCH: ${GITHUB_BRANCH}"
@@ -52,4 +53,4 @@ jobs:
make build-frontend-amd64
make run-testing
EOF
gcloud compute ssh ${GCP_INSTANCE} --zone ${GCP_ZONE} --ssh-key-expire-after=15m --tunnel-through-iap --project ${GCP_PROJECT} --command "${COMMAND}"
gcloud beta compute ssh ${GCP_INSTANCE} --zone ${GCP_ZONE} --ssh-key-expire-after=15m --tunnel-through-iap --project ${GCP_PROJECT} --command "${COMMAND}"

View File

@@ -110,6 +110,8 @@
"react-syntax-highlighter": "15.5.0",
"react-use": "^17.3.2",
"react-virtuoso": "4.0.3",
"overlayscrollbars-react": "^0.5.6",
"overlayscrollbars": "^2.8.1",
"redux": "^4.0.5",
"redux-thunk": "^2.3.0",
"rehype-raw": "7.0.0",

View File

@@ -1,6 +1,7 @@
import { ConfigProvider } from 'antd';
import getLocalStorageApi from 'api/browser/localstorage/get';
import setLocalStorageApi from 'api/browser/localstorage/set';
import logEvent from 'api/common/logEvent';
import NotFound from 'components/NotFound';
import Spinner from 'components/Spinner';
import { FeatureKeys } from 'constants/features';
@@ -48,7 +49,7 @@ function App(): JSX.Element {
const dispatch = useDispatch<Dispatch<AppActions>>();
const { trackPageView, trackEvent } = useAnalytics();
const { trackPageView } = useAnalytics();
const { hostname, pathname } = window.location;
@@ -199,7 +200,7 @@ function App(): JSX.Element {
LOCALSTORAGE.THEME_ANALYTICS_V1,
);
if (!isThemeAnalyticsSent) {
trackEvent('Theme Analytics', {
logEvent('Theme Analytics', {
theme: isDarkMode ? THEME_MODE.DARK : THEME_MODE.LIGHT,
user: pick(user, ['email', 'userId', 'name']),
org,

View File

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

View File

@@ -0,0 +1,54 @@
import TypicalOverlayScrollbar from 'components/TypicalOverlayScrollbar/TypicalOverlayScrollbar';
import VirtuosoOverlayScrollbar from 'components/VirtuosoOverlayScrollbar/VirtuosoOverlayScrollbar';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { PartialOptions } from 'overlayscrollbars';
import { CSSProperties, ReactElement, useMemo } from 'react';
type Props = {
children: ReactElement;
isVirtuoso?: boolean;
style?: CSSProperties;
options?: PartialOptions;
};
function OverlayScrollbar({
children,
isVirtuoso,
style,
options: customOptions,
}: Props): any {
const isDarkMode = useIsDarkMode();
const options = useMemo(
() =>
({
scrollbars: {
autoHide: 'scroll',
theme: isDarkMode ? 'os-theme-light' : 'os-theme-dark',
},
...(customOptions || {}),
} as PartialOptions),
[customOptions, isDarkMode],
);
if (isVirtuoso) {
return (
<VirtuosoOverlayScrollbar style={style} options={options}>
{children}
</VirtuosoOverlayScrollbar>
);
}
return (
<TypicalOverlayScrollbar style={style} options={options}>
{children}
</TypicalOverlayScrollbar>
);
}
OverlayScrollbar.defaultProps = {
isVirtuoso: false,
style: {},
options: {},
};
export default OverlayScrollbar;

View File

@@ -5,7 +5,6 @@ import { Button } from 'antd';
import { Tag } from 'antd/lib';
import Input from 'components/Input';
import { Check, X } from 'lucide-react';
import { TweenOneGroup } from 'rc-tween-one';
import React, { Dispatch, SetStateAction, useState } from 'react';
function Tags({ tags, setTags }: AddTagsProps): JSX.Element {
@@ -46,41 +45,19 @@ function Tags({ tags, setTags }: AddTagsProps): JSX.Element {
func(value);
};
const forMap = (tag: string): React.ReactElement => (
<span key={tag} style={{ display: 'inline-block' }}>
<Tag
closable
onClose={(e): void => {
e.preventDefault();
handleClose(tag);
}}
>
{tag}
</Tag>
</span>
);
const tagChild = tags.map(forMap);
const renderTagsAnimated = (): React.ReactElement => (
<TweenOneGroup
appear={false}
className="tags"
enter={{ scale: 0.8, opacity: 0, type: 'from', duration: 100 }}
leave={{ opacity: 0, width: 0, scale: 0, duration: 200 }}
onEnd={(e): void => {
if (e.type === 'appear' || e.type === 'enter') {
(e.target as any).style = 'display: inline-block';
}
}}
>
{tagChild}
</TweenOneGroup>
);
return (
<div className="tags-container">
{renderTagsAnimated()}
{tags.map<React.ReactNode>((tag) => (
<Tag
key={tag}
closable
style={{ userSelect: 'none' }}
onClose={(): void => handleClose(tag)}
>
<span>{tag}</span>
</Tag>
))}
{inputVisible && (
<div className="add-tag-container">
<Input

View File

@@ -0,0 +1,31 @@
import './typicalOverlayScrollbar.scss';
import { PartialOptions } from 'overlayscrollbars';
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
import { CSSProperties, ReactElement } from 'react';
interface Props {
children: ReactElement;
style?: CSSProperties;
options?: PartialOptions;
}
export default function TypicalOverlayScrollbar({
children,
style,
options,
}: Props): ReturnType<typeof OverlayScrollbarsComponent> {
return (
<OverlayScrollbarsComponent
defer
options={options}
style={style}
className="overlay-scrollbar"
data-overlayscrollbars-initialize
>
{children}
</OverlayScrollbarsComponent>
);
}
TypicalOverlayScrollbar.defaultProps = { style: {}, options: {} };

View File

@@ -0,0 +1,3 @@
.overlay-scrollbar {
height: 100%;
}

View File

@@ -49,7 +49,10 @@ function ValueGraph({
}
>
<Tooltip title={t('this_value_satisfies_multiple_thresholds')}>
<ExclamationCircleFilled className="value-graph-icon" />
<ExclamationCircleFilled
className="value-graph-icon"
data-testid="conflicting-thresholds"
/>
</Tooltip>
</div>
)}

View File

@@ -0,0 +1,37 @@
import './virtuosoOverlayScrollbar.scss';
import useInitializeOverlayScrollbar from 'hooks/useInitializeOverlayScrollbar/useInitializeOverlayScrollbar';
import { PartialOptions } from 'overlayscrollbars';
import React, { CSSProperties, ReactElement } from 'react';
interface VirtuosoOverlayScrollbarProps {
children: ReactElement;
style?: CSSProperties;
options: PartialOptions;
}
export default function VirtuosoOverlayScrollbar({
children,
style,
options,
}: VirtuosoOverlayScrollbarProps): JSX.Element {
const { rootRef, setScroller } = useInitializeOverlayScrollbar(options);
const enhancedChild = React.cloneElement(children, {
scrollerRef: setScroller,
'data-overlayscrollbars-initialize': true,
});
return (
<div
data-overlayscrollbars-initialize
ref={rootRef}
className="overlay-scroll-wrapper"
style={style}
>
{enhancedChild}
</div>
);
}
VirtuosoOverlayScrollbar.defaultProps = { style: {} };

View File

@@ -0,0 +1,5 @@
.overlay-scroll-wrapper {
height: 100%;
width: 100%;
overflow: auto;
}

View File

@@ -12,6 +12,7 @@ import { ColumnType, TablePaginationConfig } from 'antd/es/table';
import { FilterValue, SorterResult } from 'antd/es/table/interface';
import { ColumnsType } from 'antd/lib/table';
import { FilterConfirmProps } from 'antd/lib/table/interface';
import logEvent from 'api/common/logEvent';
import getAll from 'api/errors/getAll';
import getErrorCounts from 'api/errors/getErrorCounts';
import { ResizeTable } from 'components/ResizeTable';
@@ -23,7 +24,8 @@ import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute
import useUrlQuery from 'hooks/useUrlQuery';
import createQueryParams from 'lib/createQueryParams';
import history from 'lib/history';
import { useCallback, useEffect, useMemo } from 'react';
import { isUndefined } from 'lodash-es';
import { useCallback, useEffect, useMemo, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { useQueries } from 'react-query';
import { useSelector } from 'react-redux';
@@ -410,6 +412,26 @@ function AllErrors(): JSX.Element {
[pathname],
);
const logEventCalledRef = useRef(false);
useEffect(() => {
if (
!logEventCalledRef.current &&
!isUndefined(errorCountResponse.data?.payload)
) {
const selectedEnvironments = queries.find(
(val) => val.tagKey === 'resource_deployment_environment',
)?.tagValue;
logEvent('Exception: List page visited', {
numberOfExceptions: errorCountResponse?.data?.payload,
selectedEnvironments,
resourceAttributeUsed: !!queries?.length,
});
logEventCalledRef.current = true;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [errorCountResponse.data?.payload]);
return (
<ResizeTable
columns={columns}

View File

@@ -5,7 +5,6 @@
.app-content {
width: calc(100% - 64px);
overflow: auto;
z-index: 0;
.content-container {

View File

@@ -9,6 +9,7 @@ import getLocalStorageKey from 'api/browser/localstorage/get';
import getUserLatestVersion from 'api/user/getLatestVersion';
import getUserVersion from 'api/user/getVersion';
import cx from 'classnames';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { IS_SIDEBAR_COLLAPSED } from 'constants/app';
import ROUTES from 'constants/routes';
import SideNav from 'container/SideNav';
@@ -303,24 +304,29 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
collapsed={collapsed}
/>
)}
<div className={cx('app-content', collapsed ? 'collapsed' : '')}>
<div
className={cx('app-content', collapsed ? 'collapsed' : '')}
data-overlayscrollbars-initialize
>
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
<LayoutContent>
<ChildrenContainer
style={{
margin:
isLogsView() ||
isTracesView() ||
isDashboardView() ||
isDashboardWidgetView() ||
isDashboardListView()
? 0
: '0 1rem',
}}
>
{isToDisplayLayout && !renderFullScreen && <TopNav />}
{children}
</ChildrenContainer>
<LayoutContent data-overlayscrollbars-initialize>
<OverlayScrollbar>
<ChildrenContainer
style={{
margin:
isLogsView() ||
isTracesView() ||
isDashboardView() ||
isDashboardWidgetView() ||
isDashboardListView()
? 0
: '0 1rem',
}}
>
{isToDisplayLayout && !renderFullScreen && <TopNav />}
{children}
</ChildrenContainer>
</OverlayScrollbar>
</LayoutContent>
</Sentry.ErrorBoundary>
</div>

View File

@@ -13,7 +13,6 @@ export const Layout = styled(LayoutComponent)`
`;
export const LayoutContent = styled(LayoutComponent.Content)`
overflow-y: auto;
height: 100%;
&::-webkit-scrollbar {
width: 0.1rem;

View File

@@ -19,10 +19,10 @@ import { ColumnsType } from 'antd/es/table';
import updateCreditCardApi from 'api/billing/checkout';
import getUsage, { UsageResponsePayloadProps } from 'api/billing/getUsage';
import manageCreditCardApi from 'api/billing/manage';
import logEvent from 'api/common/logEvent';
import Spinner from 'components/Spinner';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import useAnalytics from 'hooks/analytics/useAnalytics';
import useAxiosError from 'hooks/useAxiosError';
import useLicense from 'hooks/useLicense';
import { useNotifications } from 'hooks/useNotifications';
@@ -137,8 +137,6 @@ export default function BillingContainer(): JSX.Element {
Partial<UsageResponsePayloadProps>
>({});
const { trackEvent } = useAnalytics();
const { isFetching, data: licensesData, error: licenseError } = useLicense();
const { user, org } = useSelector<AppState, AppReducer>((state) => state.app);
@@ -316,7 +314,7 @@ export default function BillingContainer(): JSX.Element {
const handleBilling = useCallback(async () => {
if (isFreeTrial && !licensesData?.payload?.trialConvertedToSubscription) {
trackEvent('Billing : Upgrade Plan', {
logEvent('Billing : Upgrade Plan', {
user: pick(user, ['email', 'userId', 'name']),
org,
});
@@ -327,7 +325,7 @@ export default function BillingContainer(): JSX.Element {
cancelURL: window.location.href,
});
} else {
trackEvent('Billing : Manage Billing', {
logEvent('Billing : Manage Billing', {
user: pick(user, ['email', 'userId', 'name']),
org,
});

View File

@@ -449,8 +449,8 @@ function CreateAlertChannels({
const result = await functionToCall();
logEvent('Alert Channel: Save channel', {
type: value,
sendResolvedAlert: selectedConfig.send_resolved,
name: selectedConfig.name,
sendResolvedAlert: selectedConfig?.send_resolved,
name: selectedConfig?.name,
new: 'true',
status: result?.status,
statusMessage: result?.statusMessage,
@@ -530,8 +530,8 @@ function CreateAlertChannels({
logEvent('Alert Channel: Test notification', {
type: channelType,
sendResolvedAlert: selectedConfig.send_resolved,
name: selectedConfig.name,
sendResolvedAlert: selectedConfig?.send_resolved,
name: selectedConfig?.name,
new: 'true',
status:
response && response.statusCode === 200 ? 'Test success' : 'Test failed',

View File

@@ -370,8 +370,8 @@ function EditAlertChannels({
}
logEvent('Alert Channel: Save channel', {
type: value,
sendResolvedAlert: selectedConfig.send_resolved,
name: selectedConfig.name,
sendResolvedAlert: selectedConfig?.send_resolved,
name: selectedConfig?.name,
new: 'false',
status: result?.status,
statusMessage: result?.statusMessage,
@@ -441,8 +441,8 @@ function EditAlertChannels({
}
logEvent('Alert Channel: Test notification', {
type: channelType,
sendResolvedAlert: selectedConfig.send_resolved,
name: selectedConfig.name,
sendResolvedAlert: selectedConfig?.send_resolved,
name: selectedConfig?.name,
new: 'false',
status:
response && response.statusCode === 200 ? 'Test success' : 'Test failed',

View File

@@ -1,8 +1,34 @@
import './EmptyLogsSearch.styles.scss';
import { Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import { useEffect, useRef } from 'react';
import { DataSource, PanelTypeKeys } from 'types/common/queryBuilder';
export default function EmptyLogsSearch({
dataSource,
panelType,
}: {
dataSource: DataSource;
panelType: PanelTypeKeys;
}): JSX.Element {
const logEventCalledRef = useRef(false);
useEffect(() => {
if (!logEventCalledRef.current) {
if (dataSource === DataSource.TRACES) {
logEvent('Traces Explorer: No results', {
panelType,
});
} else if (dataSource === DataSource.LOGS) {
logEvent('Logs Explorer: No results', {
panelType,
});
}
logEventCalledRef.current = true;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
export default function EmptyLogsSearch(): JSX.Element {
return (
<div className="empty-logs-search-container">
<div className="empty-logs-search-container-content">

View File

@@ -1,6 +1,7 @@
import './styles.scss';
import { Button, Divider, Space, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import getNextPrevId from 'api/errors/getNextPrevId';
import Editor from 'components/Editor';
import { ResizeTable } from 'components/ResizeTable';
@@ -9,8 +10,9 @@ import dayjs from 'dayjs';
import { useNotifications } from 'hooks/useNotifications';
import createQueryParams from 'lib/createQueryParams';
import history from 'lib/history';
import { isUndefined } from 'lodash-es';
import { urlKey } from 'pages/ErrorDetails/utils';
import { useMemo, useState } from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useQuery } from 'react-query';
import { useLocation } from 'react-router-dom';
@@ -111,9 +113,29 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
}));
const onClickTraceHandler = (): void => {
logEvent('Exception: Navigate to trace detail page', {
groupId: errorDetail?.groupID,
spanId: errorDetail.spanID,
traceId: errorDetail.traceID,
exceptionId: errorDetail?.errorId,
});
history.push(`/trace/${errorDetail.traceID}?spanId=${errorDetail.spanID}`);
};
const logEventCalledRef = useRef(false);
useEffect(() => {
if (!logEventCalledRef.current && !isUndefined(data)) {
logEvent('Exception: Detail page visited', {
groupId: errorDetail?.groupID,
spanId: errorDetail.spanID,
traceId: errorDetail.traceID,
exceptionId: errorDetail?.errorId,
});
logEventCalledRef.current = true;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data]);
return (
<>
<Typography>{errorDetail.exceptionType}</Typography>

View File

@@ -14,6 +14,7 @@ import {
Tooltip,
Typography,
} from 'antd';
import logEvent from 'api/common/logEvent';
import axios from 'axios';
import cx from 'classnames';
import { getViewDetailsUsingViewKey } from 'components/ExplorerCard/utils';
@@ -93,7 +94,23 @@ function ExplorerOptions({
setIsExport(value);
}, []);
const {
currentQuery,
panelType,
isStagedQueryUpdated,
redirectWithQueryBuilderData,
} = useQueryBuilder();
const handleSaveViewModalToggle = (): void => {
if (sourcepage === DataSource.TRACES) {
logEvent('Traces Explorer: Save view clicked', {
panelType,
});
} else if (sourcepage === DataSource.LOGS) {
logEvent('Logs Explorer: Save view clicked', {
panelType,
});
}
setIsSaveModalOpen(!isSaveModalOpen);
};
@@ -104,11 +121,21 @@ function ExplorerOptions({
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
const onCreateAlertsHandler = useCallback(() => {
if (sourcepage === DataSource.TRACES) {
logEvent('Traces Explorer: Create alert', {
panelType,
});
} else if (sourcepage === DataSource.LOGS) {
logEvent('Logs Explorer: Create alert', {
panelType,
});
}
history.push(
`${ROUTES.ALERTS_NEW}?${QueryParams.compositeQuery}=${encodeURIComponent(
JSON.stringify(query),
)}`,
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [history, query]);
const onCancel = (value: boolean) => (): void => {
@@ -116,6 +143,15 @@ function ExplorerOptions({
};
const onAddToDashboard = (): void => {
if (sourcepage === DataSource.TRACES) {
logEvent('Traces Explorer: Add to dashboard clicked', {
panelType,
});
} else if (sourcepage === DataSource.LOGS) {
logEvent('Logs Explorer: Add to dashboard clicked', {
panelType,
});
}
setIsExport(true);
};
@@ -127,13 +163,6 @@ function ExplorerOptions({
refetch: refetchAllView,
} = useGetAllViews(sourcepage);
const {
currentQuery,
panelType,
isStagedQueryUpdated,
redirectWithQueryBuilderData,
} = useQueryBuilder();
const compositeQuery = mapCompositeQueryFromQuery(currentQuery, panelType);
const viewName = useGetSearchQueryParam(QueryParams.viewName) || '';
@@ -224,6 +253,17 @@ function ExplorerOptions({
onMenuItemSelectHandler({
key: option.key,
});
if (sourcepage === DataSource.TRACES) {
logEvent('Traces Explorer: Select view', {
panelType,
viewName: option?.value,
});
} else if (sourcepage === DataSource.LOGS) {
logEvent('Logs Explorer: Select view', {
panelType,
viewName: option?.value,
});
}
if (ref.current) {
ref.current.blur();
}
@@ -259,6 +299,17 @@ function ExplorerOptions({
viewName: newViewName,
setNewViewName,
});
if (sourcepage === DataSource.TRACES) {
logEvent('Traces Explorer: Save view successful', {
panelType,
viewName: newViewName,
});
} else if (sourcepage === DataSource.LOGS) {
logEvent('Logs Explorer: Save view successful', {
panelType,
viewName: newViewName,
});
}
};
// TODO: Remove this and move this to scss file
@@ -499,7 +550,7 @@ function ExplorerOptions({
export interface ExplorerOptionsProps {
isLoading?: boolean;
onExport: (dashboard: Dashboard | null) => void;
onExport: (dashboard: Dashboard | null, isNewDashboard?: boolean) => void;
query: Query | null;
disabled: boolean;
sourcepage: DataSource;

View File

@@ -41,7 +41,7 @@ function ExportPanelContainer({
} = useMutation(createDashboard, {
onSuccess: (data) => {
if (data.payload) {
onExport(data?.payload);
onExport(data?.payload, true);
}
refetch();
},
@@ -55,7 +55,7 @@ function ExportPanelContainer({
({ uuid }) => uuid === selectedDashboardId,
);
onExport(currentSelectedDashboard || null);
onExport(currentSelectedDashboard || null, false);
}, [data, selectedDashboardId, onExport]);
const handleSelect = useCallback(

View File

@@ -40,7 +40,7 @@ function ExportPanel({
export interface ExportPanelProps {
isLoading?: boolean;
onExport: (dashboard: Dashboard | null) => void;
onExport: (dashboard: Dashboard | null, isNewDashboard?: boolean) => void;
query: Query | null;
}

View File

@@ -88,7 +88,7 @@ function BasicInfo({
if (!channels.loading && isNewRule) {
logEvent('Alert: New alert creation page visited', {
dataSource: ALERTS_DATA_SOURCE_MAP[alertDef?.alertType as AlertTypes],
numberOfChannels: channels.payload?.length,
numberOfChannels: channels?.payload?.length,
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps

View File

@@ -48,6 +48,7 @@ export interface ChartPreviewProps {
userQueryKey?: string;
allowSelectedIntervalForStepGen?: boolean;
yAxisUnit: string;
setQueryStatus?: (status: string) => void;
}
// eslint-disable-next-line sonarjs/cognitive-complexity
@@ -62,6 +63,7 @@ function ChartPreview({
allowSelectedIntervalForStepGen = false,
alertDef,
yAxisUnit,
setQueryStatus,
}: ChartPreviewProps): JSX.Element | null {
const { t } = useTranslation('alerts');
const dispatch = useDispatch();
@@ -149,10 +151,10 @@ function ChartPreview({
useEffect((): void => {
const { startTime, endTime } = getTimeRange(queryResponse);
if (setQueryStatus) setQueryStatus(queryResponse.status);
setMinTimeScale(startTime);
setMaxTimeScale(endTime);
}, [maxTime, minTime, globalSelectedInterval, queryResponse]);
}, [maxTime, minTime, globalSelectedInterval, queryResponse, setQueryStatus]);
if (queryResponse.data && graphType === PANEL_TYPES.BAR) {
const sortedSeriesData = getSortedSeriesData(
@@ -284,6 +286,7 @@ ChartPreview.defaultProps = {
userQueryKey: '',
allowSelectedIntervalForStepGen: false,
alertDef: undefined,
setQueryStatus: (): void => {},
};
export default ChartPreview;

View File

@@ -101,6 +101,7 @@ function FormAlertRules({
const isNewRule = ruleId === 0;
const [loading, setLoading] = useState(false);
const [queryStatus, setQueryStatus] = useState<string>('');
// alertDef holds the form values to be posted
const [alertDef, setAlertDef] = useState<AlertDef>(initialValue);
@@ -523,6 +524,7 @@ function FormAlertRules({
alertDef={alertDef}
yAxisUnit={yAxisUnit || ''}
graphType={panelType || PANEL_TYPES.TIME_SERIES}
setQueryStatus={setQueryStatus}
/>
);
@@ -540,6 +542,7 @@ function FormAlertRules({
selectedInterval={globalSelectedInterval}
yAxisUnit={yAxisUnit || ''}
graphType={panelType || PANEL_TYPES.TIME_SERIES}
setQueryStatus={setQueryStatus}
/>
);
@@ -665,7 +668,8 @@ function FormAlertRules({
disabled={
isAlertNameMissing ||
isAlertAvailableToSave ||
!isChannelConfigurationValid
!isChannelConfigurationValid ||
queryStatus === 'error'
}
>
{isNewRule ? t('button_createrule') : t('button_savechanges')}
@@ -674,7 +678,11 @@ function FormAlertRules({
<ActionButton
loading={loading || false}
disabled={isAlertNameMissing || !isChannelConfigurationValid}
disabled={
isAlertNameMissing ||
!isChannelConfigurationValid ||
queryStatus === 'error'
}
type="default"
onClick={onTestRuleHandler}
>

View File

@@ -124,6 +124,9 @@ const getSpanWithoutChildren = (
value: span.value,
event: span.event,
hasError: span.hasError,
spanKind: span.spanKind,
statusCodeString: span.statusCodeString,
statusMessage: span.statusMessage,
});
export const isSpanPresentInSearchString = (

View File

@@ -3,6 +3,7 @@ import './DashboardEmptyState.styles.scss';
import { PlusOutlined } from '@ant-design/icons';
import { Button, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import SettingsDrawer from 'container/NewDashboard/DashboardDescription/SettingsDrawer';
import useComponentPermission from 'hooks/useComponentPermission';
import { useDashboard } from 'providers/Dashboard/Dashboard';
@@ -36,6 +37,12 @@ export default function DashboardEmptyState(): JSX.Element {
const onEmptyWidgetHandler = useCallback(() => {
handleToggleDashboardSlider(true);
logEvent('Dashboard Detail: Add new panel clicked', {
dashboardId: selectedDashboard?.uuid,
dashboardName: selectedDashboard?.data.title,
numberOfPanels: selectedDashboard?.data.widgets?.length,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [handleToggleDashboardSlider]);
return (
<section className="dashboard-empty-state">

View File

@@ -14,6 +14,7 @@ import { memo, useEffect, useRef, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { UpdateTimeInterval } from 'store/actions';
import { AppState } from 'store/reducers';
import { DataSource } from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { getGraphType } from 'utils/getGraphType';
import { getSortedSeriesData } from 'utils/getSortedSeriesData';
@@ -113,6 +114,7 @@ function GridCardGraph({
};
}
updatedQuery.builder.queryData[0].pageSize = 10;
const initialDataSource = updatedQuery.builder.queryData[0].dataSource;
return {
query: updatedQuery,
graphType: PANEL_TYPES.LIST,
@@ -123,6 +125,9 @@ function GridCardGraph({
offset: 0,
limit: updatedQuery.builder.queryData[0].limit || 0,
},
// we do not need select columns in case of logs
selectColumns:
initialDataSource === DataSource.TRACES && widget.selectedTracesFields,
},
fillGaps: widget.fillSpans,
};

View File

@@ -1,6 +1,7 @@
.fullscreen-grid-container {
overflow: auto;
margin: 8px -8px;
margin-right: 0;
.react-grid-layout {
border: none !important;
@@ -49,7 +50,7 @@
.footer {
display: flex;
flex-direction: column;
position: absolute;
position: fixed;
bottom: 0;
width: -webkit-fill-available;

View File

@@ -3,6 +3,7 @@ import './GridCardLayout.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Button, Form, Input, Modal, Typography } from 'antd';
import { useForm } from 'antd/es/form/Form';
import logEvent from 'api/common/logEvent';
import cx from 'classnames';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { QueryParams } from 'constants/query';
@@ -15,7 +16,7 @@ import { useIsDarkMode } from 'hooks/useDarkMode';
import { useNotifications } from 'hooks/useNotifications';
import useUrlQuery from 'hooks/useUrlQuery';
import history from 'lib/history';
import { defaultTo } from 'lodash-es';
import { defaultTo, isUndefined } from 'lodash-es';
import isEqual from 'lodash-es/isEqual';
import {
Check,
@@ -27,7 +28,7 @@ import {
} from 'lucide-react';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { sortLayout } from 'providers/Dashboard/util';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { FullScreen, FullScreenHandle } from 'react-full-screen';
import { ItemCallback, Layout } from 'react-grid-layout';
import { useDispatch, useSelector } from 'react-redux';
@@ -126,6 +127,18 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
setDashboardLayout(sortLayout(layouts));
}, [layouts]);
const logEventCalledRef = useRef(false);
useEffect(() => {
if (!logEventCalledRef.current && !isUndefined(data)) {
logEvent('Dashboard Detail: Opened', {
dashboardId: data.uuid,
dashboardName: data.title,
numberOfPanels: data.widgets?.length,
numberOfVariables: Object.keys(data?.variables || {}).length || 0,
});
logEventCalledRef.current = true;
}
}, [data]);
const onSaveHandler = (): void => {
if (!selectedDashboard) return;
@@ -428,7 +441,11 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
return isDashboardEmpty ? (
<DashboardEmptyState />
) : (
<FullScreen handle={handle} className="fullscreen-grid-container">
<FullScreen
handle={handle}
className="fullscreen-grid-container"
data-overlayscrollbars-initialize
>
<ReactGridLayout
cols={12}
rowHeight={45}

View File

@@ -79,7 +79,7 @@ function WidgetHeader({
);
}, [widget.id, widget.panelTypes, widget.query]);
const onCreateAlertsHandler = useCreateAlerts(widget);
const onCreateAlertsHandler = useCreateAlerts(widget, 'dashboardView');
const onDownloadHandler = useCallback((): void => {
const csv = unparse(tableProcessedDataRef.current);
@@ -234,6 +234,7 @@ function WidgetHeader({
)}
<Dropdown menu={menu} trigger={['hover']} placement="bottomRight">
<MoreOutlined
data-testid="widget-header-options"
className={`widget-header-more-options ${
parentHover ? 'widget-header-hover' : ''
}`}

View File

@@ -1,6 +1,10 @@
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { createColumnsAndDataSource, getQueryLegend } from '../utils';
import {
createColumnsAndDataSource,
getQueryLegend,
sortFunction,
} from '../utils';
import {
expectedOutputWithLegends,
tableDataMultipleQueriesSuccessResponse,
@@ -39,4 +43,88 @@ describe('Table Panel utils', () => {
// should return undefined when legend not present
expect(getQueryLegend(query, 'B')).toBe(undefined);
});
it('sorter function for table sorting', () => {
let rowA: {
A: string | number;
timestamp: number;
key: string;
} = {
A: 22.4,
timestamp: 111111,
key: '1111',
};
let rowB: {
A: string | number;
timestamp: number;
key: string;
} = {
A: 'n/a',
timestamp: 111112,
key: '1112',
};
const item = {
isValueColumn: true,
name: 'A',
queryName: 'A',
};
// A has value and value is considered bigger than n/a hence 1
expect(sortFunction(rowA, rowB, item)).toBe(1);
rowA = {
A: 'n/a',
timestamp: 111111,
key: '1111',
};
rowB = {
A: 22.4,
timestamp: 111112,
key: '1112',
};
// B has value and value is considered bigger than n/a hence -1
expect(sortFunction(rowA, rowB, item)).toBe(-1);
rowA = {
A: 11,
timestamp: 111111,
key: '1111',
};
rowB = {
A: 22,
timestamp: 111112,
key: '1112',
};
// A and B has value , since B > A hence A-B
expect(sortFunction(rowA, rowB, item)).toBe(-11);
rowA = {
A: 'read',
timestamp: 111111,
key: '1111',
};
rowB = {
A: 'write',
timestamp: 111112,
key: '1112',
};
// A and B are strings so A is smaller than B because r comes before w hence -1
expect(sortFunction(rowA, rowB, item)).toBe(-1);
rowA = {
A: 'n/a',
timestamp: 111111,
key: '1111',
};
rowB = {
A: 'n/a',
timestamp: 111112,
key: '1112',
};
// A and B are strings n/a , since both of them are same hence 0
expect(sortFunction(rowA, rowB, item)).toBe(0);
});
});

View File

@@ -1,3 +1,4 @@
/* eslint-disable sonarjs/cognitive-complexity */
import { ColumnsType, ColumnType } from 'antd/es/table';
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
import { QUERY_TABLE_CONFIG } from 'container/QueryTable/config';
@@ -105,6 +106,39 @@ export function getQueryLegend(
return legend;
}
export function sortFunction(
a: RowData,
b: RowData,
item: {
name: string;
queryName: string;
isValueColumn: boolean;
},
): number {
// assumption :- number values is bigger than 'n/a'
const valueA = Number(a[`${item.name}_without_unit`] ?? a[item.name]);
const valueB = Number(b[`${item.name}_without_unit`] ?? b[item.name]);
// if both the values are numbers then return the difference here
if (!isNaN(valueA) && !isNaN(valueB)) {
return valueA - valueB;
}
// if valueB is a number then make it bigger value
if (isNaN(valueA) && !isNaN(valueB)) {
return -1;
}
// if valueA is number make it the bigger value
if (!isNaN(valueA) && isNaN(valueB)) {
return 1;
}
// if both of them are strings do the localecompare
return ((a[item.name] as string) || '').localeCompare(
(b[item.name] as string) || '',
);
}
export function createColumnsAndDataSource(
data: TableData,
currentQuery: Query,
@@ -123,18 +157,7 @@ export function createColumnsAndDataSource(
title: !isEmpty(legend) ? legend : item.name,
width: QUERY_TABLE_CONFIG.width,
render: renderColumnCell && renderColumnCell[item.name],
sorter: (a: RowData, b: RowData): number => {
const valueA = Number(a[`${item.name}_without_unit`] ?? a[item.name]);
const valueB = Number(b[`${item.name}_without_unit`] ?? b[item.name]);
if (!isNaN(valueA) && !isNaN(valueB)) {
return valueA - valueB;
}
return ((a[item.name] as string) || '').localeCompare(
(b[item.name] as string) || '',
);
},
sorter: (a: RowData, b: RowData): number => sortFunction(a, b, item),
};
return [...acc, column];

View File

@@ -940,3 +940,50 @@
border-color: var(--bg-vanilla-300) !important;
}
}
.mt-8 {
margin-top: 8px;
}
.mt-12 {
margin-top: 12px;
}
.mt-24 {
margin-top: 24px;
}
.mb-24 {
margin-bottom: 24px;
}
.ingestion-setup-details-links {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 24px;
padding: 12px;
border-radius: 4px;
background: rgba(113, 144, 249, 0.1);
color: var(--bg-robin-300, #95acfb);
.learn-more {
display: inline-flex;
justify-content: center;
align-items: center;
text-decoration: underline;
color: var(--bg-robin-300, #95acfb);
}
}
.lightMode {
.ingestion-setup-details-links {
background: rgba(113, 144, 249, 0.1);
color: var(--bg-robin-500);
.learn-more {
color: var(--bg-robin-500);
}
}
}

View File

@@ -35,10 +35,12 @@ import { useGetAllIngestionsKeys } from 'hooks/IngestionKeys/useGetAllIngestionK
import useDebouncedFn from 'hooks/useDebouncedFunction';
import { useNotifications } from 'hooks/useNotifications';
import {
ArrowUpRight,
CalendarClock,
Check,
Copy,
Infinity,
Info,
Minus,
PenLine,
Plus,
@@ -875,10 +877,35 @@ function MultiIngestionSettings(): JSX.Element {
return (
<div className="ingestion-key-container">
<div className="ingestion-key-content">
<div className="ingestion-setup-details-links">
<Info size={14} />
<span>
Find your ingestion URL and learn more about sending data to SigNoz{' '}
<a
href="https://signoz.io/docs/ingestion/signoz-cloud/overview/"
target="_blank"
className="learn-more"
rel="noreferrer"
>
here <ArrowUpRight size={14} />
</a>
</span>
</div>
<header>
<Typography.Title className="title"> Ingestion Keys </Typography.Title>
<Typography.Text className="subtitle">
Create and manage ingestion keys for the SigNoz Cloud
Create and manage ingestion keys for the SigNoz Cloud{' '}
<a
href="https://signoz.io/docs/ingestion/signoz-cloud/keys/"
target="_blank"
className="learn-more"
rel="noreferrer"
>
{' '}
Learn more <ArrowUpRight size={14} />
</a>
</Typography.Text>
</header>

View File

@@ -0,0 +1,45 @@
import { render, screen } from 'tests/test-utils';
import MultiIngestionSettings from '../MultiIngestionSettings';
describe('MultiIngestionSettings Page', () => {
beforeEach(() => {
render(<MultiIngestionSettings />);
});
afterEach(() => {
jest.clearAllMocks();
});
it('renders MultiIngestionSettings page without crashing', () => {
expect(
screen.getByText(
'Find your ingestion URL and learn more about sending data to SigNoz',
),
).toBeInTheDocument();
expect(screen.getByText('Ingestion Keys')).toBeInTheDocument();
expect(
screen.getByText('Create and manage ingestion keys for the SigNoz Cloud'),
).toBeInTheDocument();
const overviewLink = screen.getByRole('link', { name: /here/i });
expect(overviewLink).toHaveAttribute(
'href',
'https://signoz.io/docs/ingestion/signoz-cloud/overview/',
);
expect(overviewLink).toHaveAttribute('target', '_blank');
expect(overviewLink).toHaveClass('learn-more');
expect(overviewLink).toHaveAttribute('rel', 'noreferrer');
const aboutKeyslink = screen.getByRole('link', { name: /Learn more/i });
expect(aboutKeyslink).toHaveAttribute(
'href',
'https://signoz.io/docs/ingestion/signoz-cloud/keys/',
);
expect(aboutKeyslink).toHaveAttribute('target', '_blank');
expect(aboutKeyslink).toHaveClass('learn-more');
expect(aboutKeyslink).toHaveAttribute('rel', 'noreferrer');
});
});

View File

@@ -49,9 +49,9 @@ export const alertActionLogEvent = (
break;
}
logEvent('Alert: Action', {
ruleId: record.id,
ruleId: record?.id,
dataSource: ALERTS_DATA_SOURCE_MAP[record.alertType as AlertTypes],
name: record.alert,
name: record?.alert,
action: actionValue,
});
};

View File

@@ -21,6 +21,7 @@ import {
Typography,
} from 'antd';
import { TableProps } from 'antd/lib';
import logEvent from 'api/common/logEvent';
import createDashboard from 'api/dashboard/create';
import { AxiosError } from 'axios';
import cx from 'classnames';
@@ -34,7 +35,7 @@ import { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard';
import useComponentPermission from 'hooks/useComponentPermission';
import { useNotifications } from 'hooks/useNotifications';
import history from 'lib/history';
import { get, isEmpty } from 'lodash-es';
import { get, isEmpty, isUndefined } from 'lodash-es';
import {
ArrowDownWideNarrow,
ArrowUpRight,
@@ -60,6 +61,7 @@ import {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { useTranslation } from 'react-i18next';
@@ -71,7 +73,6 @@ import { Dashboard } from 'types/api/dashboard/getAll';
import AppReducer from 'types/reducer/app';
import { isCloudUser } from 'utils/app';
import useUrlQuery from '../../hooks/useUrlQuery';
import DashboardTemplatesModal from './DashboardTemplates/DashboardTemplatesModal';
import ImportJSON from './ImportJSON';
import { DeleteButton } from './TableComponents/DeleteButton';
@@ -84,7 +85,7 @@ import {
// eslint-disable-next-line sonarjs/cognitive-complexity
function DashboardsList(): JSX.Element {
const {
data: dashboardListResponse = [],
data: dashboardListResponse,
isLoading: isDashboardListLoading,
error: dashboardFetchError,
refetch: refetchDashboardList,
@@ -97,12 +98,14 @@ function DashboardsList(): JSX.Element {
setListSortOrder: setSortOrder,
} = useDashboard();
const [searchString, setSearchString] = useState<string>(
sortOrder.search || '',
);
const [action, createNewDashboard] = useComponentPermission(
['action', 'create_new_dashboards'],
role,
);
const [searchValue, setSearchValue] = useState<string>('');
const [
showNewDashboardTemplatesModal,
setShowNewDashboardTemplatesModal,
@@ -121,10 +124,6 @@ function DashboardsList(): JSX.Element {
false,
);
const params = useUrlQuery();
const searchParams = params.get('search');
const [searchString, setSearchString] = useState<string>(searchParams || '');
const getLocalStorageDynamicColumns = (): DashboardDynamicColumns => {
const dashboardDynamicColumnsString = localStorage.getItem('dashboard');
let dashboardDynamicColumns: DashboardDynamicColumns = {
@@ -186,14 +185,6 @@ function DashboardsList(): JSX.Element {
setDashboards(sortedDashboards);
};
useEffect(() => {
params.set('columnKey', sortOrder.columnKey as string);
params.set('order', sortOrder.order as string);
params.set('page', sortOrder.pagination || '1');
history.replace({ search: params.toString() });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sortOrder]);
const sortHandle = (key: string): void => {
if (!dashboards) return;
if (key === 'createdAt') {
@@ -202,6 +193,7 @@ function DashboardsList(): JSX.Element {
columnKey: 'createdAt',
order: 'descend',
pagination: sortOrder.pagination || '1',
search: sortOrder.search || '',
});
} else if (key === 'updatedAt') {
sortDashboardsByUpdatedAt(dashboards);
@@ -209,21 +201,19 @@ function DashboardsList(): JSX.Element {
columnKey: 'updatedAt',
order: 'descend',
pagination: sortOrder.pagination || '1',
search: sortOrder.search || '',
});
}
};
function handlePageSizeUpdate(page: number): void {
setSortOrder((order) => ({
...order,
pagination: String(page),
}));
setSortOrder({ ...sortOrder, pagination: String(page) });
}
useEffect(() => {
const filteredDashboards = filterDashboard(
searchString,
dashboardListResponse,
dashboardListResponse || [],
);
if (sortOrder.columnKey === 'updatedAt') {
sortDashboardsByUpdatedAt(filteredDashboards || []);
@@ -234,6 +224,7 @@ function DashboardsList(): JSX.Element {
columnKey: 'updatedAt',
order: 'descend',
pagination: sortOrder.pagination || '1',
search: sortOrder.search || '',
});
sortDashboardsByUpdatedAt(filteredDashboards || []);
}
@@ -243,6 +234,7 @@ function DashboardsList(): JSX.Element {
setSortOrder,
sortOrder.columnKey,
sortOrder.pagination,
sortOrder.search,
]);
const [newDashboardState, setNewDashboardState] = useState({
@@ -269,6 +261,7 @@ function DashboardsList(): JSX.Element {
const onNewDashboardHandler = useCallback(async () => {
try {
logEvent('Dashboard List: Create dashboard clicked', {});
setNewDashboardState({
...newDashboardState,
loading: true,
@@ -305,18 +298,23 @@ function DashboardsList(): JSX.Element {
}, [newDashboardState, t]);
const onModalHandler = (uploadedGrafana: boolean): void => {
logEvent('Dashboard List: Import JSON clicked', {});
setIsImportJSONModalVisible((state) => !state);
setUploadedGrafana(uploadedGrafana);
};
const handleSearch = (event: ChangeEvent<HTMLInputElement>): void => {
setIsFilteringDashboards(true);
setSearchValue(event.target.value);
const searchText = (event as React.BaseSyntheticEvent)?.target?.value || '';
const filteredDashboards = filterDashboard(searchText, dashboardListResponse);
const filteredDashboards = filterDashboard(
searchText,
dashboardListResponse || [],
);
setDashboards(filteredDashboards);
setIsFilteringDashboards(false);
setSearchString(searchText);
setSortOrder({ ...sortOrder, search: searchText });
};
const [state, setCopy] = useCopyToClipboard();
@@ -407,7 +405,7 @@ function DashboardsList(): JSX.Element {
{
title: 'Dashboards',
key: 'dashboard',
render: (dashboard: Data): JSX.Element => {
render: (dashboard: Data, _, index): JSX.Element => {
const timeOptions: Intl.DateTimeFormatOptions = {
hour: '2-digit',
minute: '2-digit',
@@ -441,6 +439,10 @@ function DashboardsList(): JSX.Element {
} else {
history.push(getLink());
}
logEvent('Dashboard List: Clicked on dashboard', {
dashboardId: dashboard.id,
dashboardName: dashboard.name,
});
};
return (
@@ -452,7 +454,9 @@ function DashboardsList(): JSX.Element {
style={{ height: '14px', width: '14px' }}
alt="dashboard-image"
/>
<Typography.Text>{dashboard.name}</Typography.Text>
<Typography.Text data-testid={`dashboard-title-${index}`}>
{dashboard.name}
</Typography.Text>
</div>
<div className="tags-with-actions">
@@ -619,6 +623,21 @@ function DashboardsList(): JSX.Element {
hideOnSinglePage: true,
};
const logEventCalledRef = useRef(false);
useEffect(() => {
if (
!logEventCalledRef.current &&
!isDashboardListLoading &&
!isUndefined(dashboardListResponse)
) {
logEvent('Dashboard List: Page visited', {
number: dashboardListResponse?.length,
});
logEventCalledRef.current = true;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isDashboardListLoading]);
return (
<div className="dashboards-list-container">
<div className="dashboards-list-view-content">
@@ -677,7 +696,7 @@ function DashboardsList(): JSX.Element {
<ArrowUpRight size={16} className="learn-more-arrow" />
</section>
</div>
) : dashboards?.length === 0 && !searchValue ? (
) : dashboards?.length === 0 && !searchString ? (
<div className="dashboard-empty-state">
<img
src="/Icons/dashboards.svg"
@@ -705,6 +724,9 @@ function DashboardsList(): JSX.Element {
type="text"
className="new-dashboard"
icon={<Plus size={14} />}
onClick={(): void => {
logEvent('Dashboard List: New dashboard clicked', {});
}}
>
New Dashboard
</Button>
@@ -712,6 +734,7 @@ function DashboardsList(): JSX.Element {
<Button
type="text"
className="learn-more"
data-testid="learn-more"
onClick={(): void => {
window.open(
'https://signoz.io/docs/userguide/manage-dashboards?utm_source=product&utm_medium=dashboard-list-empty-state',
@@ -731,7 +754,7 @@ function DashboardsList(): JSX.Element {
<Input
placeholder="Search by name, description, or tags..."
prefix={<Search size={12} color={Color.BG_VANILLA_400} />}
value={searchValue}
value={searchString}
onChange={handleSearch}
/>
{createNewDashboard && (
@@ -745,6 +768,9 @@ function DashboardsList(): JSX.Element {
type="primary"
className="periscope-btn primary btn"
icon={<Plus size={14} />}
onClick={(): void => {
logEvent('Dashboard List: New dashboard clicked', {});
}}
>
New dashboard
</Button>
@@ -756,7 +782,7 @@ function DashboardsList(): JSX.Element {
<div className="no-search">
<img src="/Icons/emptyState.svg" alt="img" className="img" />
<Typography.Text className="text">
No dashboards found for {searchValue}. Create a new dashboard?
No dashboards found for {searchString}. Create a new dashboard?
</Typography.Text>
</div>
) : (
@@ -778,6 +804,7 @@ function DashboardsList(): JSX.Element {
type="text"
className={cx('sort-btns')}
onClick={(): void => sortHandle('createdAt')}
data-testid="sort-by-last-created"
>
Last created
{sortOrder.columnKey === 'createdAt' && <Check size={14} />}
@@ -786,6 +813,7 @@ function DashboardsList(): JSX.Element {
type="text"
className={cx('sort-btns')}
onClick={(): void => sortHandle('updatedAt')}
data-testid="sort-by-last-updated"
>
Last updated
{sortOrder.columnKey === 'updatedAt' && <Check size={14} />}
@@ -796,7 +824,7 @@ function DashboardsList(): JSX.Element {
placement="bottomRight"
arrow={false}
>
<ArrowDownWideNarrow size={14} />
<ArrowDownWideNarrow size={14} data-testid="sort-by" />
</Popover>
</Tooltip>
<Popover

View File

@@ -5,6 +5,7 @@ import { ExclamationCircleTwoTone } from '@ant-design/icons';
import MEditor, { Monaco } from '@monaco-editor/react';
import { Color } from '@signozhq/design-tokens';
import { Button, Modal, Space, Typography, Upload, UploadProps } from 'antd';
import logEvent from 'api/common/logEvent';
import createDashboard from 'api/dashboard/create';
import ROUTES from 'constants/routes';
import { useIsDarkMode } from 'hooks/useDarkMode';
@@ -67,6 +68,8 @@ function ImportJSON({
const onClickLoadJsonHandler = async (): Promise<void> => {
try {
setDashboardCreating(true);
logEvent('Dashboard List: Import and next clicked', {});
const dashboardData = JSON.parse(editorValue) as DashboardData;
if (dashboardData?.layout) {
@@ -86,6 +89,10 @@ function ImportJSON({
dashboardId: response.payload.uuid,
}),
);
logEvent('Dashboard List: New dashboard imported successfully', {
dashboardId: response.payload?.uuid,
dashboardName: response.payload?.data?.title,
});
} else if (response.error === 'feature usage exceeded') {
setIsFeatureAlert(true);
notifications.error({
@@ -180,6 +187,9 @@ function ImportJSON({
type="default"
className="periscope-btn"
icon={<MonitorDot size={14} />}
onClick={(): void => {
logEvent('Dashboard List: Upload JSON file clicked', {});
}}
>
{' '}
{t('upload_json_file')}

View File

@@ -3,6 +3,7 @@ import LogDetail from 'components/LogDetail';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import ListLogView from 'components/Logs/ListLogView';
import RawLogView from 'components/Logs/RawLogView';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import Spinner from 'components/Spinner';
import { CARD_BODY_STYLE } from 'constants/card';
import { LOCALSTORAGE } from 'constants/localStorage';
@@ -128,13 +129,15 @@ function LiveLogsList({ logs }: LiveLogsListProps): JSX.Element {
/>
) : (
<Card style={{ width: '100%' }} bodyStyle={CARD_BODY_STYLE}>
<Virtuoso
ref={ref}
initialTopMostItemIndex={activeLogIndex !== -1 ? activeLogIndex : 0}
data={logs}
totalCount={logs.length}
itemContent={getItemContent}
/>
<OverlayScrollbar isVirtuoso>
<Virtuoso
ref={ref}
initialTopMostItemIndex={activeLogIndex !== -1 ? activeLogIndex : 0}
data={logs}
totalCount={logs.length}
itemContent={getItemContent}
/>
</OverlayScrollbar>
</Card>
)}
</InfinityWrapperStyled>

View File

@@ -2,6 +2,7 @@ import './ContextLogRenderer.styles.scss';
import { Skeleton } from 'antd';
import RawLogView from 'components/Logs/RawLogView';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import ShowButton from 'container/LogsContextList/ShowButton';
import { ORDERBY_FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config';
import { useCallback, useEffect, useState } from 'react';
@@ -94,13 +95,15 @@ function ContextLogRenderer({
}}
/>
)}
<Virtuoso
className="virtuoso-list"
initialTopMostItemIndex={0}
data={logs}
itemContent={getItemContent}
style={{ height: `calc(${logs.length} * 32px)` }}
/>
<OverlayScrollbar isVirtuoso>
<Virtuoso
className="virtuoso-list"
initialTopMostItemIndex={0}
data={logs}
itemContent={getItemContent}
style={{ height: `calc(${logs.length} * 32px)` }}
/>
</OverlayScrollbar>
{isAfterLogsFetching && (
<Skeleton
style={{

View File

@@ -1,6 +1,7 @@
import './LogsContextList.styles.scss';
import RawLogView from 'components/Logs/RawLogView';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import Spinner from 'components/Spinner';
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
import { PANEL_TYPES } from 'constants/queryBuilder';
@@ -187,14 +188,15 @@ function LogsContextList({
<EmptyText>No Data</EmptyText>
)}
{isFetching && <Spinner size="large" height="10rem" />}
<Virtuoso
className="virtuoso-list"
initialTopMostItemIndex={0}
data={logs}
itemContent={getItemContent}
followOutput={order === ORDERBY_FILTERS.DESC}
/>
<OverlayScrollbar isVirtuoso>
<Virtuoso
className="virtuoso-list"
initialTopMostItemIndex={0}
data={logs}
itemContent={getItemContent}
followOutput={order === ORDERBY_FILTERS.DESC}
/>
</OverlayScrollbar>
</ListContainer>
{order === ORDERBY_FILTERS.DESC && (

View File

@@ -0,0 +1,11 @@
.logs-table {
table tbody tr {
&:hover {
background-color: rgba(171, 189, 255, 0.04);
td {
background-color: rgba(171, 189, 255, 0.04) !important;
}
}
}
}

View File

@@ -2,3 +2,51 @@
cursor: pointer;
position: relative;
}
.log-timestamp {
width: 240px;
max-width: 240px;
min-width: 240px;
padding: 4px 8px 6px;
div {
font-weight: 400;
line-height: 20px; /* 128.571% */
letter-spacing: 0.2px;
}
}
.log-body {
margin: 4px 0;
width: 700px;
max-width: 700px;
min-width: 700px;
color: white;
padding: 4px 8px 6px;
div {
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 128.571% */
letter-spacing: 0.1px;
width: 100%;
min-width: 0 !important;
}
}
.log-actions {
position: absolute;
right: 0;
height: 100%;
vertical-align: middle;
}
.lightMode {
.log-body {
color: '#1d1d1d';
background-color: #ffffffd5;
}
}

View File

@@ -73,6 +73,22 @@ export default function TableRow({
const children = elementWithChildren.children as ReactElement;
const props = elementWithChildren.props as Record<string, unknown>;
const isBody = column.key === 'body';
const isTimestamp = column.key === 'timestamp';
if (isTimestamp || isBody) {
return (
<td
key={column.key}
className={`${isTimestamp ? 'log-timestamp' : 'log-body'}`}
>
{cloneElement(children, props)}
</td>
);
}
// Setting the width of body column to 500px to maintain the consistency and reduce layout shift due to body content change
return (
<TableCellStyled
$isDragColumn={false}
@@ -84,11 +100,13 @@ export default function TableRow({
);
})}
{hasActions && isLogsExplorerPage && (
<LogLinesActionButtons
handleShowContext={handleShowContext}
onLogCopy={onLogCopy}
customClassName="table-view-log-actions"
/>
<div className="log-actions">
<LogLinesActionButtons
handleShowContext={handleShowContext}
onLogCopy={onLogCopy}
customClassName="table-view-log-actions"
/>
</div>
)}
</>
);

View File

@@ -1,3 +1,5 @@
import './InfinityTableView.styles.scss';
import LogDetail from 'components/LogDetail';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import { useTableView } from 'components/Logs/TableView/useTableView';
@@ -138,6 +140,7 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
tableViewProps.activeLogIndex !== -1 ? tableViewProps.activeLogIndex : 0
}
style={infinityDefaultStyles}
className="logs-table"
data={dataSource}
components={{
// eslint-disable-next-line react/jsx-props-no-spreading

View File

@@ -6,6 +6,7 @@ import { VIEW_TYPES } from 'components/LogDetail/constants';
// components
import ListLogView from 'components/Logs/ListLogView';
import RawLogView from 'components/Logs/RawLogView';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import Spinner from 'components/Spinner';
import { CARD_BODY_STYLE } from 'constants/card';
import { LOCALSTORAGE } from 'constants/localStorage';
@@ -133,15 +134,17 @@ function LogsExplorerList({
style={{ width: '100%', marginTop: '20px' }}
bodyStyle={CARD_BODY_STYLE}
>
<Virtuoso
ref={ref}
initialTopMostItemIndex={activeLogIndex !== -1 ? activeLogIndex : 0}
data={logs}
endReached={onEndReached}
totalCount={logs.length}
itemContent={getItemContent}
components={components}
/>
<OverlayScrollbar isVirtuoso>
<Virtuoso
ref={ref}
initialTopMostItemIndex={activeLogIndex !== -1 ? activeLogIndex : 0}
data={logs}
endReached={onEndReached}
totalCount={logs.length}
itemContent={getItemContent}
components={components}
/>
</OverlayScrollbar>
</Card>
);
}, [
@@ -169,7 +172,9 @@ function LogsExplorerList({
!isFetching &&
logs.length === 0 &&
!isError &&
isFilterApplied && <EmptyLogsSearch />}
isFilterApplied && (
<EmptyLogsSearch dataSource={DataSource.LOGS} panelType="LIST" />
)}
{isError && !isLoading && !isFetching && <LogsError />}

View File

@@ -2,6 +2,7 @@
import './LogsExplorerViews.styles.scss';
import { Button } from 'antd';
import logEvent from 'api/common/logEvent';
import LogsFormatOptionsMenu from 'components/LogsFormatOptionsMenu/LogsFormatOptionsMenu';
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
import { LOCALSTORAGE } from 'constants/localStorage';
@@ -37,7 +38,14 @@ import { useNotifications } from 'hooks/useNotifications';
import useUrlQueryData from 'hooks/useUrlQueryData';
import { FlatLogData } from 'lib/logs/flatLogData';
import { getPaginationQueryData } from 'lib/newQueryBuilder/getPaginationQueryData';
import { cloneDeep, defaultTo, isEmpty, omit, set } from 'lodash-es';
import {
cloneDeep,
defaultTo,
isEmpty,
isUndefined,
omit,
set,
} from 'lodash-es';
import { Sliders } from 'lucide-react';
import { SELECTED_VIEWS } from 'pages/LogsExplorer/utils';
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
@@ -310,6 +318,19 @@ function LogsExplorerViews({
],
);
const logEventCalledRef = useRef(false);
useEffect(() => {
if (!logEventCalledRef.current && !isUndefined(data?.payload)) {
const currentData = data?.payload?.data?.newResult?.data?.result || [];
logEvent('Logs Explorer: Page visited', {
panelType,
isEmpty: !currentData?.[0]?.list,
});
logEventCalledRef.current = true;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data?.payload]);
const {
mutate: updateDashboard,
isLoading: isUpdateDashboardLoading,
@@ -324,7 +345,7 @@ function LogsExplorerViews({
}, [currentQuery]);
const handleExport = useCallback(
(dashboard: Dashboard | null): void => {
(dashboard: Dashboard | null, isNewDashboard?: boolean): void => {
if (!dashboard || !panelType) return;
const panelTypeParam = AVAILABLE_EXPORT_PANEL_TYPES.includes(panelType)
@@ -346,6 +367,12 @@ function LogsExplorerViews({
options.selectColumns,
);
logEvent('Logs Explorer: Add to dashboard successful', {
panelType,
isNewDashboard,
dashboardName: dashboard?.data?.title,
});
updateDashboard(updatedDashboard, {
onSuccess: (data) => {
if (data.error) {

View File

@@ -3,6 +3,7 @@ import './LogsPanelComponent.styles.scss';
import { Table } from 'antd';
import LogDetail from 'components/LogDetail';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { PANEL_TYPES } from 'constants/queryBuilder';
import Controls from 'container/Controls';
@@ -207,17 +208,19 @@ function LogsPanelComponent({
<>
<div className="logs-table">
<div className="resize-table">
<Table
pagination={false}
tableLayout="fixed"
scroll={{ x: `calc(50vw - 10px)` }}
sticky
loading={queryResponse.isFetching}
style={tableStyles}
dataSource={flattenLogData}
columns={columns}
onRow={handleRow}
/>
<OverlayScrollbar>
<Table
pagination={false}
tableLayout="fixed"
scroll={{ x: `calc(50vw - 10px)` }}
sticky
loading={queryResponse.isFetching}
style={tableStyles}
dataSource={flattenLogData}
columns={columns}
onRow={handleRow}
/>
</OverlayScrollbar>
</div>
{!widget.query.builder.queryData[0].limit && (
<div className="controller">

View File

@@ -7,6 +7,7 @@ import { VIEW_TYPES } from 'components/LogDetail/constants';
import ListLogView from 'components/Logs/ListLogView';
import RawLogView from 'components/Logs/RawLogView';
import LogsTableView from 'components/Logs/TableView';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import Spinner from 'components/Spinner';
import { CARD_BODY_STYLE } from 'constants/card';
import { useActiveLog } from 'hooks/logs/useActiveLog';
@@ -97,7 +98,9 @@ function LogsTable(props: LogsTableProps): JSX.Element {
return (
<Card className="logs-card" bodyStyle={CARD_BODY_STYLE}>
<Virtuoso totalCount={logs.length} itemContent={getItemContent} />
<OverlayScrollbar isVirtuoso>
<Virtuoso totalCount={logs.length} itemContent={getItemContent} />
</OverlayScrollbar>
</Card>
);
}, [getItemContent, linesPerRow, logs, onSetActiveLog, selected, viewMode]);

View File

@@ -1,4 +1,5 @@
import { Col } from 'antd';
import logEvent from 'api/common/logEvent';
import { ENTITY_VERSION_V4 } from 'constants/app';
import { PANEL_TYPES } from 'constants/queryBuilder';
import Graph from 'container/GridCardLayout/GridCard';
@@ -11,7 +12,7 @@ import {
convertRawQueriesToTraceSelectedTags,
resourceAttributesToTagFilterItems,
} from 'hooks/useResourceAttribute/utils';
import { useMemo, useState } from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useParams } from 'react-router-dom';
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
@@ -97,6 +98,24 @@ function DBCall(): JSX.Element {
[servicename, tagFilterItems],
);
const logEventCalledRef = useRef(false);
useEffect(() => {
if (!logEventCalledRef.current) {
const selectedEnvironments = queries.find(
(val) => val.tagKey === 'resource_deployment_environment',
)?.tagValue;
logEvent('APM: Service detail page visited', {
selectedEnvironments,
resourceAttributeUsed: !!queries?.length,
section: 'dbMetrics',
});
logEventCalledRef.current = true;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const apmToTraceQuery = useGetAPMToTracesQueries({
servicename,
isDBCall: true,

View File

@@ -1,4 +1,5 @@
import { Col } from 'antd';
import logEvent from 'api/common/logEvent';
import { ENTITY_VERSION_V4 } from 'constants/app';
import { PANEL_TYPES } from 'constants/queryBuilder';
import Graph from 'container/GridCardLayout/GridCard';
@@ -13,7 +14,7 @@ import {
convertRawQueriesToTraceSelectedTags,
resourceAttributesToTagFilterItems,
} from 'hooks/useResourceAttribute/utils';
import { useMemo, useState } from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useParams } from 'react-router-dom';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { EQueryType } from 'types/common/dashboard';
@@ -114,6 +115,23 @@ function External(): JSX.Element {
],
});
const logEventCalledRef = useRef(false);
useEffect(() => {
if (!logEventCalledRef.current) {
const selectedEnvironments = queries.find(
(val) => val.tagKey === 'resource_deployment_environment',
)?.tagValue;
logEvent('APM: Service detail page visited', {
selectedEnvironments,
resourceAttributeUsed: !!queries?.length,
section: 'externalMetrics',
});
logEventCalledRef.current = true;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const externalCallRPSWidget = useMemo(
() =>
getWidgetQueryBuilder({

View File

@@ -1,3 +1,4 @@
import logEvent from 'api/common/logEvent';
import getTopLevelOperations, {
ServiceDataProps,
} from 'api/metrics/getTopLevelOperations';
@@ -17,7 +18,7 @@ import useUrlQuery from 'hooks/useUrlQuery';
import history from 'lib/history';
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
import { defaultTo } from 'lodash-es';
import { useCallback, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useQuery } from 'react-query';
import { useDispatch } from 'react-redux';
import { useLocation, useParams } from 'react-router-dom';
@@ -81,6 +82,23 @@ function Application(): JSX.Element {
[handleSetTimeStamp],
);
const logEventCalledRef = useRef(false);
useEffect(() => {
if (!logEventCalledRef.current) {
const selectedEnvironments = queries.find(
(val) => val.tagKey === 'resource_deployment_environment',
)?.tagValue;
logEvent('APM: Service detail page visited', {
selectedEnvironments,
resourceAttributeUsed: !!queries?.length,
section: 'overview',
});
logEventCalledRef.current = true;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const {
data: topLevelOperations,
error: topLevelOperationsError,

View File

@@ -1,6 +1,7 @@
import './ComponentSlider.styles.scss';
import { Card, Modal } from 'antd';
import logEvent from 'api/common/logEvent';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import createQueryParams from 'lib/createQueryParams';
@@ -20,6 +21,13 @@ function DashboardGraphSlider(): JSX.Element {
const onClickHandler = (name: PANEL_TYPES) => (): void => {
const id = uuid();
handleToggleDashboardSlider(false);
logEvent('Dashboard Detail: New panel type selected', {
// dashboardId: '',
// dashboardName: '',
// numberOfPanels: 0, // todo - at this point we don't know these attributes
panelType: name,
widgetId: id,
});
const queryParamsLog = {
graphType: name,
widgetId: id,
@@ -47,7 +55,6 @@ function DashboardGraphSlider(): JSX.Element {
PANEL_TYPES_INITIAL_QUERY[name],
),
};
if (name === PANEL_TYPES.LIST) {
history.push(
`${history.location.pathname}/new?${createQueryParams(queryParamsLog)}`,

View File

@@ -2,6 +2,7 @@ import './Description.styles.scss';
import { Button } from 'antd';
import ConfigureIcon from 'assets/Integrations/ConfigureIcon';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { useRef, useState } from 'react';
import DashboardSettingsContent from '../DashboardSettings';
@@ -41,7 +42,9 @@ function SettingsDrawer({ drawerTitle }: { drawerTitle: string }): JSX.Element {
open={visible}
rootClassName="settings-container-root"
>
<DashboardSettingsContent variableViewModeRef={variableViewModeRef} />
<OverlayScrollbar>
<DashboardSettingsContent variableViewModeRef={variableViewModeRef} />
</OverlayScrollbar>
</DrawerContainer>
</>
);

View File

@@ -2,6 +2,7 @@ import './Description.styles.scss';
import { PlusOutlined } from '@ant-design/icons';
import { Button, Card, Input, Modal, Popover, Tag, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn';
import { dashboardHelpMessage } from 'components/facingIssueBtn/util';
import { SOMETHING_WENT_WRONG } from 'constants/api';
@@ -126,6 +127,12 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
const onEmptyWidgetHandler = useCallback(() => {
handleToggleDashboardSlider(true);
logEvent('Dashboard Detail: Add new panel clicked', {
dashboardId: selectedDashboard?.uuid,
dashboardName: selectedDashboard?.data.title,
numberOfPanels: selectedDashboard?.data.widgets?.length,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [handleToggleDashboardSlider]);
const handleLockDashboardToggle = (): void => {
@@ -259,6 +266,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
urlQuery.set('columnKey', listSortOrder.columnKey as string);
urlQuery.set('order', listSortOrder.order as string);
urlQuery.set('page', listSortOrder.pagination as string);
urlQuery.set('search', listSortOrder.search as string);
urlQuery.delete(QueryParams.relativeTime);
const generatedUrl = `${ROUTES.ALL_DASHBOARD}?${urlQuery.toString()}`;
@@ -404,7 +412,12 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
trigger="click"
placement="bottomRight"
>
<Button icon={<Ellipsis size={14} />} type="text" className="icons" />
<Button
icon={<Ellipsis size={14} />}
type="text"
className="icons"
data-testid="options"
/>
</Popover>
{!isDashboardLocked && editDashboard && (
<SettingsDrawer drawerTitle="Dashboard Configuration" />
@@ -415,7 +428,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
onClick={onEmptyWidgetHandler}
icon={<PlusOutlined />}
type="primary"
data-testid="add-panel"
data-testid="add-panel-header"
>
New Panel
</Button>

View File

@@ -6,7 +6,7 @@ import GridGraphs from './GridGraphs';
function NewDashboard(): JSX.Element {
const handle = useFullScreenHandle();
return (
<div style={{ overflowX: 'hidden' }}>
<div>
<Description handle={handle} />
<GridGraphs handle={handle} />
</div>

View File

@@ -309,6 +309,7 @@ function ExplorerColumnsRenderer({
>
<Button
className="action-btn"
data-testid="add-columns-button"
icon={
<PlusCircle
size={16}

View File

@@ -2,6 +2,7 @@ import './QuerySection.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Button, Tabs, Tooltip, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import PromQLIcon from 'assets/Dashboard/PromQl';
import TextToolTip from 'components/TextToolTip';
import { PANEL_TYPES } from 'constants/queryBuilder';
@@ -14,7 +15,7 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useUrlQuery from 'hooks/useUrlQuery';
import { defaultTo } from 'lodash-es';
import { defaultTo, isUndefined } from 'lodash-es';
import { Atom, Play, Terminal } from 'lucide-react';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import {
@@ -122,6 +123,18 @@ function QuerySection({
};
const handleRunQuery = (): void => {
const widgetId = urlQuery.get('widgetId');
const isNewPanel = isUndefined(widgets?.find((e) => e.id === widgetId));
logEvent('Panel Edit: Stage and run query', {
dataSource: currentQuery.builder?.queryData?.[0]?.dataSource,
panelType: selectedWidget.panelTypes,
queryType: currentQuery.queryType,
widgetId: selectedWidget.id,
dashboardId: selectedDashboard?.uuid,
dashboardName: selectedDashboard?.data.title,
isNewPanel,
});
handleStageQuery(currentQuery);
};

View File

@@ -82,7 +82,7 @@ function RightContainer({
const selectedGraphType =
GraphTypes.find((e) => e.name === selectedGraph)?.display || '';
const onCreateAlertsHandler = useCreateAlerts(selectedWidget);
const onCreateAlertsHandler = useCreateAlerts(selectedWidget, 'panelView');
const allowThreshold = panelTypeVsThreshold[selectedGraph];
const allowSoftMinMax = panelTypeVsSoftMinMax[selectedGraph];
@@ -163,6 +163,7 @@ function RightContainer({
value={selectedGraph}
style={{ width: '100%' }}
className="panel-type-select"
data-testid="panel-change-select"
>
{graphTypes.map((item) => (
<Option key={item.name} value={item.name}>

View File

@@ -3,8 +3,10 @@ import './NewWidget.styles.scss';
import { WarningOutlined } from '@ant-design/icons';
import { Button, Flex, Modal, Space, Tooltip, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn';
import { chartHelpMessage } from 'components/facingIssueBtn/util';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { FeatureKeys } from 'constants/features';
import { QueryParams } from 'constants/query';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
@@ -30,7 +32,7 @@ import {
getPreviousWidgets,
getSelectedWidgetIndex,
} from 'providers/Dashboard/util';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { generatePath, useParams } from 'react-router-dom';
@@ -77,6 +79,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
stagedQuery,
redirectWithQueryBuilderData,
supersetQuery,
setSupersetQuery,
} = useQueryBuilder();
const isQueryModified = useMemo(
@@ -100,6 +103,8 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
const [isNewDashboard, setIsNewDashboard] = useState<boolean>(false);
const logEventCalledRef = useRef(false);
useEffect(() => {
const widgetId = query.get('widgetId');
const selectedWidget = widgets?.find((e) => e.id === widgetId);
@@ -107,6 +112,18 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
if (isWidgetNotPresent) {
setIsNewDashboard(true);
}
if (!logEventCalledRef.current) {
logEvent('Panel Edit: Page visited', {
panelType: selectedWidget?.panelTypes,
dashboardId: selectedDashboard?.uuid,
widgetId: selectedWidget?.id,
dashboardName: selectedDashboard?.data.title,
isNewPanel: !!isWidgetNotPresent,
dataSource: currentQuery.builder.queryData?.[0]?.dataSource,
});
logEventCalledRef.current = true;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@@ -481,7 +498,20 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
};
const onSaveDashboard = useCallback((): void => {
const widgetId = query.get('widgetId');
const selectWidget = widgets?.find((e) => e.id === widgetId);
logEvent('Panel Edit: Save changes', {
panelType: selectedWidget.panelTypes,
dashboardId: selectedDashboard?.uuid,
widgetId: selectedWidget.id,
dashboardName: selectedDashboard?.data.title,
queryType: currentQuery.queryType,
isNewPanel: isUndefined(selectWidget),
dataSource: currentQuery.builder.queryData?.[0]?.dataSource,
});
setSaveModal(true);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const isQueryBuilderActive = useIsFeatureDisabled(
@@ -519,6 +549,17 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
isNewTraceLogsAvailable,
]);
useEffect(() => {
/**
* we need this extra handling for superset query because we cannot keep this in sync with current query
* always.we do not sync superset query in the initQueryBuilderData because that function is called on all stage and run
* actions. we do not want that as we loose out on superset functionalities if we do the same. hence initialising the superset query
* on mount here with the currentQuery in the begining itself
*/
setSupersetQuery(currentQuery);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
registerShortcut(DashboardShortcuts.SaveChanges, onSaveDashboard);
registerShortcut(DashboardShortcuts.DiscardChanges, onClickDiscardHandler);
@@ -530,11 +571,39 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [onSaveDashboard]);
useEffect(() => {
if (selectedGraph === PANEL_TYPES.LIST) {
const initialDataSource = currentQuery.builder.queryData[0].dataSource;
if (initialDataSource === DataSource.LOGS) {
// we do not need selected log columns in the request data as the entire response contains all the necessary data
setRequestData((prev) => ({
...prev,
tableParams: {
...prev.tableParams,
},
}));
} else if (initialDataSource === DataSource.TRACES) {
setRequestData((prev) => ({
...prev,
tableParams: {
...prev.tableParams,
selectColumns: selectedTracesFields,
},
}));
}
}
}, [selectedLogFields, selectedTracesFields, currentQuery, selectedGraph]);
return (
<Container>
<div className="edit-header">
<div className="left-header">
<X size={14} onClick={onClickDiscardHandler} className="discard-icon" />
<X
size={14}
onClick={onClickDiscardHandler}
className="discard-icon"
data-testid="discard-button"
/>
<Flex align="center" gap={24}>
<Typography.Text className="configure-panel">
Configure panel
@@ -586,60 +655,64 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
<PanelContainer>
<LeftContainerWrapper isDarkMode={useIsDarkMode()}>
{selectedWidget && (
<LeftContainer
selectedGraph={graphType}
selectedLogFields={selectedLogFields}
setSelectedLogFields={setSelectedLogFields}
selectedTracesFields={selectedTracesFields}
setSelectedTracesFields={setSelectedTracesFields}
selectedWidget={selectedWidget}
selectedTime={selectedTime}
requestData={requestData}
setRequestData={setRequestData}
isLoadingPanelData={isLoadingPanelData}
/>
)}
<OverlayScrollbar>
{selectedWidget && (
<LeftContainer
selectedGraph={graphType}
selectedLogFields={selectedLogFields}
setSelectedLogFields={setSelectedLogFields}
selectedTracesFields={selectedTracesFields}
setSelectedTracesFields={setSelectedTracesFields}
selectedWidget={selectedWidget}
selectedTime={selectedTime}
requestData={requestData}
setRequestData={setRequestData}
isLoadingPanelData={isLoadingPanelData}
/>
)}
</OverlayScrollbar>
</LeftContainerWrapper>
<RightContainerWrapper>
<RightContainer
setGraphHandler={setGraphHandler}
title={title}
setTitle={setTitle}
description={description}
setDescription={setDescription}
stacked={stacked}
setStacked={setStacked}
stackedBarChart={stackedBarChart}
setStackedBarChart={setStackedBarChart}
opacity={opacity}
yAxisUnit={yAxisUnit}
columnUnits={columnUnits}
setColumnUnits={setColumnUnits}
bucketCount={bucketCount}
bucketWidth={bucketWidth}
combineHistogram={combineHistogram}
setCombineHistogram={setCombineHistogram}
setBucketWidth={setBucketWidth}
setBucketCount={setBucketCount}
setOpacity={setOpacity}
selectedNullZeroValue={selectedNullZeroValue}
setSelectedNullZeroValue={setSelectedNullZeroValue}
selectedGraph={graphType}
setSelectedTime={setSelectedTime}
selectedTime={selectedTime}
setYAxisUnit={setYAxisUnit}
thresholds={thresholds}
setThresholds={setThresholds}
selectedWidget={selectedWidget}
isFillSpans={isFillSpans}
setIsFillSpans={setIsFillSpans}
softMin={softMin}
setSoftMin={setSoftMin}
softMax={softMax}
setSoftMax={setSoftMax}
/>
<OverlayScrollbar>
<RightContainer
setGraphHandler={setGraphHandler}
title={title}
setTitle={setTitle}
description={description}
setDescription={setDescription}
stacked={stacked}
setStacked={setStacked}
stackedBarChart={stackedBarChart}
setStackedBarChart={setStackedBarChart}
opacity={opacity}
yAxisUnit={yAxisUnit}
columnUnits={columnUnits}
setColumnUnits={setColumnUnits}
bucketCount={bucketCount}
bucketWidth={bucketWidth}
combineHistogram={combineHistogram}
setCombineHistogram={setCombineHistogram}
setBucketWidth={setBucketWidth}
setBucketCount={setBucketCount}
setOpacity={setOpacity}
selectedNullZeroValue={selectedNullZeroValue}
setSelectedNullZeroValue={setSelectedNullZeroValue}
selectedGraph={graphType}
setSelectedTime={setSelectedTime}
selectedTime={selectedTime}
setYAxisUnit={setYAxisUnit}
thresholds={thresholds}
setThresholds={setThresholds}
selectedWidget={selectedWidget}
isFillSpans={isFillSpans}
setIsFillSpans={setIsFillSpans}
softMin={softMin}
setSoftMin={setSoftMin}
softMax={softMax}
setSoftMax={setSoftMax}
/>
</OverlayScrollbar>
</RightContainerWrapper>
</PanelContainer>
<Modal

View File

@@ -54,6 +54,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
'queryName',
'expression',
'disabled',
'legend',
],
},
},
@@ -72,6 +73,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
'queryName',
'expression',
'disabled',
'legend',
],
},
},
@@ -88,6 +90,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
'queryName',
'expression',
'disabled',
'legend',
],
},
},
@@ -107,6 +110,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
'queryName',
'expression',
'disabled',
'legend',
],
},
},
@@ -125,6 +129,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
'queryName',
'expression',
'disabled',
'legend',
],
},
},
@@ -141,6 +146,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
'queryName',
'expression',
'disabled',
'legend',
],
},
},
@@ -157,6 +163,8 @@ export const panelTypeDataSourceFormValuesMap: Record<
'having',
'orderBy',
'functions',
'disabled',
'legend',
],
},
},
@@ -172,6 +180,8 @@ export const panelTypeDataSourceFormValuesMap: Record<
'orderBy',
'functions',
'spaceAggregation',
'disabled',
'legend',
],
},
},
@@ -185,6 +195,8 @@ export const panelTypeDataSourceFormValuesMap: Record<
'limit',
'having',
'orderBy',
'disabled',
'legend',
],
},
},
@@ -204,6 +216,8 @@ export const panelTypeDataSourceFormValuesMap: Record<
'queryName',
'expression',
'disabled',
'reduceTo',
'legend',
],
},
},
@@ -222,6 +236,8 @@ export const panelTypeDataSourceFormValuesMap: Record<
'queryName',
'expression',
'disabled',
'reduceTo',
'legend',
],
},
},
@@ -238,6 +254,8 @@ export const panelTypeDataSourceFormValuesMap: Record<
'queryName',
'expression',
'disabled',
'reduceTo',
'legend',
],
},
},
@@ -257,6 +275,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
'queryName',
'expression',
'disabled',
'legend',
],
},
},
@@ -275,6 +294,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
'queryName',
'expression',
'disabled',
'legend',
],
},
},
@@ -291,6 +311,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
'queryName',
'expression',
'disabled',
'legend',
],
},
},
@@ -325,6 +346,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
'queryName',
'expression',
'disabled',
'legend',
],
},
},
@@ -341,6 +363,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
'queryName',
'expression',
'disabled',
'legend',
],
},
},
@@ -357,6 +380,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
'queryName',
'expression',
'disabled',
'legend',
],
},
},

View File

@@ -1,6 +1,7 @@
import './NoLogs.styles.scss';
import { Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import ROUTES from 'constants/routes';
import history from 'lib/history';
import { ArrowUpRight } from 'lucide-react';
@@ -21,6 +22,11 @@ export default function NoLogs({
e.stopPropagation();
if (cloudUser) {
if (dataSource === DataSource.TRACES) {
logEvent('Traces Explorer: Navigate to onboarding', {});
} else if (dataSource === DataSource.LOGS) {
logEvent('Logs Explorer: Navigate to onboarding', {});
}
history.push(
dataSource === 'traces'
? ROUTES.GET_STARTED_APPLICATION_MONITORING

View File

@@ -11,7 +11,6 @@ import ROUTES from 'constants/routes';
import FullScreenHeader from 'container/FullScreenHeader/FullScreenHeader';
import InviteUserModal from 'container/OrganizationSettings/InviteUserModal/InviteUserModal';
import { InviteMemberFormValues } from 'container/OrganizationSettings/PendingInvitesContainer';
import useAnalytics from 'hooks/analytics/useAnalytics';
import history from 'lib/history';
import { UserPlus } from 'lucide-react';
import { useCallback, useEffect, useState } from 'react';
@@ -104,7 +103,6 @@ export default function Onboarding(): JSX.Element {
const [selectedModuleSteps, setSelectedModuleSteps] = useState(APM_STEPS);
const [activeStep, setActiveStep] = useState(1);
const [current, setCurrent] = useState(0);
const { trackEvent } = useAnalytics();
const { location } = history;
const { t } = useTranslation(['onboarding']);
@@ -120,7 +118,7 @@ export default function Onboarding(): JSX.Element {
} = useOnboardingContext();
useEffectOnce(() => {
trackEvent('Onboarding V2 Started');
logEvent('Onboarding V2 Started', {});
});
const { status, data: ingestionData } = useQuery({
@@ -231,7 +229,7 @@ export default function Onboarding(): JSX.Element {
const nextStep = activeStep + 1;
// on next
trackEvent('Onboarding V2: Get Started', {
logEvent('Onboarding V2: Get Started', {
selectedModule: selectedModule.id,
nextStepId: nextStep,
});

View File

@@ -5,9 +5,9 @@ import {
CloseCircleTwoTone,
LoadingOutlined,
} from '@ant-design/icons';
import logEvent from 'api/common/logEvent';
import Header from 'container/OnboardingContainer/common/Header/Header';
import { useOnboardingContext } from 'container/OnboardingContainer/context/OnboardingContext';
import useAnalytics from 'hooks/analytics/useAnalytics';
import { useQueryService } from 'hooks/useQueryService';
import useResourceAttribute from 'hooks/useResourceAttribute';
import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils';
@@ -41,8 +41,6 @@ export default function ConnectionStatus(): JSX.Element {
[queries],
);
const { trackEvent } = useAnalytics();
const [retryCount, setRetryCount] = useState(20); // Retry for 3 mins 20s
const [loading, setLoading] = useState(true);
const [isReceivingData, setIsReceivingData] = useState(false);
@@ -155,7 +153,7 @@ export default function ConnectionStatus(): JSX.Element {
if (data || isError) {
setRetryCount(retryCount - 1);
if (retryCount < 0) {
trackEvent('Onboarding V2: Connection Status', {
logEvent('Onboarding V2: Connection Status', {
dataSource: selectedDataSource?.id,
framework: selectedFramework,
environment: selectedEnvironment,
@@ -174,7 +172,7 @@ export default function ConnectionStatus(): JSX.Element {
setLoading(false);
setIsReceivingData(true);
trackEvent('Onboarding V2: Connection Status', {
logEvent('Onboarding V2: Connection Status', {
dataSource: selectedDataSource?.id,
framework: selectedFramework,
environment: selectedEnvironment,

View File

@@ -5,11 +5,11 @@ import {
CloseCircleTwoTone,
LoadingOutlined,
} from '@ant-design/icons';
import logEvent from 'api/common/logEvent';
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
import { PANEL_TYPES } from 'constants/queryBuilder';
import Header from 'container/OnboardingContainer/common/Header/Header';
import { useOnboardingContext } from 'container/OnboardingContainer/context/OnboardingContext';
import useAnalytics from 'hooks/analytics/useAnalytics';
import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange';
import { useEffect, useState } from 'react';
import { SuccessResponse } from 'types/api';
@@ -32,7 +32,6 @@ export default function LogsConnectionStatus(): JSX.Element {
activeStep,
selectedEnvironment,
} = useOnboardingContext();
const { trackEvent } = useAnalytics();
const [isReceivingData, setIsReceivingData] = useState(false);
const [pollingInterval, setPollingInterval] = useState<number | false>(15000); // initial Polling interval of 15 secs , Set to false after 5 mins
const [retryCount, setRetryCount] = useState(20); // Retry for 5 mins
@@ -105,7 +104,7 @@ export default function LogsConnectionStatus(): JSX.Element {
setRetryCount(retryCount - 1);
if (retryCount < 0) {
trackEvent('Onboarding V2: Connection Status', {
logEvent('Onboarding V2: Connection Status', {
dataSource: selectedDataSource?.id,
environment: selectedEnvironment,
module: activeStep?.module?.id,
@@ -141,7 +140,7 @@ export default function LogsConnectionStatus(): JSX.Element {
setRetryCount(-1);
setPollingInterval(false);
trackEvent('Onboarding V2: Connection Status', {
logEvent('Onboarding V2: Connection Status', {
dataSource: selectedDataSource?.id,
environment: selectedEnvironment,
module: activeStep?.module?.id,

View File

@@ -15,7 +15,6 @@ import ROUTES from 'constants/routes';
import { stepsMap } from 'container/OnboardingContainer/constants/stepsConfig';
import { DataSourceType } from 'container/OnboardingContainer/Steps/DataSource/DataSource';
import { hasFrameworks } from 'container/OnboardingContainer/utils/dataSourceUtils';
import useAnalytics from 'hooks/analytics/useAnalytics';
import history from 'lib/history';
import { isEmpty, isNull } from 'lodash-es';
import { HelpCircle, UserPlus } from 'lucide-react';
@@ -79,7 +78,6 @@ export default function ModuleStepsContainer({
} = useOnboardingContext();
const [current, setCurrent] = useState(0);
const { trackEvent } = useAnalytics();
const [metaData, setMetaData] = useState<MetaDataProps[]>(defaultMetaData);
const lastStepIndex = selectedModuleSteps.length - 1;
@@ -143,7 +141,7 @@ export default function ModuleStepsContainer({
};
const redirectToModules = (): void => {
trackEvent('Onboarding V2 Complete', {
logEvent('Onboarding V2 Complete', {
module: selectedModule.id,
dataSource: selectedDataSource?.id,
framework: selectedFramework,
@@ -186,14 +184,14 @@ export default function ModuleStepsContainer({
// on next step click track events
switch (selectedModuleSteps[current].id) {
case stepsMap.dataSource:
trackEvent('Onboarding V2: Data Source Selected', {
logEvent('Onboarding V2: Data Source Selected', {
dataSource: selectedDataSource?.id,
framework: selectedFramework,
module: activeStep?.module?.id,
});
break;
case stepsMap.environmentDetails:
trackEvent('Onboarding V2: Environment Selected', {
logEvent('Onboarding V2: Environment Selected', {
dataSource: selectedDataSource?.id,
framework: selectedFramework,
environment: selectedEnvironment,
@@ -201,7 +199,7 @@ export default function ModuleStepsContainer({
});
break;
case stepsMap.selectMethod:
trackEvent('Onboarding V2: Method Selected', {
logEvent('Onboarding V2: Method Selected', {
dataSource: selectedDataSource?.id,
framework: selectedFramework,
environment: selectedEnvironment,
@@ -211,7 +209,7 @@ export default function ModuleStepsContainer({
break;
case stepsMap.setupOtelCollector:
trackEvent('Onboarding V2: Setup Otel Collector', {
logEvent('Onboarding V2: Setup Otel Collector', {
dataSource: selectedDataSource?.id,
framework: selectedFramework,
environment: selectedEnvironment,
@@ -220,7 +218,7 @@ export default function ModuleStepsContainer({
});
break;
case stepsMap.instrumentApplication:
trackEvent('Onboarding V2: Instrument Application', {
logEvent('Onboarding V2: Instrument Application', {
dataSource: selectedDataSource?.id,
framework: selectedFramework,
environment: selectedEnvironment,
@@ -229,13 +227,13 @@ export default function ModuleStepsContainer({
});
break;
case stepsMap.cloneRepository:
trackEvent('Onboarding V2: Clone Repository', {
logEvent('Onboarding V2: Clone Repository', {
dataSource: selectedDataSource?.id,
module: activeStep?.module?.id,
});
break;
case stepsMap.runApplication:
trackEvent('Onboarding V2: Run Application', {
logEvent('Onboarding V2: Run Application', {
dataSource: selectedDataSource?.id,
framework: selectedFramework,
environment: selectedEnvironment,
@@ -244,95 +242,95 @@ export default function ModuleStepsContainer({
});
break;
case stepsMap.addHttpDrain:
trackEvent('Onboarding V2: Add HTTP Drain', {
logEvent('Onboarding V2: Add HTTP Drain', {
dataSource: selectedDataSource?.id,
module: activeStep?.module?.id,
});
break;
case stepsMap.startContainer:
trackEvent('Onboarding V2: Start Container', {
logEvent('Onboarding V2: Start Container', {
dataSource: selectedDataSource?.id,
module: activeStep?.module?.id,
});
break;
case stepsMap.setupLogDrains:
trackEvent('Onboarding V2: Setup Log Drains', {
logEvent('Onboarding V2: Setup Log Drains', {
dataSource: selectedDataSource?.id,
module: activeStep?.module?.id,
});
break;
case stepsMap.configureReceiver:
trackEvent('Onboarding V2: Configure Receiver', {
logEvent('Onboarding V2: Configure Receiver', {
dataSource: selectedDataSource?.id,
environment: selectedEnvironment,
module: activeStep?.module?.id,
});
break;
case stepsMap.configureAws:
trackEvent('Onboarding V2: Configure AWS', {
logEvent('Onboarding V2: Configure AWS', {
dataSource: selectedDataSource?.id,
environment: selectedEnvironment,
module: activeStep?.module?.id,
});
break;
case stepsMap.sendLogsCloudwatch:
trackEvent('Onboarding V2: Send Logs Cloudwatch', {
logEvent('Onboarding V2: Send Logs Cloudwatch', {
dataSource: selectedDataSource?.id,
environment: selectedEnvironment,
module: activeStep?.module?.id,
});
break;
case stepsMap.setupDaemonService:
trackEvent('Onboarding V2: Setup ECS Daemon Service', {
logEvent('Onboarding V2: Setup ECS Daemon Service', {
dataSource: selectedDataSource?.id,
environment: selectedEnvironment,
module: activeStep?.module?.id,
});
break;
case stepsMap.createOtelConfig:
trackEvent('Onboarding V2: Create ECS OTel Config', {
logEvent('Onboarding V2: Create ECS OTel Config', {
dataSource: selectedDataSource?.id,
environment: selectedEnvironment,
module: activeStep?.module?.id,
});
break;
case stepsMap.createDaemonService:
trackEvent('Onboarding V2: Create ECS Daemon Service', {
logEvent('Onboarding V2: Create ECS Daemon Service', {
dataSource: selectedDataSource?.id,
environment: selectedEnvironment,
module: activeStep?.module?.id,
});
break;
case stepsMap.ecsSendData:
trackEvent('Onboarding V2: ECS send traces data', {
logEvent('Onboarding V2: ECS send traces data', {
dataSource: selectedDataSource?.id,
environment: selectedEnvironment,
module: activeStep?.module?.id,
});
break;
case stepsMap.createSidecarCollectorContainer:
trackEvent('Onboarding V2: ECS create Sidecar Container', {
logEvent('Onboarding V2: ECS create Sidecar Container', {
dataSource: selectedDataSource?.id,
environment: selectedEnvironment,
module: activeStep?.module?.id,
});
break;
case stepsMap.deployTaskDefinition:
trackEvent('Onboarding V2: ECS deploy task definition', {
logEvent('Onboarding V2: ECS deploy task definition', {
dataSource: selectedDataSource?.id,
environment: selectedEnvironment,
module: activeStep?.module?.id,
});
break;
case stepsMap.ecsSendLogsData:
trackEvent('Onboarding V2: ECS Fargate send logs data', {
logEvent('Onboarding V2: ECS Fargate send logs data', {
dataSource: selectedDataSource?.id,
environment: selectedEnvironment,
module: activeStep?.module?.id,
});
break;
case stepsMap.monitorDashboard:
trackEvent('Onboarding V2: EKS monitor dashboard', {
logEvent('Onboarding V2: EKS monitor dashboard', {
dataSource: selectedDataSource?.id,
environment: selectedEnvironment,
module: activeStep?.module?.id,

View File

@@ -0,0 +1,31 @@
import { render } from 'tests/test-utils';
import { Widgets } from 'types/api/dashboard/getAll';
import TablePanelWrapper from '../TablePanelWrapper';
import {
tablePanelQueryResponse,
tablePanelWidgetQuery,
} from './tablePanelWrapperHelper';
describe('Table panel wrappper tests', () => {
it('table should render fine with the query response and column units', () => {
const { container, getByText } = render(
<TablePanelWrapper
widget={(tablePanelWidgetQuery as unknown) as Widgets}
queryResponse={(tablePanelQueryResponse as unknown) as any}
onDragSelect={(): void => {}}
/>,
);
// checking the overall rendering of the table
expect(container).toMatchSnapshot();
// the first row of the table should have the latency value with units
expect(getByText('4.35 s')).toBeInTheDocument();
// the rows should have optimised value for human readability
expect(getByText('31.3 ms')).toBeInTheDocument();
// the applied legend should appear as the column header
expect(getByText('latency-per-service')).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,38 @@
import { render } from 'tests/test-utils';
import { Widgets } from 'types/api/dashboard/getAll';
import ValuePanelWrapper from '../ValuePanelWrapper';
import {
thresholds,
valuePanelQueryResponse,
valuePanelWidget,
} from './valuePanelWrapperHelper';
describe('Value panel wrappper tests', () => {
it('should render value panel correctly with yaxis unit', () => {
const { getByText } = render(
<ValuePanelWrapper
widget={(valuePanelWidget as unknown) as Widgets}
queryResponse={(valuePanelQueryResponse as unknown) as any}
onDragSelect={(): void => {}}
/>,
);
// selected y axis unit as miliseconds (ms)
expect(getByText('295 ms')).toBeInTheDocument();
});
it('should render tooltip when there are conflicting thresholds', () => {
const { getByTestId, container } = render(
<ValuePanelWrapper
widget={({ ...valuePanelWidget, thresholds } as unknown) as Widgets}
queryResponse={(valuePanelQueryResponse as unknown) as any}
onDragSelect={(): void => {}}
/>,
);
expect(getByTestId('conflicting-thresholds')).toBeInTheDocument();
// added snapshot test here for checking the thresholds color being applied properly
expect(container).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,389 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Table panel wrappper tests table should render fine with the query response and column units 1`] = `
.c1 {
position: absolute;
right: -0.313rem;
bottom: 0;
z-index: 1;
width: 0.625rem;
height: 100%;
cursor: col-resize;
}
.c0 {
height: 95%;
overflow: hidden;
}
.c0 .ant-table-wrapper {
height: 100%;
}
.c0 .ant-spin-nested-loading {
height: 100%;
}
.c0 .ant-spin-container {
height: 100%;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
}
.c0 .ant-table {
-webkit-flex: 1;
-ms-flex: 1;
flex: 1;
overflow: auto;
}
.c0 .ant-table > .ant-table-container > .ant-table-content > table {
min-width: 99% !important;
}
<div>
<div
class="c0"
>
<div
class="query-table"
>
<div
class="ant-table-wrapper css-dev-only-do-not-override-2i2tap"
>
<div
class="ant-spin-nested-loading css-dev-only-do-not-override-2i2tap"
>
<div
class="ant-spin-container"
>
<div
class="ant-table ant-table-small ant-table-layout-fixed ant-table-scroll-horizontal"
>
<div
class="ant-table-container"
>
<div
class="ant-table-content"
style="overflow-x: auto; overflow-y: hidden;"
>
<table
style="width: auto; min-width: 100%; table-layout: fixed;"
>
<colgroup>
<col
style="width: 145px;"
/>
<col
style="width: 145px;"
/>
</colgroup>
<thead
class="ant-table-thead"
>
<tr>
<th
aria-label="service_name"
class="ant-table-cell ant-table-column-has-sorters react-resizable"
scope="col"
tabindex="0"
>
<div
class="ant-table-column-sorters"
>
<span
class="ant-table-column-title"
>
service_name
</span>
<span
class="ant-table-column-sorter ant-table-column-sorter-full"
>
<span
aria-hidden="true"
class="ant-table-column-sorter-inner"
>
<span
aria-label="caret-up"
class="anticon anticon-caret-up ant-table-column-sorter-up"
role="img"
>
<svg
aria-hidden="true"
data-icon="caret-up"
fill="currentColor"
focusable="false"
height="1em"
viewBox="0 0 1024 1024"
width="1em"
>
<path
d="M858.9 689L530.5 308.2c-9.4-10.9-27.5-10.9-37 0L165.1 689c-12.2 14.2-1.2 35 18.5 35h656.8c19.7 0 30.7-20.8 18.5-35z"
/>
</svg>
</span>
<span
aria-label="caret-down"
class="anticon anticon-caret-down ant-table-column-sorter-down"
role="img"
>
<svg
aria-hidden="true"
data-icon="caret-down"
fill="currentColor"
focusable="false"
height="1em"
viewBox="0 0 1024 1024"
width="1em"
>
<path
d="M840.4 300H183.6c-19.7 0-30.7 20.8-18.5 35l328.4 380.8c9.4 10.9 27.5 10.9 37 0L858.9 335c12.2-14.2 1.2-35-18.5-35z"
/>
</svg>
</span>
</span>
</span>
</div>
<span
class="c1 react-resizable-handle"
/>
</th>
<th
aria-label="latency-per-service"
class="ant-table-cell ant-table-column-has-sorters react-resizable"
scope="col"
tabindex="0"
>
<div
class="ant-table-column-sorters"
>
<span
class="ant-table-column-title"
>
latency-per-service
</span>
<span
class="ant-table-column-sorter ant-table-column-sorter-full"
>
<span
aria-hidden="true"
class="ant-table-column-sorter-inner"
>
<span
aria-label="caret-up"
class="anticon anticon-caret-up ant-table-column-sorter-up"
role="img"
>
<svg
aria-hidden="true"
data-icon="caret-up"
fill="currentColor"
focusable="false"
height="1em"
viewBox="0 0 1024 1024"
width="1em"
>
<path
d="M858.9 689L530.5 308.2c-9.4-10.9-27.5-10.9-37 0L165.1 689c-12.2 14.2-1.2 35 18.5 35h656.8c19.7 0 30.7-20.8 18.5-35z"
/>
</svg>
</span>
<span
aria-label="caret-down"
class="anticon anticon-caret-down ant-table-column-sorter-down"
role="img"
>
<svg
aria-hidden="true"
data-icon="caret-down"
fill="currentColor"
focusable="false"
height="1em"
viewBox="0 0 1024 1024"
width="1em"
>
<path
d="M840.4 300H183.6c-19.7 0-30.7 20.8-18.5 35l328.4 380.8c9.4 10.9 27.5 10.9 37 0L858.9 335c12.2-14.2 1.2-35-18.5-35z"
/>
</svg>
</span>
</span>
</span>
</div>
<span
class="c1 react-resizable-handle"
/>
</th>
</tr>
</thead>
<tbody
class="ant-table-tbody"
>
<tr
aria-hidden="true"
class="ant-table-measure-row"
style="height: 0px; font-size: 0px;"
>
<td
style="padding: 0px; border: 0px; height: 0px;"
>
<div
style="height: 0px; overflow: hidden;"
>
 
</div>
</td>
<td
style="padding: 0px; border: 0px; height: 0px;"
>
<div
style="height: 0px; overflow: hidden;"
>
 
</div>
</td>
</tr>
<tr
class="ant-table-row ant-table-row-level-0"
>
<td
class="ant-table-cell"
>
<div>
demo-app
</div>
</td>
<td
class="ant-table-cell"
>
<div>
4.35 s
</div>
</td>
</tr>
<tr
class="ant-table-row ant-table-row-level-0"
>
<td
class="ant-table-cell"
>
<div>
customer
</div>
</td>
<td
class="ant-table-cell"
>
<div>
431 ms
</div>
</td>
</tr>
<tr
class="ant-table-row ant-table-row-level-0"
>
<td
class="ant-table-cell"
>
<div>
mysql
</div>
</td>
<td
class="ant-table-cell"
>
<div>
431 ms
</div>
</td>
</tr>
<tr
class="ant-table-row ant-table-row-level-0"
>
<td
class="ant-table-cell"
>
<div>
frontend
</div>
</td>
<td
class="ant-table-cell"
>
<div>
287 ms
</div>
</td>
</tr>
<tr
class="ant-table-row ant-table-row-level-0"
>
<td
class="ant-table-cell"
>
<div>
driver
</div>
</td>
<td
class="ant-table-cell"
>
<div>
230 ms
</div>
</td>
</tr>
<tr
class="ant-table-row ant-table-row-level-0"
>
<td
class="ant-table-cell"
>
<div>
route
</div>
</td>
<td
class="ant-table-cell"
>
<div>
66.4 ms
</div>
</td>
</tr>
<tr
class="ant-table-row ant-table-row-level-0"
>
<td
class="ant-table-cell"
>
<div>
redis
</div>
</td>
<td
class="ant-table-cell"
>
<div>
31.3 ms
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;

View File

@@ -0,0 +1,76 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Value panel wrappper tests should render tooltip when there are conflicting thresholds 1`] = `
.c1 {
height: 100%;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
}
.c0 {
text-align: center;
padding-top: 1rem;
}
<div>
<div
class="c0"
>
<article
class="ant-typography css-dev-only-do-not-override-2i2tap"
/>
</div>
<div
class="c1"
>
<div
class="value-graph-container"
>
<span
class="ant-typography value-graph-text css-dev-only-do-not-override-2i2tap"
style="color: Blue;"
>
295 ms
</span>
<div
class="value-graph-textconflict"
>
<span
aria-label="exclamation-circle"
class="anticon anticon-exclamation-circle value-graph-icon"
data-testid="conflicting-thresholds"
role="img"
>
<svg
aria-hidden="true"
data-icon="exclamation-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm-32 232c0-4.4 3.6-8 8-8h48c4.4 0 8 3.6 8 8v272c0 4.4-3.6 8-8 8h-48c-4.4 0-8-3.6-8-8V296zm32 440a48.01 48.01 0 010-96 48.01 48.01 0 010 96z"
/>
</svg>
</span>
</div>
</div>
</div>
</div>
`;

View File

@@ -0,0 +1,286 @@
export const tablePanelWidgetQuery = {
id: '727533b0-7718-4f99-a1db-a1875649325c',
title: '',
description: '',
isStacked: false,
nullZeroValues: 'zero',
opacity: '1',
panelTypes: 'table',
query: {
clickhouse_sql: [
{
name: 'A',
legend: '',
disabled: false,
query: '',
},
],
promql: [
{
name: 'A',
query: '',
legend: '',
disabled: false,
},
],
builder: {
queryData: [
{
dataSource: 'metrics',
queryName: 'A',
aggregateOperator: 'count',
aggregateAttribute: {
key: 'signoz_latency',
dataType: 'float64',
type: 'ExponentialHistogram',
isColumn: true,
isJSON: false,
id: 'signoz_latency--float64--ExponentialHistogram--true',
},
timeAggregation: '',
spaceAggregation: 'p90',
functions: [],
filters: {
items: [],
op: 'AND',
},
expression: 'A',
disabled: false,
stepInterval: 60,
having: [],
limit: null,
orderBy: [],
groupBy: [
{
key: 'service_name',
dataType: 'string',
type: 'tag',
isColumn: false,
isJSON: false,
id: 'service_name--string--tag--false',
},
],
legend: 'latency-per-service',
reduceTo: 'avg',
},
],
queryFormulas: [],
},
id: '7feafec2-a450-4b5a-8897-260c1a9fe1e4',
queryType: 'builder',
},
timePreferance: 'GLOBAL_TIME',
softMax: null,
softMin: null,
selectedLogFields: [
{
dataType: 'string',
type: '',
name: 'body',
},
{
dataType: 'string',
type: '',
name: 'timestamp',
},
],
selectedTracesFields: [
{
key: 'serviceName',
dataType: 'string',
type: 'tag',
isColumn: true,
isJSON: false,
id: 'serviceName--string--tag--true',
},
{
key: 'name',
dataType: 'string',
type: 'tag',
isColumn: true,
isJSON: false,
id: 'name--string--tag--true',
},
{
key: 'durationNano',
dataType: 'float64',
type: 'tag',
isColumn: true,
isJSON: false,
id: 'durationNano--float64--tag--true',
},
{
key: 'httpMethod',
dataType: 'string',
type: 'tag',
isColumn: true,
isJSON: false,
id: 'httpMethod--string--tag--true',
},
{
key: 'responseStatusCode',
dataType: 'string',
type: 'tag',
isColumn: true,
isJSON: false,
id: 'responseStatusCode--string--tag--true',
},
],
yAxisUnit: 'none',
thresholds: [],
fillSpans: false,
columnUnits: {
A: 'ms',
},
bucketCount: 30,
stackedBarChart: false,
bucketWidth: 0,
mergeAllActiveQueries: false,
};
export const tablePanelQueryResponse = {
status: 'success',
isLoading: false,
isSuccess: true,
isError: false,
isIdle: false,
data: {
statusCode: 200,
error: null,
message: 'success',
payload: {
status: 'success',
data: {
resultType: '',
result: [
{
table: {
columns: [
{
name: 'service_name',
queryName: '',
isValueColumn: false,
},
{
name: 'A',
queryName: 'A',
isValueColumn: true,
},
],
rows: [
{
data: {
A: 4353.81,
service_name: 'demo-app',
},
},
{
data: {
A: 431.25,
service_name: 'customer',
},
},
{
data: {
A: 431.25,
service_name: 'mysql',
},
},
{
data: {
A: 287.11,
service_name: 'frontend',
},
},
{
data: {
A: 230.02,
service_name: 'driver',
},
},
{
data: {
A: 66.37,
service_name: 'route',
},
},
{
data: {
A: 31.3,
service_name: 'redis',
},
},
],
},
},
],
},
},
params: {
start: 1721207225000,
end: 1721207525000,
step: 60,
variables: {},
formatForWeb: true,
compositeQuery: {
queryType: 'builder',
panelType: 'table',
fillGaps: false,
builderQueries: {
A: {
dataSource: 'metrics',
queryName: 'A',
aggregateOperator: 'count',
aggregateAttribute: {
key: 'signoz_latency',
dataType: 'float64',
type: 'ExponentialHistogram',
isColumn: true,
isJSON: false,
id: 'signoz_latency--float64--ExponentialHistogram--true',
},
timeAggregation: '',
spaceAggregation: 'p90',
functions: [],
filters: {
items: [],
op: 'AND',
},
expression: 'A',
disabled: false,
stepInterval: 60,
having: [],
limit: null,
orderBy: [],
groupBy: [
{
key: 'service_name',
dataType: 'string',
type: 'tag',
isColumn: false,
isJSON: false,
id: 'service_name--string--tag--false',
},
],
legend: '',
reduceTo: 'avg',
},
},
},
},
},
dataUpdatedAt: 1721207526018,
error: null,
errorUpdatedAt: 0,
failureCount: 0,
errorUpdateCount: 0,
isFetched: true,
isFetchedAfterMount: true,
isFetching: false,
isRefetching: false,
isLoadingError: false,
isPlaceholderData: false,
isPreviousData: false,
isRefetchError: false,
isStale: true,
};

View File

@@ -0,0 +1,267 @@
export const valuePanelWidget = {
id: 'b8b93086-ef01-47bf-9044-1e7abd583be4',
title: 'signoz latency in ms',
description: '',
isStacked: false,
nullZeroValues: 'zero',
opacity: '1',
panelTypes: 'value',
query: {
clickhouse_sql: [
{
name: 'A',
legend: '',
disabled: false,
query: '',
},
],
promql: [
{
name: 'A',
query: '',
legend: '',
disabled: false,
},
],
builder: {
queryData: [
{
dataSource: 'metrics',
queryName: 'A',
aggregateOperator: 'count',
aggregateAttribute: {
key: 'signoz_latency',
dataType: 'float64',
type: 'ExponentialHistogram',
isColumn: true,
isJSON: false,
id: 'signoz_latency--float64--ExponentialHistogram--true',
},
timeAggregation: '',
spaceAggregation: 'p90',
functions: [],
filters: {
items: [],
op: 'AND',
},
expression: 'A',
disabled: false,
stepInterval: 60,
having: [],
limit: null,
orderBy: [],
groupBy: [],
legend: '',
reduceTo: 'avg',
},
],
queryFormulas: [],
},
id: '3bec289c-49c3-4d7e-98bb-84d47c79909c',
queryType: 'builder',
},
timePreferance: 'GLOBAL_TIME',
softMax: null,
softMin: null,
selectedLogFields: [
{
dataType: 'string',
type: '',
name: 'body',
},
{
dataType: 'string',
type: '',
name: 'timestamp',
},
],
selectedTracesFields: [
{
key: 'serviceName',
dataType: 'string',
type: 'tag',
isColumn: true,
isJSON: false,
id: 'serviceName--string--tag--true',
},
{
key: 'name',
dataType: 'string',
type: 'tag',
isColumn: true,
isJSON: false,
id: 'name--string--tag--true',
},
{
key: 'durationNano',
dataType: 'float64',
type: 'tag',
isColumn: true,
isJSON: false,
id: 'durationNano--float64--tag--true',
},
{
key: 'httpMethod',
dataType: 'string',
type: 'tag',
isColumn: true,
isJSON: false,
id: 'httpMethod--string--tag--true',
},
{
key: 'responseStatusCode',
dataType: 'string',
type: 'tag',
isColumn: true,
isJSON: false,
id: 'responseStatusCode--string--tag--true',
},
],
yAxisUnit: 'ms',
thresholds: [],
fillSpans: false,
columnUnits: {},
bucketCount: 30,
stackedBarChart: false,
bucketWidth: 0,
mergeAllActiveQueries: false,
};
export const thresholds = [
{
index: '8eb16a3a-b4f1-47c8-943a-4b1786884583',
isEditEnabled: false,
thresholdColor: 'Blue',
thresholdFormat: 'Text',
thresholdOperator: '>',
thresholdUnit: 'none',
thresholdValue: 100,
keyIndex: 1,
selectedGraph: 'value',
thresholdTableOptions: '',
thresholdLabel: '',
},
{
index: 'eb9c1186-ad7d-42dd-8e7f-3913a321d7cf',
isEditEnabled: false,
thresholdColor: 'Red',
thresholdFormat: 'Text',
thresholdOperator: '>',
thresholdUnit: 'none',
thresholdValue: 0,
keyIndex: 0,
selectedGraph: 'value',
thresholdTableOptions: '',
thresholdLabel: '',
},
];
export const valuePanelQueryResponse = {
status: 'success',
isLoading: false,
isSuccess: true,
isError: false,
isIdle: false,
data: {
statusCode: 200,
error: null,
message: 'success',
payload: {
data: {
result: [
{
metric: {
A: 'A',
},
values: [[0, '295.4299833508185']],
queryName: 'A',
legend: 'A',
},
],
resultType: '',
newResult: {
status: 'success',
data: {
resultType: '',
result: [
{
queryName: 'A',
series: [
{
labels: {
A: 'A',
},
labelsArray: null,
values: [
{
timestamp: 0,
value: '295.4299833508185',
},
],
},
],
},
],
},
},
},
},
params: {
start: 1721203451000,
end: 1721203751000,
step: 60,
variables: {},
formatForWeb: false,
compositeQuery: {
queryType: 'builder',
panelType: 'value',
fillGaps: false,
builderQueries: {
A: {
dataSource: 'metrics',
queryName: 'A',
aggregateOperator: 'count',
aggregateAttribute: {
key: 'signoz_latency',
dataType: 'float64',
type: 'ExponentialHistogram',
isColumn: true,
isJSON: false,
id: 'signoz_latency--float64--ExponentialHistogram--true',
},
timeAggregation: '',
spaceAggregation: 'p90',
functions: [],
filters: {
items: [],
op: 'AND',
},
expression: 'A',
disabled: false,
stepInterval: 60,
having: [],
limit: null,
orderBy: [],
groupBy: [],
legend: '',
reduceTo: 'avg',
},
},
},
},
},
dataUpdatedAt: 1721203751775,
error: null,
errorUpdatedAt: 0,
failureCount: 0,
errorUpdateCount: 0,
isFetched: true,
isFetchedAfterMount: true,
isFetching: false,
isRefetching: false,
isLoadingError: false,
isPlaceholderData: false,
isPreviousData: false,
isRefetchError: false,
isStale: true,
};

View File

@@ -1,6 +1,6 @@
import { EditFilled, PlusOutlined } from '@ant-design/icons';
import logEvent from 'api/common/logEvent';
import TextToolTip from 'components/TextToolTip';
import useAnalytics from 'hooks/analytics/useAnalytics';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { ActionMode, ActionType, Pipeline } from 'types/api/pipeline/def';
@@ -15,7 +15,6 @@ function CreatePipelineButton({
pipelineData,
}: CreatePipelineButtonProps): JSX.Element {
const { t } = useTranslation(['pipeline']);
const { trackEvent } = useAnalytics();
const isAddNewPipelineVisible = useMemo(
() => checkDataLength(pipelineData?.pipelines),
@@ -26,7 +25,7 @@ function CreatePipelineButton({
const onEnterEditMode = (): void => {
setActionMode(ActionMode.Editing);
trackEvent('Logs: Pipelines: Entered Edit Mode', {
logEvent('Logs: Pipelines: Entered Edit Mode', {
source: 'signoz-ui',
});
};
@@ -34,7 +33,7 @@ function CreatePipelineButton({
setActionMode(ActionMode.Editing);
setActionType(ActionType.AddPipeline);
trackEvent('Logs: Pipelines: Clicked Add New Pipeline', {
logEvent('Logs: Pipelines: Clicked Add New Pipeline', {
source: 'signoz-ui',
});
};

View File

@@ -1,6 +1,6 @@
import { PlusCircleOutlined } from '@ant-design/icons';
import { TableLocale } from 'antd/es/table/interface';
import useAnalytics from 'hooks/analytics/useAnalytics';
import logEvent from 'api/common/logEvent';
import { useIsDarkMode } from 'hooks/useDarkMode';
import React, { useCallback, useMemo } from 'react';
import { DndProvider } from 'react-dnd';
@@ -39,7 +39,6 @@ function PipelineExpandView({
}: PipelineExpandViewProps): JSX.Element {
const { t } = useTranslation(['pipeline']);
const isDarkMode = useIsDarkMode();
const { trackEvent } = useAnalytics();
const isEditingActionMode = isActionMode === ActionMode.Editing;
const deleteProcessorHandler = useCallback(
@@ -192,7 +191,7 @@ function PipelineExpandView({
const addNewProcessorHandler = useCallback((): void => {
setActionType(ActionType.AddProcessor);
trackEvent('Logs: Pipelines: Clicked Add New Processor', {
logEvent('Logs: Pipelines: Clicked Add New Processor', {
source: 'signoz-ui',
});
// eslint-disable-next-line react-hooks/exhaustive-deps

View File

@@ -3,11 +3,18 @@ import './styles.scss';
import { ExclamationCircleOutlined, PlusOutlined } from '@ant-design/icons';
import { Card, Modal, Table, Typography } from 'antd';
import { ExpandableConfig } from 'antd/es/table/interface';
import logEvent from 'api/common/logEvent';
import savePipeline from 'api/pipeline/post';
import useAnalytics from 'hooks/analytics/useAnalytics';
import { useNotifications } from 'hooks/useNotifications';
import { isUndefined } from 'lodash-es';
import cloneDeep from 'lodash-es/cloneDeep';
import React, { useCallback, useMemo, useState } from 'react';
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { useTranslation } from 'react-i18next';
@@ -92,7 +99,6 @@ function PipelineListsView({
const [modal, contextHolder] = Modal.useModal();
const { notifications } = useNotifications();
const [pipelineSearchValue, setPipelineSearchValue] = useState<string>('');
const { trackEvent } = useAnalytics();
const [prevPipelineData, setPrevPipelineData] = useState<Array<PipelineData>>(
cloneDeep(pipelineData?.pipelines || []),
);
@@ -368,7 +374,7 @@ function PipelineListsView({
const addNewPipelineHandler = useCallback((): void => {
setActionType(ActionType.AddPipeline);
trackEvent('Logs: Pipelines: Clicked Add New Pipeline', {
logEvent('Logs: Pipelines: Clicked Add New Pipeline', {
source: 'signoz-ui',
});
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -407,7 +413,7 @@ function PipelineListsView({
setCurrPipelineData(pipelinesInDB);
setPrevPipelineData(pipelinesInDB);
trackEvent('Logs: Pipelines: Saved Pipelines', {
logEvent('Logs: Pipelines: Saved Pipelines', {
count: pipelinesInDB.length,
enabled: pipelinesInDB.filter((p) => p.enabled).length,
source: 'signoz-ui',
@@ -466,6 +472,16 @@ function PipelineListsView({
getExpandIcon(expanded, onExpand, record),
};
const logEventCalledRef = useRef(false);
useEffect(() => {
if (!logEventCalledRef.current && !isUndefined(currPipelineData)) {
logEvent('Logs Pipelines: List page visited', {
number: currPipelineData?.length,
});
logEventCalledRef.current = true;
}
}, [currPipelineData]);
return (
<>
{contextHolder}

View File

@@ -1,15 +1,13 @@
import { EyeFilled } from '@ant-design/icons';
import { Divider, Modal } from 'antd';
import logEvent from 'api/common/logEvent';
import PipelineProcessingPreview from 'container/PipelinePage/PipelineListsView/Preview/PipelineProcessingPreview';
import useAnalytics from 'hooks/analytics/useAnalytics';
import { useState } from 'react';
import { PipelineData } from 'types/api/pipeline/def';
import { iconStyle } from '../../../config';
function PreviewAction({ pipeline }: PreviewActionProps): JSX.Element | null {
const { trackEvent } = useAnalytics();
const [previewKey, setPreviewKey] = useState<string | null>(null);
const isModalOpen = Boolean(previewKey);
@@ -23,7 +21,7 @@ function PreviewAction({ pipeline }: PreviewActionProps): JSX.Element | null {
const onOpenPreview = (): void => {
openModal();
trackEvent('Logs: Pipelines: Clicked Preview Pipeline', {
logEvent('Logs: Pipelines: Clicked Preview Pipeline', {
source: 'signoz-ui',
});
};

View File

@@ -1,5 +1,6 @@
import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import logEvent from 'api/common/logEvent';
import { I18nextProvider } from 'react-i18next';
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
@@ -9,14 +10,7 @@ import store from 'store';
import CreatePipelineButton from '../Layouts/Pipeline/CreatePipelineButton';
import { pipelineApiResponseMockData } from '../mocks/pipeline';
const trackEventVar = jest.fn();
jest.mock('hooks/analytics/useAnalytics', () => ({
__esModule: true,
default: jest.fn().mockImplementation(() => ({
trackEvent: trackEventVar,
trackPageView: jest.fn(),
})),
}));
jest.mock('api/common/logEvent');
describe('PipelinePage container test', () => {
it('should render CreatePipelineButton section', async () => {
@@ -58,7 +52,7 @@ describe('PipelinePage container test', () => {
expect(editButton).toBeInTheDocument();
await userEvent.click(editButton);
expect(trackEventVar).toBeCalledWith('Logs: Pipelines: Entered Edit Mode', {
expect(logEvent).toBeCalledWith('Logs: Pipelines: Entered Edit Mode', {
source: 'signoz-ui',
});
});
@@ -83,11 +77,8 @@ describe('PipelinePage container test', () => {
expect(editButton).toBeInTheDocument();
await userEvent.click(editButton);
expect(trackEventVar).toBeCalledWith(
'Logs: Pipelines: Clicked Add New Pipeline',
{
source: 'signoz-ui',
},
);
expect(logEvent).toBeCalledWith('Logs: Pipelines: Clicked Add New Pipeline', {
source: 'signoz-ui',
});
});
});

View File

@@ -191,6 +191,7 @@ export const GroupByFilter = memo(function GroupByFilter({
labelInValue
notFoundContent={isFetching ? <Spin size="small" /> : null}
onChange={handleChange}
data-testid="group-by"
/>
);
});

View File

@@ -27,6 +27,7 @@ export const ReduceToFilter = memo(function ReduceToFilter({
style={{ width: '100%' }}
options={REDUCE_TO_VALUES}
value={currentValue}
data-testid="reduce-to"
labelInValue
onChange={handleChange}
/>

View File

@@ -1,11 +1,13 @@
import localStorageGet from 'api/browser/localstorage/get';
import localStorageSet from 'api/browser/localstorage/set';
import logEvent from 'api/common/logEvent';
import { SKIP_ONBOARDING } from 'constants/onboarding';
import useErrorNotification from 'hooks/useErrorNotification';
import { useQueryService } from 'hooks/useQueryService';
import useResourceAttribute from 'hooks/useResourceAttribute';
import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils';
import { useMemo, useState } from 'react';
import { isUndefined } from 'lodash-es';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
@@ -45,6 +47,26 @@ function ServiceTraces(): JSX.Element {
setSkipOnboarding(true);
};
const logEventCalledRef = useRef(false);
useEffect(() => {
if (!logEventCalledRef.current && !isUndefined(data)) {
const selectedEnvironments = queries.find(
(val) => val.tagKey === 'resource_deployment_environment',
)?.tagValue;
const rps = data.reduce((total, service) => total + service.callRate, 0);
logEvent('APM: List page visited', {
numberOfServices: data?.length,
selectedEnvironments,
resourceAttributeUsed: !!queries?.length,
rps,
});
logEventCalledRef.current = true;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data]);
if (
services.length === 0 &&
isLoading === false &&

View File

@@ -4,6 +4,7 @@ import './SideNav.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Button, Tooltip } from 'antd';
import logEvent from 'api/common/logEvent';
import cx from 'classnames';
import { FeatureKeys } from 'constants/features';
import ROUTES from 'constants/routes';
@@ -179,6 +180,11 @@ function SideNav({
};
const onClickShortcuts = (e: MouseEvent): void => {
// eslint-disable-next-line sonarjs/no-duplicate-string
logEvent('Sidebar: Menu clicked', {
menuRoute: '/shortcuts',
menuLabel: 'Keyboard Shortcuts',
});
if (isCtrlMetaKey(e)) {
openInNewTab('/shortcuts');
} else {
@@ -187,6 +193,10 @@ function SideNav({
};
const onClickGetStarted = (event: MouseEvent): void => {
logEvent('Sidebar: Menu clicked', {
menuRoute: '/get-started',
menuLabel: 'Get Started',
});
if (isCtrlMetaKey(event)) {
openInNewTab('/get-started');
} else {
@@ -313,6 +323,10 @@ function SideNav({
} else if (item) {
onClickHandler(item?.key as string, event);
}
logEvent('Sidebar: Menu clicked', {
menuRoute: item?.key,
menuLabel: item?.label,
});
};
useEffect(() => {
@@ -440,6 +454,10 @@ function SideNav({
isActive={activeMenuKey === item?.key}
onClick={(event: MouseEvent): void => {
handleUserManagentMenuItemClick(item?.key as string, event);
logEvent('Sidebar: Menu clicked', {
menuRoute: item?.key,
menuLabel: item?.label,
});
}}
/>
),
@@ -456,6 +474,10 @@ function SideNav({
} else {
history.push(`${inviteMemberMenuItem.key}`);
}
logEvent('Sidebar: Menu clicked', {
menuRoute: inviteMemberMenuItem?.key,
menuLabel: inviteMemberMenuItem?.label,
});
}}
/>
)}
@@ -470,6 +492,10 @@ function SideNav({
userSettingsMenuItem?.key as string,
event,
);
logEvent('Sidebar: Menu clicked', {
menuRoute: userSettingsMenuItem?.key,
menuLabel: 'User',
});
}}
/>
)}

View File

@@ -155,7 +155,9 @@ function TimeSeriesView({
chartData[0]?.length === 0 &&
!isLoading &&
!isError &&
isFilterApplied && <EmptyLogsSearch />}
isFilterApplied && (
<EmptyLogsSearch dataSource={dataSource} panelType="TIME_SERIES" />
)}
{chartData &&
chartData[0] &&

View File

@@ -1,5 +1,4 @@
import { Typography } from 'antd';
import Card from 'antd/es/card/Card';
import { Card, Typography } from 'antd';
import styled from 'styled-components';
export const Container = styled(Card)`

View File

@@ -1,4 +1,4 @@
import { Button, Modal, Tabs, Typography } from 'antd';
import { Button, Modal, Tabs, Tooltip, Typography } from 'antd';
import Editor from 'components/Editor';
import { StyledSpace } from 'components/Styled';
import { QueryParams } from 'constants/query';
@@ -102,8 +102,7 @@ function SelectedSpanDetails(props: SelectedSpanDetailsProps): JSX.Element {
marginTop: '16px',
}}
>
{' '}
Details for selected Span{' '}
Details for selected Span
</Typography.Text>
<Typography.Text style={{ fontWeight: 700 }}>Service</Typography.Text>
@@ -114,6 +113,30 @@ function SelectedSpanDetails(props: SelectedSpanDetailsProps): JSX.Element {
<Typography>{tree.name}</Typography>
<Typography.Text style={{ fontWeight: 700 }}>SpanKind</Typography.Text>
<Typography>{tree.spanKind}</Typography>
<Typography.Text style={{ fontWeight: 700 }}>
StatusCodeString
</Typography.Text>
<Tooltip placement="left" title={tree.statusCodeString}>
<Typography>{tree.statusCodeString}</Typography>
</Tooltip>
{tree.statusMessage && (
<>
<Typography.Text style={{ fontWeight: 700 }}>
StatusMessage
</Typography.Text>
<Tooltip placement="left" title={tree.statusMessage}>
<Typography>{tree.statusMessage}</Typography>
</Tooltip>
</>
)}
<Button size="small" style={{ marginTop: '8px' }} onClick={onLogsHandler}>
Go to Related logs
</Button>

View File

@@ -1,8 +1,7 @@
import './TraceDetails.styles.scss';
import { FilterOutlined } from '@ant-design/icons';
import { Button, Col, Typography } from 'antd';
import Sider from 'antd/es/layout/Sider';
import { Button, Col, Layout, Typography } from 'antd';
import cx from 'classnames';
import {
StyledCol,
@@ -42,6 +41,8 @@ import {
INTERVAL_UNITS,
} from './utils';
const { Sider } = Layout;
function TraceDetail({ response }: TraceDetailProps): JSX.Element {
const spanServiceColors = useMemo(
() => spanServiceNameToColorMapping(response[0].events),

View File

@@ -13,6 +13,9 @@ describe('traces/getTreeLevelsCount', () => {
children,
serviceName: '',
serviceColour: '',
spanKind: '',
statusCodeString: '',
statusMessage: '',
});
test('should return 0 for empty tree', () => {

View File

@@ -37,6 +37,9 @@ test('loads and displays greeting', () => {
event: [],
hasError: false,
parent: undefined,
spanKind: '',
statusCodeString: '',
statusMessage: '',
},
}}
/>

View File

@@ -156,7 +156,9 @@ function ListView({ isFilterApplied }: ListViewProps): JSX.Element {
<NoLogs dataSource={DataSource.TRACES} />
)}
{isDataPresent && isFilterApplied && <EmptyLogsSearch />}
{isDataPresent && isFilterApplied && (
<EmptyLogsSearch dataSource={DataSource.TRACES} panelType="LIST" />
)}
{!isError && transformedQueryTableData.length !== 0 && (
<ResizeTable

View File

@@ -1,5 +1,4 @@
import { Col } from 'antd';
import Card from 'antd/es/card/Card';
import { Card, Col } from 'antd';
import styled from 'styled-components';
export const Container = styled(Card)`

View File

@@ -105,7 +105,9 @@ function TracesView({ isFilterApplied }: TracesViewProps): JSX.Element {
!isFetching &&
(tableData || []).length === 0 &&
!isError &&
isFilterApplied && <EmptyLogsSearch />}
isFilterApplied && (
<EmptyLogsSearch dataSource={DataSource.TRACES} panelType="TRACE" />
)}
{(tableData || []).length !== 0 && (
<ResizeTable

View File

@@ -1,6 +1,7 @@
import './TracesTableComponent.styles.scss';
import { Table } from 'antd';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import Controls from 'container/Controls';
import { PER_PAGE_OPTIONS } from 'container/TracesExplorer/ListView/configs';
@@ -42,6 +43,7 @@ function TracesTableComponent({
setRequestData((prev) => ({
...prev,
tableParams: {
...prev.tableParams,
pagination,
},
}));
@@ -86,17 +88,19 @@ function TracesTableComponent({
return (
<div className="traces-table">
<div className="resize-table">
<Table
pagination={false}
tableLayout="fixed"
scroll={{ x: true }}
loading={queryResponse.isFetching}
style={tableStyles}
dataSource={transformedQueryTableData}
columns={columns}
onRow={handleRow}
sticky
/>
<OverlayScrollbar>
<Table
pagination={false}
tableLayout="fixed"
scroll={{ x: true }}
loading={queryResponse.isFetching}
style={tableStyles}
dataSource={transformedQueryTableData}
columns={columns}
onRow={handleRow}
sticky
/>
</OverlayScrollbar>
</div>
<div className="controller">
<Controls

View File

@@ -1,8 +1,10 @@
import logEvent from 'api/common/logEvent';
import { getQueryRangeFormat } from 'api/dashboard/queryRangeFormat';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import { MenuItemKeys } from 'container/GridCardLayout/WidgetHeader/contants';
import { useNotifications } from 'hooks/useNotifications';
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
import { prepareQueryRangePayload } from 'lib/dashboard/prepareQueryRangePayload';
@@ -17,7 +19,7 @@ import { Widgets } from 'types/api/dashboard/getAll';
import { GlobalReducer } from 'types/reducer/globalTime';
import { getGraphType } from 'utils/getGraphType';
const useCreateAlerts = (widget?: Widgets): VoidFunction => {
const useCreateAlerts = (widget?: Widgets, caller?: string): VoidFunction => {
const queryRangeMutation = useMutation(getQueryRangeFormat);
const { selectedTime: globalSelectedInterval } = useSelector<
@@ -32,6 +34,24 @@ const useCreateAlerts = (widget?: Widgets): VoidFunction => {
return useCallback(() => {
if (!widget) return;
if (caller === 'panelView') {
logEvent('Panel Edit: Create alert', {
panelType: widget.panelTypes,
dashboardName: selectedDashboard?.data?.title,
dashboardId: selectedDashboard?.uuid,
widgetId: widget.id,
queryType: widget.query.queryType,
});
} else if (caller === 'dashboardView') {
logEvent('Dashboard Detail: Panel action', {
action: MenuItemKeys.CreateAlerts,
panelType: widget.panelTypes,
dashboardName: selectedDashboard?.data?.title,
dashboardId: selectedDashboard?.uuid,
widgetId: widget.id,
queryType: widget.query.queryType,
});
}
const { queryPayload } = prepareQueryRangePayload({
query: widget.query,
globalSelectedInterval,
@@ -41,7 +61,10 @@ const useCreateAlerts = (widget?: Widgets): VoidFunction => {
});
queryRangeMutation.mutate(queryPayload, {
onSuccess: (data) => {
const updatedQuery = mapQueryDataFromApi(data.compositeQuery);
const updatedQuery = mapQueryDataFromApi(
data.compositeQuery,
widget?.query,
);
history.push(
`${ROUTES.ALERTS_NEW}?${QueryParams.compositeQuery}=${encodeURIComponent(
@@ -57,6 +80,7 @@ const useCreateAlerts = (widget?: Widgets): VoidFunction => {
});
},
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
globalSelectedInterval,
notifications,

View File

@@ -0,0 +1,43 @@
import { PartialOptions } from 'overlayscrollbars';
import { useOverlayScrollbars } from 'overlayscrollbars-react';
import {
Dispatch,
RefObject,
SetStateAction,
useEffect,
useRef,
useState,
} from 'react';
const useInitializeOverlayScrollbar = (
options: PartialOptions,
): {
setScroller: Dispatch<SetStateAction<null>>;
rootRef: RefObject<HTMLDivElement>;
} => {
const rootRef = useRef(null);
const [scroller, setScroller] = useState(null);
const [initialize, osInstance] = useOverlayScrollbars({
defer: true,
options,
});
useEffect(() => {
const { current: root } = rootRef;
if (scroller && root) {
initialize({
target: root,
elements: {
viewport: scroller,
},
});
}
return (): void => osInstance()?.destroy();
}, [scroller, initialize, osInstance]);
return { setScroller, rootRef };
};
export default useInitializeOverlayScrollbar;

View File

@@ -16,6 +16,9 @@ const spans: Span[] = [
[''],
[''],
false,
'Server',
'Unset',
'Lorem Ipsum',
],
[
2,
@@ -30,6 +33,9 @@ const spans: Span[] = [
[''],
[''],
false,
'Server2',
'Unset2',
'Lorem Ipsum2',
],
[
3,
@@ -44,6 +50,9 @@ const spans: Span[] = [
[''],
[''],
false,
'Server3',
'Unset3',
'Lorem Ipsum3',
],
];

View File

@@ -0,0 +1,56 @@
import { mapQueryDataFromApi } from '../mapQueryDataFromApi';
import {
compositeQueriesWithFunctions,
compositeQueryWithoutVariables,
compositeQueryWithVariables,
defaultOutput,
outputWithFunctions,
replaceVariables,
stepIntervalUnchanged,
widgetQueriesWithFunctions,
widgetQueryWithoutVariables,
widgetQueryWithVariables,
} from './mapQueryDataFromApiInputs';
jest.mock('uuid', () => ({
v4: (): string => 'test-id',
}));
describe('mapQueryDataFromApi function tests', () => {
it('should not update the step interval when query is passed', () => {
const output = mapQueryDataFromApi(
compositeQueryWithoutVariables,
widgetQueryWithoutVariables,
);
// composite query is the response from the `v3/query_range/format` API call.
// even if the composite query returns stepInterval updated do not modify it
expect(output).toStrictEqual(stepIntervalUnchanged);
});
it('should update filter from the composite query', () => {
const output = mapQueryDataFromApi(
compositeQueryWithVariables,
widgetQueryWithVariables,
);
// replace the variables in the widget query and leave the rest items untouched
expect(output).toStrictEqual(replaceVariables);
});
it('should not update the step intervals with multiple queries and functions', () => {
const output = mapQueryDataFromApi(
compositeQueriesWithFunctions,
widgetQueriesWithFunctions,
);
expect(output).toStrictEqual(outputWithFunctions);
});
it('should use the default query values and the compositeQuery object when query is not passed', () => {
const output = mapQueryDataFromApi(compositeQueryWithoutVariables);
// when the query object is not passed take the initial values and merge the composite query on top of it
expect(output).toStrictEqual(defaultOutput);
});
});

View File

@@ -0,0 +1,741 @@
/* eslint-disable sonarjs/no-duplicate-string */
import { PANEL_TYPES } from 'constants/queryBuilder';
import { ICompositeMetricQuery } from 'types/api/alerts/compositeQuery';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder';
export const compositeQueryWithoutVariables = ({
builderQueries: {
A: {
queryName: 'A',
stepInterval: 240,
dataSource: DataSource.METRICS,
aggregateOperator: 'rate',
aggregateAttribute: {
key: 'system_disk_operations',
dataType: DataTypes.Float64,
type: 'Sum',
isColumn: true,
isJSON: false,
},
filters: {
op: 'AND',
items: [],
},
expression: 'A',
disabled: false,
limit: 0,
offset: 0,
pageSize: 0,
reduceTo: 'avg',
timeAggregation: 'rate',
spaceAggregation: 'sum',
ShiftBy: 0,
},
},
panelType: PANEL_TYPES.TIME_SERIES,
queryType: EQueryType.QUERY_BUILDER,
} as unknown) as ICompositeMetricQuery;
export const widgetQueryWithoutVariables = ({
clickhouse_sql: [
{
name: 'A',
legend: '',
disabled: false,
query: '',
},
],
promql: [
{
name: 'A',
query: '',
legend: '',
disabled: false,
},
],
builder: {
queryData: [
{
dataSource: 'metrics',
queryName: 'A',
aggregateOperator: 'rate',
aggregateAttribute: {
key: 'system_disk_operations',
dataType: 'float64',
type: 'Sum',
isColumn: true,
isJSON: false,
id: 'system_disk_operations--float64--Sum--true',
},
timeAggregation: 'rate',
spaceAggregation: 'sum',
functions: [],
filters: {
items: [],
op: 'AND',
},
expression: 'A',
disabled: false,
stepInterval: 60,
having: [],
limit: null,
orderBy: [],
groupBy: [],
legend: '',
reduceTo: 'avg',
},
],
queryFormulas: [],
},
id: '2bbbd8d8-db99-40be-b9c6-9e197c5bc537',
queryType: 'builder',
} as unknown) as Query;
export const stepIntervalUnchanged = {
builder: {
queryData: [
{
aggregateAttribute: {
dataType: 'float64',
id: 'system_disk_operations--float64--Sum--true',
isColumn: true,
isJSON: false,
key: 'system_disk_operations',
type: 'Sum',
},
aggregateOperator: 'rate',
dataSource: 'metrics',
disabled: false,
expression: 'A',
filters: {
items: [],
op: 'AND',
},
functions: [],
groupBy: [],
having: [],
legend: '',
limit: null,
orderBy: [],
queryName: 'A',
reduceTo: 'avg',
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'rate',
},
],
queryFormulas: [],
},
clickhouse_sql: [
{
disabled: false,
legend: '',
name: 'A',
query: '',
},
],
id: 'test-id',
promql: [
{
disabled: false,
legend: '',
name: 'A',
query: '',
},
],
queryType: 'builder',
unit: undefined,
};
export const compositeQueryWithVariables = ({
builderQueries: {
A: {
queryName: 'A',
stepInterval: 240,
dataSource: 'metrics',
aggregateOperator: 'sum_rate',
aggregateAttribute: {
key: 'signoz_calls_total',
dataType: 'float64',
type: '',
isColumn: true,
isJSON: false,
},
filters: {
op: 'AND',
items: [
{
key: {
key: 'deployment_environment',
dataType: 'string',
type: 'tag',
isColumn: false,
isJSON: false,
},
value: 'default',
op: 'in',
},
{
key: {
key: 'service_name',
dataType: 'string',
type: 'tag',
isColumn: false,
isJSON: false,
},
value: 'frontend',
op: 'in',
},
{
key: {
key: 'operation',
dataType: 'string',
type: 'tag',
isColumn: false,
isJSON: false,
},
value: 'HTTP GET /dispatch',
op: 'in',
},
],
},
groupBy: [
{
key: 'service_name',
dataType: 'string',
type: 'tag',
isColumn: false,
isJSON: false,
},
{
key: 'operation',
dataType: 'string',
type: 'tag',
isColumn: false,
isJSON: false,
},
],
expression: 'A',
disabled: false,
legend: '{{service_name}}-{{operation}}',
limit: 0,
offset: 0,
pageSize: 0,
reduceTo: 'sum',
timeAggregation: 'rate',
spaceAggregation: 'sum',
ShiftBy: 0,
},
},
panelType: 'graph',
queryType: 'builder',
} as unknown) as ICompositeMetricQuery;
export const widgetQueryWithVariables = ({
clickhouse_sql: [
{
name: 'A',
legend: '',
disabled: false,
query: '',
},
],
promql: [
{
name: 'A',
query: '',
legend: '',
disabled: false,
},
],
builder: {
queryData: [
{
dataSource: 'metrics',
queryName: 'A',
aggregateOperator: 'sum_rate',
aggregateAttribute: {
dataType: 'float64',
id: 'signoz_calls_total--float64----true',
isColumn: true,
isJSON: false,
key: 'signoz_calls_total',
type: '',
},
timeAggregation: 'rate',
spaceAggregation: 'sum',
functions: [],
filters: {
items: [
{
id: 'aa56621e',
key: {
dataType: 'string',
id: 'deployment_environment--string--tag--false',
isColumn: false,
isJSON: false,
key: 'deployment_environment',
type: 'tag',
},
op: 'in',
value: ['{{.deployment_environment}}'],
},
{
id: '97055a02',
key: {
dataType: 'string',
id: 'service_name--string--tag--false',
isColumn: false,
isJSON: false,
key: 'service_name',
type: 'tag',
},
op: 'in',
value: ['{{.service_name}}'],
},
{
id: '8c4599f2',
key: {
dataType: 'string',
id: 'operation--string--tag--false',
isColumn: false,
isJSON: false,
key: 'operation',
type: 'tag',
},
op: 'in',
value: ['{{.endpoint}}'],
},
],
op: 'AND',
},
expression: 'A',
disabled: false,
stepInterval: 60,
having: [],
limit: null,
orderBy: [],
groupBy: [
{
dataType: 'string',
isColumn: false,
isJSON: false,
key: 'service_name',
type: 'tag',
id: 'service_name--string--tag--false',
},
{
dataType: 'string',
isColumn: false,
isJSON: false,
key: 'operation',
type: 'tag',
id: 'operation--string--tag--false',
},
],
legend: '{{service_name}}-{{operation}}',
reduceTo: 'sum',
},
],
queryFormulas: [],
},
id: '64fcd7be-61d0-4f92-bbb2-1449b089f766',
queryType: 'builder',
} as unknown) as Query;
export const replaceVariables = {
builder: {
queryData: [
{
aggregateAttribute: {
dataType: 'float64',
id: 'signoz_calls_total--float64----true',
isColumn: true,
isJSON: false,
key: 'signoz_calls_total',
type: '',
},
aggregateOperator: 'sum_rate',
dataSource: 'metrics',
disabled: false,
expression: 'A',
filters: {
items: [
{
key: {
dataType: 'string',
isColumn: false,
isJSON: false,
key: 'deployment_environment',
type: 'tag',
},
op: 'in',
value: 'default',
},
{
key: {
dataType: 'string',
isColumn: false,
isJSON: false,
key: 'service_name',
type: 'tag',
},
op: 'in',
value: 'frontend',
},
{
key: {
dataType: 'string',
isColumn: false,
isJSON: false,
key: 'operation',
type: 'tag',
},
op: 'in',
value: 'HTTP GET /dispatch',
},
],
op: 'AND',
},
functions: [],
groupBy: [
{
dataType: 'string',
id: 'service_name--string--tag--false',
isColumn: false,
isJSON: false,
key: 'service_name',
type: 'tag',
},
{
dataType: 'string',
id: 'operation--string--tag--false',
isColumn: false,
isJSON: false,
key: 'operation',
type: 'tag',
},
],
having: [],
legend: '{{service_name}}-{{operation}}',
limit: null,
orderBy: [],
queryName: 'A',
reduceTo: 'sum',
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'rate',
},
],
queryFormulas: [],
},
clickhouse_sql: [
{
disabled: false,
legend: '',
name: 'A',
query: '',
},
],
id: 'test-id',
promql: [
{
disabled: false,
legend: '',
name: 'A',
query: '',
},
],
queryType: 'builder',
unit: undefined,
};
export const defaultOutput = {
builder: {
queryData: [
{
ShiftBy: 0,
aggregateAttribute: {
dataType: 'float64',
isColumn: true,
isJSON: false,
key: 'system_disk_operations',
type: 'Sum',
},
aggregateOperator: 'rate',
dataSource: 'metrics',
disabled: false,
expression: 'A',
filters: { items: [], op: 'AND' },
functions: [],
groupBy: [],
having: [],
legend: '',
limit: 0,
offset: 0,
orderBy: [],
pageSize: 0,
queryName: 'A',
reduceTo: 'avg',
spaceAggregation: 'sum',
stepInterval: 240,
timeAggregation: 'rate',
},
],
queryFormulas: [],
},
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
id: 'test-id',
promql: [{ disabled: false, legend: '', name: 'A', query: '' }],
queryType: 'builder',
unit: undefined,
};
export const compositeQueriesWithFunctions = ({
builderQueries: {
A: {
queryName: 'A',
stepInterval: 60,
dataSource: 'metrics',
aggregateOperator: 'count',
aggregateAttribute: {
key: 'signoz_latency_bucket',
dataType: 'float64',
type: 'Histogram',
isColumn: true,
isJSON: false,
},
filters: {
op: 'AND',
items: [],
},
expression: 'A',
disabled: false,
limit: 0,
offset: 0,
pageSize: 0,
reduceTo: 'avg',
spaceAggregation: 'p90',
ShiftBy: 0,
},
B: {
queryName: 'B',
stepInterval: 120,
dataSource: 'metrics',
aggregateOperator: 'rate',
aggregateAttribute: {
key: 'system_disk_io',
dataType: 'float64',
type: 'Sum',
isColumn: true,
isJSON: false,
},
filters: {
op: 'AND',
items: [],
},
expression: 'B',
disabled: false,
limit: 0,
offset: 0,
pageSize: 0,
reduceTo: 'avg',
timeAggregation: 'rate',
spaceAggregation: 'sum',
ShiftBy: 0,
},
F1: {
queryName: 'F1',
stepInterval: 1,
dataSource: '',
aggregateOperator: '',
aggregateAttribute: {
key: '',
dataType: '',
type: '',
isColumn: false,
isJSON: false,
},
expression: 'A / B ',
disabled: false,
limit: 0,
offset: 0,
pageSize: 0,
ShiftBy: 0,
},
},
panelType: 'graph',
queryType: 'builder',
} as unknown) as ICompositeMetricQuery;
export const widgetQueriesWithFunctions = ({
clickhouse_sql: [
{
name: 'A',
legend: '',
disabled: false,
query: '',
},
],
promql: [
{
name: 'A',
query: '',
legend: '',
disabled: false,
},
],
builder: {
queryData: [
{
dataSource: 'metrics',
queryName: 'A',
aggregateOperator: 'count',
aggregateAttribute: {
dataType: 'float64',
id: 'signoz_latency_bucket--float64--Histogram--true',
isColumn: true,
isJSON: false,
key: 'signoz_latency_bucket',
type: 'Histogram',
},
timeAggregation: '',
spaceAggregation: 'p90',
functions: [],
filters: {
items: [],
op: 'AND',
},
expression: 'A',
disabled: false,
stepInterval: 120,
having: [],
limit: null,
orderBy: [],
groupBy: [],
legend: '',
reduceTo: 'avg',
},
{
dataSource: 'metrics',
queryName: 'B',
aggregateOperator: 'rate',
aggregateAttribute: {
key: 'system_disk_io',
dataType: 'float64',
type: 'Sum',
isColumn: true,
isJSON: false,
id: 'system_disk_io--float64--Sum--true',
},
timeAggregation: 'rate',
spaceAggregation: 'sum',
functions: [],
filters: {
items: [],
op: 'AND',
},
expression: 'B',
disabled: false,
stepInterval: 120,
having: [],
limit: null,
orderBy: [],
groupBy: [],
legend: '',
reduceTo: 'avg',
},
],
queryFormulas: [
{
queryName: 'F1',
expression: 'A / B ',
disabled: false,
legend: '',
},
],
},
id: '5d1844fe-9b44-4f15-b6fe-f1b843550b77',
queryType: 'builder',
} as unknown) as Query;
export const outputWithFunctions = {
builder: {
queryData: [
{
dataSource: 'metrics',
queryName: 'A',
aggregateOperator: 'count',
aggregateAttribute: {
dataType: 'float64',
id: 'signoz_latency_bucket--float64--Histogram--true',
isColumn: true,
isJSON: false,
key: 'signoz_latency_bucket',
type: 'Histogram',
},
timeAggregation: '',
spaceAggregation: 'p90',
functions: [],
filters: {
op: 'AND',
items: [],
},
expression: 'A',
disabled: false,
stepInterval: 120,
having: [],
limit: null,
orderBy: [],
groupBy: [],
legend: '',
reduceTo: 'avg',
},
{
dataSource: 'metrics',
queryName: 'B',
aggregateOperator: 'rate',
aggregateAttribute: {
key: 'system_disk_io',
dataType: 'float64',
type: 'Sum',
isColumn: true,
isJSON: false,
id: 'system_disk_io--float64--Sum--true',
},
timeAggregation: 'rate',
spaceAggregation: 'sum',
functions: [],
filters: {
op: 'AND',
items: [],
},
expression: 'B',
disabled: false,
stepInterval: 120,
having: [],
limit: null,
orderBy: [],
groupBy: [],
legend: '',
reduceTo: 'avg',
},
],
queryFormulas: [
{
queryName: 'F1',
expression: 'A / B ',
disabled: false,
legend: '',
},
],
},
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
id: 'test-id',
promql: [{ disabled: false, legend: '', name: 'A', query: '' }],
queryType: 'builder',
unit: undefined,
};

View File

@@ -7,9 +7,13 @@ import { transformQueryBuilderDataModel } from '../transformQueryBuilderDataMode
export const mapQueryDataFromApi = (
compositeQuery: ICompositeMetricQuery,
query?: Query,
): Query => {
const builder = compositeQuery.builderQueries
? transformQueryBuilderDataModel(compositeQuery.builderQueries)
? transformQueryBuilderDataModel(
compositeQuery.builderQueries,
query?.builder,
)
: initialQueryState.builder;
const promql = compositeQuery.promQueries

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