mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-05 18:40:56 +08:00
commit
8d84ce8f06
@ -198,14 +198,14 @@ Not sure how to get started? Just ping us on `#contributing` in our [slack commu
|
||||
|
||||
#### Frontend
|
||||
|
||||
- [Palash Gupta](https://github.com/palashgdev)
|
||||
- [Yunus M](https://github.com/YounixM)
|
||||
- [Rajat Dabade](https://github.com/Rajat-Dabade)
|
||||
- [Vikrant Gupta](https://github.com/vikrantgupta25)
|
||||
- [Sagar Rajput](https://github.com/SagarRajput-7)
|
||||
|
||||
#### DevOps
|
||||
|
||||
- [Prashant Shahi](https://github.com/prashant-shahi)
|
||||
- [Dhawal Sanghvi](https://github.com/dhawal1248)
|
||||
- [Vibhu Pandey](https://github.com/grandwizard28)
|
||||
|
||||
<br /><br />
|
||||
|
||||
|
@ -146,7 +146,7 @@ services:
|
||||
condition: on-failure
|
||||
|
||||
query-service:
|
||||
image: signoz/query-service:0.49.1
|
||||
image: signoz/query-service:0.50.0
|
||||
command:
|
||||
[
|
||||
"-config=/root/config/prometheus.yml",
|
||||
|
@ -164,7 +164,7 @@ services:
|
||||
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
|
||||
|
||||
query-service:
|
||||
image: signoz/query-service:${DOCKER_TAG:-0.49.1}
|
||||
image: signoz/query-service:${DOCKER_TAG:-0.50.0}
|
||||
container_name: signoz-query-service
|
||||
command:
|
||||
[
|
||||
@ -204,7 +204,7 @@ services:
|
||||
<<: *db-depend
|
||||
|
||||
frontend:
|
||||
image: signoz/frontend:${DOCKER_TAG:-0.49.1}
|
||||
image: signoz/frontend:${DOCKER_TAG:-0.50.0}
|
||||
container_name: signoz-frontend
|
||||
restart: on-failure
|
||||
depends_on:
|
||||
|
@ -164,7 +164,7 @@ services:
|
||||
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
|
||||
|
||||
query-service:
|
||||
image: signoz/query-service:${DOCKER_TAG:-0.49.1}
|
||||
image: signoz/query-service:${DOCKER_TAG:-0.50.0}
|
||||
container_name: signoz-query-service
|
||||
command:
|
||||
[
|
||||
@ -203,7 +203,7 @@ services:
|
||||
<<: *db-depend
|
||||
|
||||
frontend:
|
||||
image: signoz/frontend:${DOCKER_TAG:-0.49.1}
|
||||
image: signoz/frontend:${DOCKER_TAG:-0.50.0}
|
||||
container_name: signoz-frontend
|
||||
restart: on-failure
|
||||
depends_on:
|
||||
|
@ -9,6 +9,7 @@ const config: Config.InitialOptions = {
|
||||
modulePathIgnorePatterns: ['dist'],
|
||||
moduleNameMapper: {
|
||||
'\\.(css|less|scss)$': '<rootDir>/__mocks__/cssMock.ts',
|
||||
'\\.md$': '<rootDir>/__mocks__/cssMock.ts',
|
||||
},
|
||||
globals: {
|
||||
extensionsToTreatAsEsm: ['.ts'],
|
||||
|
@ -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",
|
||||
|
BIN
frontend/public/fonts/GeistMonoVF.woff2
Normal file
BIN
frontend/public/fonts/GeistMonoVF.woff2
Normal file
Binary file not shown.
8
frontend/public/locales/en-GB/onboarding.json
Normal file
8
frontend/public/locales/en-GB/onboarding.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"invite_user": "Invite your teammates",
|
||||
"invite": "Invite",
|
||||
"skip": "Skip",
|
||||
"invite_user_helper_text": "Not the right person to get started? No worries! Invite someone who can.",
|
||||
"select_use_case": "Select a use-case to get started",
|
||||
"get_started": "Get Started"
|
||||
}
|
@ -6,5 +6,6 @@
|
||||
"share": "Share",
|
||||
"save": "Save",
|
||||
"edit": "Edit",
|
||||
"logged_in": "Logged In"
|
||||
"logged_in": "Logged In",
|
||||
"pending_data_placeholder": "Just a bit of patience, just a little bit’s enough ⎯ we’re getting your {{dataSource}}!"
|
||||
}
|
||||
|
8
frontend/public/locales/en/onboarding.json
Normal file
8
frontend/public/locales/en/onboarding.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"invite_user": "Invite your teammates",
|
||||
"invite": "Invite",
|
||||
"skip": "Skip",
|
||||
"invite_user_helper_text": "Not the right person to get started? No worries! Invite someone who can.",
|
||||
"select_use_case": "Select a use-case to get started",
|
||||
"get_started": "Get Started"
|
||||
}
|
@ -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;
|
||||
|
@ -52,7 +52,7 @@
|
||||
|
||||
.log-body {
|
||||
font-family: 'SF Mono';
|
||||
font-family: 'Space Mono', monospace;
|
||||
font-family: 'Geist Mono';
|
||||
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-normal);
|
||||
|
@ -49,7 +49,7 @@ export const ExpandIconWrapper = styled(Col)`
|
||||
export const RawLogContent = styled.div<RawLogContentProps>`
|
||||
margin-bottom: 0;
|
||||
font-family: 'SF Mono', monospace;
|
||||
font-family: 'Space Mono', monospace;
|
||||
font-family: 'Geist Mono';
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
text-align: left;
|
||||
|
@ -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;
|
@ -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: {} };
|
@ -0,0 +1,3 @@
|
||||
.overlay-scrollbar {
|
||||
height: 100%;
|
||||
}
|
@ -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: {} };
|
@ -0,0 +1,5 @@
|
||||
.overlay-scroll-wrapper {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
}
|
@ -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}
|
||||
|
@ -5,7 +5,6 @@
|
||||
|
||||
.app-content {
|
||||
width: calc(100% - 64px);
|
||||
overflow: auto;
|
||||
z-index: 0;
|
||||
|
||||
.content-container {
|
||||
|
@ -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,9 +304,13 @@ 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>
|
||||
<LayoutContent data-overlayscrollbars-initialize>
|
||||
<OverlayScrollbar>
|
||||
<ChildrenContainer
|
||||
style={{
|
||||
margin:
|
||||
@ -321,6 +326,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
{isToDisplayLayout && !renderFullScreen && <TopNav />}
|
||||
{children}
|
||||
</ChildrenContainer>
|
||||
</OverlayScrollbar>
|
||||
</LayoutContent>
|
||||
</Sentry.ErrorBoundary>
|
||||
</div>
|
||||
|
@ -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;
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -91,8 +91,7 @@
|
||||
box-shadow: none !important;
|
||||
|
||||
&.ant-btn-round {
|
||||
padding-inline-start: 10px;
|
||||
padding-inline-end: 8px;
|
||||
padding: 8px 12px 8px 10px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { Button, Typography } from 'antd';
|
||||
import createDashboard from 'api/dashboard/create';
|
||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||
import { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard';
|
||||
import useAxiosError from 'hooks/useAxiosError';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
@ -40,7 +41,7 @@ function ExportPanelContainer({
|
||||
} = useMutation(createDashboard, {
|
||||
onSuccess: (data) => {
|
||||
if (data.payload) {
|
||||
onExport(data?.payload);
|
||||
onExport(data?.payload, true);
|
||||
}
|
||||
refetch();
|
||||
},
|
||||
@ -54,7 +55,7 @@ function ExportPanelContainer({
|
||||
({ uuid }) => uuid === selectedDashboardId,
|
||||
);
|
||||
|
||||
onExport(currentSelectedDashboard || null);
|
||||
onExport(currentSelectedDashboard || null, false);
|
||||
}, [data, selectedDashboardId, onExport]);
|
||||
|
||||
const handleSelect = useCallback(
|
||||
@ -70,6 +71,7 @@ function ExportPanelContainer({
|
||||
ns: 'dashboard',
|
||||
}),
|
||||
uploadedGrafana: false,
|
||||
version: ENTITY_VERSION_V4,
|
||||
});
|
||||
}, [t, createNewDashboard]);
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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(
|
||||
@ -246,17 +248,19 @@ function ChartPreview({
|
||||
return (
|
||||
<ChartContainer>
|
||||
{headline}
|
||||
|
||||
<div ref={graphRef} style={{ height: '100%' }}>
|
||||
{queryResponse.isLoading && (
|
||||
<Spinner size="large" tip="Loading..." height="100%" />
|
||||
)}
|
||||
{(queryResponse?.isError || queryResponse?.error) && (
|
||||
<FailedMessageContainer color="red" title="Failed to refresh the chart">
|
||||
<InfoCircleOutlined />{' '}
|
||||
{queryResponse.error.message || t('preview_chart_unexpected_error')}
|
||||
</FailedMessageContainer>
|
||||
)}
|
||||
|
||||
{chartData && !queryResponse.isError && (
|
||||
<div ref={graphRef} style={{ height: '100%' }}>
|
||||
{queryResponse.isLoading && (
|
||||
<Spinner size="large" tip="Loading..." height="100%" />
|
||||
)}
|
||||
<GridPanelSwitch
|
||||
options={options}
|
||||
panelType={graphType}
|
||||
@ -268,8 +272,8 @@ function ChartPreview({
|
||||
query={query || initialQueriesMap.metrics}
|
||||
yAxisUnit={yAxisUnit}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ChartContainer>
|
||||
);
|
||||
}
|
||||
@ -282,6 +286,7 @@ ChartPreview.defaultProps = {
|
||||
userQueryKey: '',
|
||||
allowSelectedIntervalForStepGen: false,
|
||||
alertDef: undefined,
|
||||
setQueryStatus: (): void => {},
|
||||
};
|
||||
|
||||
export default ChartPreview;
|
||||
|
@ -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}
|
||||
>
|
||||
|
@ -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 = (
|
||||
|
@ -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">
|
||||
|
@ -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,10 @@ function GridCardGraph({
|
||||
offset: 0,
|
||||
limit: updatedQuery.builder.queryData[0].limit || 0,
|
||||
},
|
||||
selectColumns:
|
||||
initialDataSource === DataSource.LOGS
|
||||
? widget.selectedLogFields
|
||||
: widget.selectedTracesFields,
|
||||
},
|
||||
fillGaps: widget.fillSpans,
|
||||
};
|
||||
|
@ -1,6 +1,7 @@
|
||||
.fullscreen-grid-container {
|
||||
overflow: auto;
|
||||
margin: 8px -8px;
|
||||
margin-right: 0;
|
||||
|
||||
.react-grid-layout {
|
||||
border: none !important;
|
||||
|
@ -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}
|
||||
|
@ -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' : ''
|
||||
}`}
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
@ -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];
|
||||
|
@ -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';
|
||||
@ -269,6 +271,7 @@ function DashboardsList(): JSX.Element {
|
||||
|
||||
const onNewDashboardHandler = useCallback(async () => {
|
||||
try {
|
||||
logEvent('Dashboard List: Create dashboard clicked', {});
|
||||
setNewDashboardState({
|
||||
...newDashboardState,
|
||||
loading: true,
|
||||
@ -305,6 +308,8 @@ function DashboardsList(): JSX.Element {
|
||||
}, [newDashboardState, t]);
|
||||
|
||||
const onModalHandler = (uploadedGrafana: boolean): void => {
|
||||
logEvent('Dashboard List: Import JSON clicked', {});
|
||||
|
||||
setIsImportJSONModalVisible((state) => !state);
|
||||
setUploadedGrafana(uploadedGrafana);
|
||||
};
|
||||
@ -441,6 +446,10 @@ function DashboardsList(): JSX.Element {
|
||||
} else {
|
||||
history.push(getLink());
|
||||
}
|
||||
logEvent('Dashboard List: Clicked on dashboard', {
|
||||
dashboardId: dashboard.id,
|
||||
dashboardName: dashboard.name,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
@ -619,6 +628,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">
|
||||
@ -705,6 +729,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>
|
||||
@ -745,6 +772,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>
|
||||
|
@ -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({
|
||||
@ -141,11 +148,6 @@ function ImportJSON({
|
||||
colors: {
|
||||
'editor.background': Color.BG_INK_300,
|
||||
},
|
||||
fontFamily: 'Space Mono',
|
||||
fontSize: 20,
|
||||
fontWeight: 'normal',
|
||||
lineHeight: 18,
|
||||
letterSpacing: -0.06,
|
||||
});
|
||||
}
|
||||
|
||||
@ -185,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')}
|
||||
@ -233,6 +238,11 @@ function ImportJSON({
|
||||
fontFamily: 'Space Mono',
|
||||
}}
|
||||
theme={isDarkMode ? 'my-theme' : 'light'}
|
||||
onMount={(_, monaco): void => {
|
||||
document.fonts.ready.then(() => {
|
||||
monaco.editor.remeasureFonts();
|
||||
});
|
||||
}}
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
beforeMount={setEditorTheme}
|
||||
/>
|
||||
|
@ -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,6 +129,7 @@ function LiveLogsList({ logs }: LiveLogsListProps): JSX.Element {
|
||||
/>
|
||||
) : (
|
||||
<Card style={{ width: '100%' }} bodyStyle={CARD_BODY_STYLE}>
|
||||
<OverlayScrollbar isVirtuoso>
|
||||
<Virtuoso
|
||||
ref={ref}
|
||||
initialTopMostItemIndex={activeLogIndex !== -1 ? activeLogIndex : 0}
|
||||
@ -135,6 +137,7 @@ function LiveLogsList({ logs }: LiveLogsListProps): JSX.Element {
|
||||
totalCount={logs.length}
|
||||
itemContent={getItemContent}
|
||||
/>
|
||||
</OverlayScrollbar>
|
||||
</Card>
|
||||
)}
|
||||
</InfinityWrapperStyled>
|
||||
|
@ -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,6 +95,7 @@ function ContextLogRenderer({
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<OverlayScrollbar isVirtuoso>
|
||||
<Virtuoso
|
||||
className="virtuoso-list"
|
||||
initialTopMostItemIndex={0}
|
||||
@ -101,6 +103,7 @@ function ContextLogRenderer({
|
||||
itemContent={getItemContent}
|
||||
style={{ height: `calc(${logs.length} * 32px)` }}
|
||||
/>
|
||||
</OverlayScrollbar>
|
||||
{isAfterLogsFetching && (
|
||||
<Skeleton
|
||||
style={{
|
||||
|
@ -8,7 +8,7 @@
|
||||
.label {
|
||||
color: var(--text-robin-400);
|
||||
font-family: SF Mono;
|
||||
font-family: 'Space Mono', monospace;
|
||||
font-family: 'Geist Mono';
|
||||
font-size: 13px;
|
||||
font-weight: var(--font-weight-normal);
|
||||
line-height: 18px;
|
||||
|
@ -28,7 +28,7 @@ function JSONView({ logData }: JSONViewProps): JSX.Element {
|
||||
},
|
||||
fontWeight: 400,
|
||||
// fontFamily: 'SF Mono',
|
||||
fontFamily: 'Space Mono',
|
||||
fontFamily: 'Geist Mono',
|
||||
fontSize: 13,
|
||||
lineHeight: '18px',
|
||||
colorDecorators: true,
|
||||
|
@ -53,8 +53,7 @@ function Overview({
|
||||
enabled: false,
|
||||
},
|
||||
fontWeight: 400,
|
||||
// fontFamily: 'SF Mono',
|
||||
fontFamily: 'Space Mono',
|
||||
fontFamily: 'Geist Mono',
|
||||
fontSize: 13,
|
||||
lineHeight: '18px',
|
||||
colorDecorators: true,
|
||||
@ -80,12 +79,6 @@ function Overview({
|
||||
colors: {
|
||||
'editor.background': Color.BG_INK_400,
|
||||
},
|
||||
// fontFamily: 'SF Mono',
|
||||
fontFamily: 'Space Mono',
|
||||
fontSize: 12,
|
||||
fontWeight: 'normal',
|
||||
lineHeight: 18,
|
||||
letterSpacing: -0.06,
|
||||
});
|
||||
}
|
||||
|
||||
@ -124,6 +117,11 @@ function Overview({
|
||||
onChange={(): void => {}}
|
||||
height="20vh"
|
||||
theme={isDarkMode ? 'my-theme' : 'light'}
|
||||
onMount={(_, monaco): void => {
|
||||
document.fonts.ready.then(() => {
|
||||
monaco.editor.remeasureFonts();
|
||||
});
|
||||
}}
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
beforeMount={setEditorTheme}
|
||||
/>
|
||||
|
@ -57,6 +57,8 @@
|
||||
background: rgba(22, 25, 34, 0.4);
|
||||
|
||||
.value-field {
|
||||
font-family: 'Geist Mono';
|
||||
|
||||
position: relative;
|
||||
}
|
||||
|
||||
|
@ -289,7 +289,13 @@ function TableView({
|
||||
return (
|
||||
<div className="value-field">
|
||||
<CopyClipboardHOC textToCopy={textToCopy}>
|
||||
<span style={{ color: Color.BG_SIENNA_400 }}>
|
||||
<span
|
||||
style={{
|
||||
color: Color.BG_SIENNA_400,
|
||||
whiteSpace: 'pre-wrap',
|
||||
tabSize: 4,
|
||||
}}
|
||||
>
|
||||
{removeEscapeCharacters(fieldData.value)}
|
||||
</span>
|
||||
</CopyClipboardHOC>
|
||||
|
@ -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,7 +188,7 @@ function LogsContextList({
|
||||
<EmptyText>No Data</EmptyText>
|
||||
)}
|
||||
{isFetching && <Spinner size="large" height="10rem" />}
|
||||
|
||||
<OverlayScrollbar isVirtuoso>
|
||||
<Virtuoso
|
||||
className="virtuoso-list"
|
||||
initialTopMostItemIndex={0}
|
||||
@ -195,6 +196,7 @@ function LogsContextList({
|
||||
itemContent={getItemContent}
|
||||
followOutput={order === ORDERBY_FILTERS.DESC}
|
||||
/>
|
||||
</OverlayScrollbar>
|
||||
</ListContainer>
|
||||
|
||||
{order === ORDERBY_FILTERS.DESC && (
|
||||
|
@ -9,6 +9,6 @@ export const CardStyled = styled(Card)`
|
||||
height: 200px;
|
||||
min-height: 200px;
|
||||
padding: 0 16px 16px 16px;
|
||||
font-family: 'Space Mono', monospace;
|
||||
font-family: 'Geist Mono';
|
||||
}
|
||||
`;
|
||||
|
@ -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,6 +134,7 @@ function LogsExplorerList({
|
||||
style={{ width: '100%', marginTop: '20px' }}
|
||||
bodyStyle={CARD_BODY_STYLE}
|
||||
>
|
||||
<OverlayScrollbar isVirtuoso>
|
||||
<Virtuoso
|
||||
ref={ref}
|
||||
initialTopMostItemIndex={activeLogIndex !== -1 ? activeLogIndex : 0}
|
||||
@ -142,6 +144,7 @@ function LogsExplorerList({
|
||||
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 />}
|
||||
|
||||
|
@ -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';
|
||||
@ -63,10 +71,10 @@ import { v4 } from 'uuid';
|
||||
|
||||
function LogsExplorerViews({
|
||||
selectedView,
|
||||
showHistogram,
|
||||
showFrequencyChart,
|
||||
}: {
|
||||
selectedView: SELECTED_VIEWS;
|
||||
showHistogram: boolean;
|
||||
showFrequencyChart: boolean;
|
||||
}): JSX.Element {
|
||||
const { notifications } = useNotifications();
|
||||
const history = useHistory();
|
||||
@ -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) {
|
||||
@ -561,7 +588,7 @@ function LogsExplorerViews({
|
||||
|
||||
return (
|
||||
<div className="logs-explorer-views-container">
|
||||
{showHistogram && (
|
||||
{showFrequencyChart && (
|
||||
<LogsExplorerChart
|
||||
className="logs-histogram"
|
||||
isLoading={isFetchingListChartData || isLoadingListChartData}
|
||||
|
@ -76,7 +76,10 @@ const renderer = (): RenderResult =>
|
||||
<VirtuosoMockContext.Provider
|
||||
value={{ viewportHeight: 300, itemHeight: 100 }}
|
||||
>
|
||||
<LogsExplorerViews selectedView={SELECTED_VIEWS.SEARCH} showHistogram />
|
||||
<LogsExplorerViews
|
||||
selectedView={SELECTED_VIEWS.SEARCH}
|
||||
showFrequencyChart
|
||||
/>
|
||||
</VirtuosoMockContext.Provider>
|
||||
</QueryBuilderProvider>
|
||||
</MockQueryClientProvider>
|
||||
@ -120,11 +123,7 @@ describe('LogsExplorerViews -', () => {
|
||||
|
||||
// switch to table view
|
||||
await userEvent.click(queryByTestId('table-view') as HTMLElement);
|
||||
expect(
|
||||
queryByText(
|
||||
'Just a bit of patience, just a little bit’s enough ⎯ we’re getting your logs!',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
expect(queryByText('pending_data_placeholder')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('check error state', async () => {
|
||||
|
@ -1,8 +1,11 @@
|
||||
import './LogsLoading.styles.scss';
|
||||
|
||||
import { Typography } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
export function LogsLoading(): JSX.Element {
|
||||
const { t } = useTranslation('common');
|
||||
return (
|
||||
<div className="loading-logs">
|
||||
<div className="loading-logs-content">
|
||||
@ -13,8 +16,7 @@ export function LogsLoading(): JSX.Element {
|
||||
/>
|
||||
|
||||
<Typography>
|
||||
Just a bit of patience, just a little bit’s enough ⎯ we’re getting your
|
||||
logs!
|
||||
{t('pending_data_placeholder', { dataSource: DataSource.LOGS })}
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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,6 +208,7 @@ function LogsPanelComponent({
|
||||
<>
|
||||
<div className="logs-table">
|
||||
<div className="resize-table">
|
||||
<OverlayScrollbar>
|
||||
<Table
|
||||
pagination={false}
|
||||
tableLayout="fixed"
|
||||
@ -218,6 +220,7 @@ function LogsPanelComponent({
|
||||
columns={columns}
|
||||
onRow={handleRow}
|
||||
/>
|
||||
</OverlayScrollbar>
|
||||
</div>
|
||||
{!widget.query.builder.queryData[0].limit && (
|
||||
<div className="controller">
|
||||
|
@ -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}>
|
||||
<OverlayScrollbar isVirtuoso>
|
||||
<Virtuoso totalCount={logs.length} itemContent={getItemContent} />
|
||||
</OverlayScrollbar>
|
||||
</Card>
|
||||
);
|
||||
}, [getItemContent, linesPerRow, logs, onSetActiveLog, selected, viewMode]);
|
||||
|
@ -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,
|
||||
|
@ -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({
|
||||
|
@ -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,
|
||||
|
@ -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)}`,
|
||||
|
@ -58,6 +58,7 @@
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-right: 16px;
|
||||
|
||||
.dashboard-breadcrumbs {
|
||||
height: 48px;
|
||||
|
@ -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"
|
||||
>
|
||||
<OverlayScrollbar>
|
||||
<DashboardSettingsContent variableViewModeRef={variableViewModeRef} />
|
||||
</OverlayScrollbar>
|
||||
</DrawerContainer>
|
||||
</>
|
||||
);
|
||||
|
@ -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 => {
|
||||
@ -404,7 +411,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 +427,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>
|
||||
|
@ -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>
|
||||
|
@ -87,9 +87,6 @@ function ClickHouseQueryBuilder({
|
||||
'editor.background': Color.BG_INK_300,
|
||||
},
|
||||
});
|
||||
document.fonts.ready.then(() => {
|
||||
monaco.editor.remeasureFonts();
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
@ -105,6 +102,11 @@ function ClickHouseQueryBuilder({
|
||||
height="200px"
|
||||
onChange={handleUpdateEditor}
|
||||
value={queryData.query}
|
||||
onMount={(_, monaco): void => {
|
||||
document.fonts.ready.then(() => {
|
||||
monaco.editor.remeasureFonts();
|
||||
});
|
||||
}}
|
||||
options={{
|
||||
scrollbar: {
|
||||
alwaysConsumeMouseWheel: false,
|
||||
|
@ -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);
|
||||
};
|
||||
|
||||
|
@ -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}>
|
||||
|
@ -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';
|
||||
@ -100,6 +102,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 +111,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 +497,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(
|
||||
@ -530,11 +559,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) {
|
||||
setRequestData((prev) => ({
|
||||
...prev,
|
||||
tableParams: {
|
||||
...prev.tableParams,
|
||||
selectColumns: selectedLogFields,
|
||||
},
|
||||
}));
|
||||
} 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,6 +643,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
|
||||
<PanelContainer>
|
||||
<LeftContainerWrapper isDarkMode={useIsDarkMode()}>
|
||||
<OverlayScrollbar>
|
||||
{selectedWidget && (
|
||||
<LeftContainer
|
||||
selectedGraph={graphType}
|
||||
@ -600,9 +658,11 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
isLoadingPanelData={isLoadingPanelData}
|
||||
/>
|
||||
)}
|
||||
</OverlayScrollbar>
|
||||
</LeftContainerWrapper>
|
||||
|
||||
<RightContainerWrapper>
|
||||
<OverlayScrollbar>
|
||||
<RightContainer
|
||||
setGraphHandler={setGraphHandler}
|
||||
title={title}
|
||||
@ -640,6 +700,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
softMax={softMax}
|
||||
setSoftMax={setSoftMax}
|
||||
/>
|
||||
</OverlayScrollbar>
|
||||
</RightContainerWrapper>
|
||||
</PanelContainer>
|
||||
<Modal
|
||||
|
@ -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',
|
||||
],
|
||||
},
|
||||
},
|
||||
@ -372,8 +396,12 @@ export function handleQueryChange(
|
||||
builder: {
|
||||
...supersetQuery.builder,
|
||||
queryData: supersetQuery.builder.queryData.map((query, index) => {
|
||||
const { dataSource } = query;
|
||||
const tempQuery = { ...initialQueryBuilderFormValuesMap[dataSource] };
|
||||
const { dataSource, expression, queryName } = query;
|
||||
const tempQuery = {
|
||||
...initialQueryBuilderFormValuesMap[dataSource],
|
||||
expression,
|
||||
queryName,
|
||||
};
|
||||
|
||||
const fieldsToSelect =
|
||||
panelTypeDataSourceFormValuesMap[newPanelType][dataSource].builder
|
||||
|
@ -39,6 +39,7 @@
|
||||
font-weight: 500;
|
||||
line-height: 18px; /* 128.571% */
|
||||
letter-spacing: -0.07px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,13 @@
|
||||
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';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { isCloudUser } from 'utils/app';
|
||||
import DOCLINKS from 'utils/docLinks';
|
||||
|
||||
export default function NoLogs({
|
||||
dataSource,
|
||||
@ -20,13 +22,20 @@ 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
|
||||
: ROUTES.GET_STARTED_LOGS_MANAGEMENT,
|
||||
);
|
||||
} else if (dataSource === 'traces') {
|
||||
window.open(DOCLINKS.TRACES_EXPLORER_EMPTY_STATE, '_blank');
|
||||
} else {
|
||||
window.open(`https://signoz.io/docs/userguide/${dataSource}/`, '_blank');
|
||||
window.open(`${DOCLINKS.USER_GUIDE}${dataSource}/`, '_blank');
|
||||
}
|
||||
};
|
||||
return (
|
||||
|
@ -1,16 +1,6 @@
|
||||
.container {
|
||||
width: 100%;
|
||||
// max-width: 1440px;
|
||||
margin: 0 auto;
|
||||
|
||||
&.darkMode {
|
||||
}
|
||||
|
||||
&.lightMode {
|
||||
.onboardingHeader {
|
||||
color: #1d1d1d;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.moduleSelectContainer {
|
||||
@ -61,6 +51,8 @@
|
||||
width: 300px;
|
||||
transition: 0.3s;
|
||||
|
||||
background-color: #000;
|
||||
|
||||
.ant-card-body {
|
||||
padding: 0px;
|
||||
}
|
||||
@ -80,6 +72,9 @@
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-align: center;
|
||||
|
||||
border-bottom: 1px solid #303030;
|
||||
background-color: var(--bg-ink-400);
|
||||
}
|
||||
|
||||
.moduleStyles.selected {
|
||||
@ -157,3 +152,107 @@
|
||||
padding: 12px;
|
||||
margin: 24px 0;
|
||||
}
|
||||
|
||||
.invite-member-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 32px 0;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
.invite-member {
|
||||
display: flex;
|
||||
width: 480px;
|
||||
height: 64px;
|
||||
padding: 16px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--bg-slate-500);
|
||||
background: var(--bg-ink-400);
|
||||
|
||||
.ant-typography {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
> button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.onboarding-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.skip-to-console {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
position: absolute;
|
||||
top: 40px;
|
||||
right: 40px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--bg-vanilla-200);
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.invite-member-wrapper {
|
||||
.invite-member {
|
||||
border: 1px solid var(--bg-vanilla-200);
|
||||
background: var(--bg-vanilla-100);
|
||||
|
||||
.ant-typography {
|
||||
color: var(--bg-slate-200);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.skip-to-console {
|
||||
color: var(--bg-slate-200);
|
||||
|
||||
&:hover {
|
||||
color: var(--bg-slate-200);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.container {
|
||||
.onboardingHeader {
|
||||
color: var(--bg-slate-200);
|
||||
}
|
||||
}
|
||||
|
||||
.moduleStyles {
|
||||
background-color: var(--bg-vanilla-100);
|
||||
}
|
||||
|
||||
.moduleTitleStyle {
|
||||
border-bottom: 1px solid var(--bg-vanilla-300);
|
||||
background-color: var(--bg-vanilla-100);
|
||||
}
|
||||
|
||||
.moduleDesc {
|
||||
background-color: var(--bg-vanilla-100);
|
||||
}
|
||||
}
|
||||
|
@ -3,15 +3,19 @@
|
||||
import './Onboarding.styles.scss';
|
||||
|
||||
import { ArrowRightOutlined } from '@ant-design/icons';
|
||||
import { Button, Card, Typography } from 'antd';
|
||||
import { Button, Card, Form, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import getIngestionData from 'api/settings/getIngestionData';
|
||||
import cx from 'classnames';
|
||||
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 { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import history from 'lib/history';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { UserPlus } from 'lucide-react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQuery } from 'react-query';
|
||||
import { useEffectOnce } from 'react-use';
|
||||
|
||||
@ -100,9 +104,9 @@ export default function Onboarding(): JSX.Element {
|
||||
const [selectedModuleSteps, setSelectedModuleSteps] = useState(APM_STEPS);
|
||||
const [activeStep, setActiveStep] = useState(1);
|
||||
const [current, setCurrent] = useState(0);
|
||||
const isDarkMode = useIsDarkMode();
|
||||
const { trackEvent } = useAnalytics();
|
||||
const { location } = history;
|
||||
const { t } = useTranslation(['onboarding']);
|
||||
|
||||
const {
|
||||
selectedDataSource,
|
||||
@ -279,13 +283,38 @@ export default function Onboarding(): JSX.Element {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const [form] = Form.useForm<InviteMemberFormValues>();
|
||||
const [
|
||||
isInviteTeamMemberModalOpen,
|
||||
setIsInviteTeamMemberModalOpen,
|
||||
] = useState<boolean>(false);
|
||||
|
||||
const toggleModal = useCallback(
|
||||
(value: boolean): void => {
|
||||
setIsInviteTeamMemberModalOpen(value);
|
||||
if (!value) {
|
||||
form.resetFields();
|
||||
}
|
||||
},
|
||||
[form],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cx('container', isDarkMode ? 'darkMode' : 'lightMode')}>
|
||||
<div className="container">
|
||||
{activeStep === 1 && (
|
||||
<>
|
||||
<div className="onboarding-page">
|
||||
<div
|
||||
onClick={(): void => {
|
||||
logEvent('Onboarding V2: Skip Button Clicked', {});
|
||||
history.push('/');
|
||||
}}
|
||||
className="skip-to-console"
|
||||
>
|
||||
{t('skip')}
|
||||
</div>
|
||||
<FullScreenHeader />
|
||||
<div className="onboardingHeader">
|
||||
<h1> Select a use-case to get started</h1>
|
||||
<h1>{t('select_use_case')}</h1>
|
||||
</div>
|
||||
<div className="modulesContainer">
|
||||
<div className="moduleContainerRowStyles">
|
||||
@ -298,26 +327,13 @@ export default function Onboarding(): JSX.Element {
|
||||
'moduleStyles',
|
||||
selectedModule.id === selectedUseCase.id ? 'selected' : '',
|
||||
)}
|
||||
style={{
|
||||
backgroundColor: isDarkMode ? '#000' : '#FFF',
|
||||
}}
|
||||
key={selectedUseCase.id}
|
||||
onClick={(): void => handleModuleSelect(selectedUseCase)}
|
||||
>
|
||||
<Typography.Title
|
||||
className="moduleTitleStyle"
|
||||
level={4}
|
||||
style={{
|
||||
borderBottom: isDarkMode ? '1px solid #303030' : '1px solid #ddd',
|
||||
backgroundColor: isDarkMode ? '#141414' : '#FFF',
|
||||
}}
|
||||
>
|
||||
<Typography.Title className="moduleTitleStyle" level={4}>
|
||||
{selectedUseCase.title}
|
||||
</Typography.Title>
|
||||
<Typography.Paragraph
|
||||
className="moduleDesc"
|
||||
style={{ backgroundColor: isDarkMode ? '#000' : '#FFF' }}
|
||||
>
|
||||
<Typography.Paragraph className="moduleDesc">
|
||||
{selectedUseCase.desc}
|
||||
</Typography.Paragraph>
|
||||
</Card>
|
||||
@ -327,10 +343,31 @@ export default function Onboarding(): JSX.Element {
|
||||
</div>
|
||||
<div className="continue-to-next-step">
|
||||
<Button type="primary" icon={<ArrowRightOutlined />} onClick={handleNext}>
|
||||
Get Started
|
||||
{t('get_started')}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
<div className="invite-member-wrapper">
|
||||
<Typography.Text className="helper-text">
|
||||
{t('invite_user_helper_text')}
|
||||
</Typography.Text>
|
||||
<div className="invite-member">
|
||||
<Typography.Text>{t('invite_user')}</Typography.Text>
|
||||
<Button
|
||||
onClick={(): void => {
|
||||
logEvent('Onboarding V2: Invite Member', {
|
||||
module: selectedModule?.id,
|
||||
page: 'homepage',
|
||||
});
|
||||
setIsInviteTeamMemberModalOpen(true);
|
||||
}}
|
||||
icon={<UserPlus size={16} />}
|
||||
type="primary"
|
||||
>
|
||||
{t('invite')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeStep > 1 && (
|
||||
@ -345,9 +382,15 @@ export default function Onboarding(): JSX.Element {
|
||||
}}
|
||||
selectedModule={selectedModule}
|
||||
selectedModuleSteps={selectedModuleSteps}
|
||||
setIsInviteTeamMemberModalOpen={setIsInviteTeamMemberModalOpen}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<InviteUserModal
|
||||
form={form}
|
||||
isInviteTeamMemberModalOpen={isInviteTeamMemberModalOpen}
|
||||
toggleModal={toggleModal}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -0,0 +1,127 @@
|
||||
/* eslint-disable sonarjs/no-identical-functions */
|
||||
import { queryByAttribute, waitFor } from '@testing-library/react';
|
||||
import { fireEvent, render, screen, within } from 'tests/test-utils';
|
||||
|
||||
import OnboardingContainer from '..';
|
||||
import { OnboardingContextProvider } from '../context/OnboardingContext';
|
||||
|
||||
jest.mock('react-markdown', () => jest.fn());
|
||||
jest.mock('rehype-raw', () => jest.fn());
|
||||
|
||||
const successNotification = jest.fn();
|
||||
jest.mock('hooks/useNotifications', () => ({
|
||||
__esModule: true,
|
||||
useNotifications: jest.fn(() => ({
|
||||
notifications: {
|
||||
success: successNotification,
|
||||
error: jest.fn(),
|
||||
},
|
||||
})),
|
||||
}));
|
||||
|
||||
window.analytics = {
|
||||
track: jest.fn(),
|
||||
};
|
||||
|
||||
describe('Onboarding invite team member flow', () => {
|
||||
it('initial render and get started page', async () => {
|
||||
const { findByText } = render(
|
||||
<OnboardingContextProvider>
|
||||
<OnboardingContainer />
|
||||
</OnboardingContextProvider>,
|
||||
);
|
||||
|
||||
await expect(findByText('SigNoz')).resolves.toBeInTheDocument();
|
||||
|
||||
// Check all the option present
|
||||
const monitoringTexts = [
|
||||
{
|
||||
title: 'Application Monitoring',
|
||||
description:
|
||||
'Monitor application metrics like p99 latency, error rates, external API calls, and db calls.',
|
||||
},
|
||||
{
|
||||
title: 'Logs Management',
|
||||
description:
|
||||
'Easily filter and query logs, build dashboards and alerts based on attributes in logs',
|
||||
},
|
||||
{
|
||||
title: 'Infrastructure Monitoring',
|
||||
description:
|
||||
'Monitor Kubernetes infrastructure metrics, hostmetrics, or metrics of any third-party integration',
|
||||
},
|
||||
{
|
||||
title: 'AWS Monitoring',
|
||||
description:
|
||||
'Monitor your traces, logs and metrics for AWS services like EC2, ECS, EKS etc.',
|
||||
},
|
||||
{
|
||||
title: 'Azure Monitoring',
|
||||
description:
|
||||
'Monitor your traces, logs and metrics for Azure services like AKS, Container Apps, App Service etc.',
|
||||
},
|
||||
];
|
||||
|
||||
monitoringTexts.forEach(async ({ title, description }) => {
|
||||
await expect(findByText(title)).resolves.toBeInTheDocument();
|
||||
await expect(findByText(description)).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Invite team member button
|
||||
await expect(findByText('invite')).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('invite team member', async () => {
|
||||
const { findByText } = render(
|
||||
<OnboardingContextProvider>
|
||||
<OnboardingContainer />
|
||||
</OnboardingContextProvider>,
|
||||
);
|
||||
|
||||
// Invite team member button
|
||||
const inviteBtn = await findByText('invite');
|
||||
expect(inviteBtn).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(inviteBtn);
|
||||
const inviteModal = await screen.findByTestId('invite-team-members-modal');
|
||||
expect(inviteModal).toBeInTheDocument();
|
||||
|
||||
const inviteModalTitle = await within(inviteModal).findAllByText(
|
||||
/invite_team_members/i,
|
||||
);
|
||||
expect(inviteModalTitle[0]).toBeInTheDocument();
|
||||
|
||||
// Verify that the invite modal contains an input field for entering the email address
|
||||
const emailInput = within(inviteModal).getByText('email_address');
|
||||
expect(emailInput).toBeInTheDocument();
|
||||
|
||||
// Verify that the invite modal contains a dropdown for selecting the role
|
||||
const role = within(inviteModal).getByText('role');
|
||||
expect(role).toBeInTheDocument();
|
||||
|
||||
// Verify that the invite modal contains a button for sending the invitation
|
||||
const sendButton = within(inviteModal).getByTestId(
|
||||
'invite-team-members-button',
|
||||
);
|
||||
expect(sendButton).toBeInTheDocument();
|
||||
|
||||
// Verify that the invite modal sends the invitation
|
||||
fireEvent.input(queryByAttribute('id', inviteModal, 'members_0_email')!, {
|
||||
target: { value: 'test@example.com' },
|
||||
});
|
||||
expect(
|
||||
queryByAttribute('value', inviteModal, 'test@example.com'),
|
||||
).toBeInTheDocument();
|
||||
|
||||
const roleDropdown = within(inviteModal).getByTestId('role-select');
|
||||
expect(roleDropdown).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(sendButton);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(successNotification).toHaveBeenCalledWith({
|
||||
message: 'Invite sent successfully',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
@ -39,6 +39,9 @@
|
||||
.steps-container {
|
||||
width: 20%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
|
||||
.steps-container-header {
|
||||
display: flex;
|
||||
@ -69,6 +72,30 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.invite-user-btn {
|
||||
display: flex;
|
||||
width: 170px;
|
||||
height: 32px;
|
||||
padding: 6px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 2px;
|
||||
margin-bottom: 31px;
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
background: var(--bg-ink-300);
|
||||
box-shadow: none;
|
||||
|
||||
.ant-typography {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 10px;
|
||||
letter-spacing: 0.12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.selected-step-content {
|
||||
@ -196,5 +223,14 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.invite-user-btn {
|
||||
border: 1px solid var(--bg-vanilla-300);
|
||||
background: var(--bg-vanilla-100);
|
||||
|
||||
.ant-typography {
|
||||
color: var(--bg-slate-200);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,8 +18,8 @@ import { hasFrameworks } from 'container/OnboardingContainer/utils/dataSourceUti
|
||||
import useAnalytics from 'hooks/analytics/useAnalytics';
|
||||
import history from 'lib/history';
|
||||
import { isEmpty, isNull } from 'lodash-es';
|
||||
import { HelpCircle } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { HelpCircle, UserPlus } from 'lucide-react';
|
||||
import { SetStateAction, useState } from 'react';
|
||||
|
||||
import { useOnboardingContext } from '../../context/OnboardingContext';
|
||||
import {
|
||||
@ -33,6 +33,7 @@ interface ModuleStepsContainerProps {
|
||||
onReselectModule: any;
|
||||
selectedModule: ModuleProps;
|
||||
selectedModuleSteps: SelectedModuleStepProps[];
|
||||
setIsInviteTeamMemberModalOpen: (value: SetStateAction<boolean>) => void;
|
||||
}
|
||||
|
||||
interface MetaDataProps {
|
||||
@ -63,6 +64,7 @@ export default function ModuleStepsContainer({
|
||||
onReselectModule,
|
||||
selectedModule,
|
||||
selectedModuleSteps,
|
||||
setIsInviteTeamMemberModalOpen,
|
||||
}: ModuleStepsContainerProps): JSX.Element {
|
||||
const {
|
||||
activeStep,
|
||||
@ -409,6 +411,7 @@ Thanks
|
||||
return (
|
||||
<div className="onboarding-module-steps">
|
||||
<div className="steps-container">
|
||||
<div>
|
||||
<div className="steps-container-header">
|
||||
<div className="brand-logo" onClick={handleLogoClick}>
|
||||
<img src="/Logos/signoz-brand-logo.svg" alt="SigNoz" />
|
||||
@ -436,6 +439,20 @@ Thanks
|
||||
items={selectedModuleSteps}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
onClick={(): void => {
|
||||
logEvent('Onboarding V2: Invite Member', {
|
||||
module: selectedModule?.id,
|
||||
page: 'sidebar',
|
||||
});
|
||||
setIsInviteTeamMemberModalOpen(true);
|
||||
}}
|
||||
icon={<UserPlus size={16} />}
|
||||
className="invite-user-btn"
|
||||
>
|
||||
Invite teammates
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="selected-step-content">
|
||||
<div className="step-data">
|
||||
|
@ -50,7 +50,7 @@ function InviteTeamMembers({ form, onFinish }: Props): JSX.Element {
|
||||
<Input placeholder={t('name_placeholder')} />
|
||||
</Form.Item>
|
||||
<Form.Item name={[name, 'role']} initialValue="VIEWER">
|
||||
<SelectDrawer>
|
||||
<SelectDrawer data-testid="role-select">
|
||||
<Select.Option value="ADMIN">ADMIN</Select.Option>
|
||||
<Select.Option value="VIEWER">VIEWER</Select.Option>
|
||||
<Select.Option value="EDITOR">EDITOR</Select.Option>
|
||||
|
@ -0,0 +1,182 @@
|
||||
import { Button, Form, Modal } from 'antd';
|
||||
import { FormInstance } from 'antd/lib';
|
||||
import getPendingInvites from 'api/user/getPendingInvites';
|
||||
import sendInvite from 'api/user/sendInvite';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import {
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQuery } from 'react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { PayloadProps } from 'types/api/user/getPendingInvites';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
import { ROLES } from 'types/roles';
|
||||
|
||||
import InviteTeamMembers from '../InviteTeamMembers';
|
||||
import { InviteMemberFormValues } from '../PendingInvitesContainer';
|
||||
|
||||
export interface InviteUserModalProps {
|
||||
isInviteTeamMemberModalOpen: boolean;
|
||||
toggleModal: (value: boolean) => void;
|
||||
form: FormInstance<InviteMemberFormValues>;
|
||||
setDataSource?: Dispatch<SetStateAction<DataProps[]>>;
|
||||
shouldCallApi?: boolean;
|
||||
}
|
||||
|
||||
interface DataProps {
|
||||
key: number;
|
||||
name: string;
|
||||
email: string;
|
||||
accessLevel: ROLES;
|
||||
inviteLink: string;
|
||||
}
|
||||
|
||||
function InviteUserModal(props: InviteUserModalProps): JSX.Element {
|
||||
const {
|
||||
isInviteTeamMemberModalOpen,
|
||||
toggleModal,
|
||||
form,
|
||||
setDataSource,
|
||||
shouldCallApi = false,
|
||||
} = props;
|
||||
const { notifications } = useNotifications();
|
||||
const { t } = useTranslation(['organizationsettings', 'common']);
|
||||
const { user } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||
const [isInvitingMembers, setIsInvitingMembers] = useState<boolean>(false);
|
||||
const [modalForm] = Form.useForm<InviteMemberFormValues>(form);
|
||||
|
||||
const getPendingInvitesResponse = useQuery({
|
||||
queryFn: getPendingInvites,
|
||||
queryKey: ['getPendingInvites', user?.accessJwt],
|
||||
enabled: shouldCallApi,
|
||||
});
|
||||
|
||||
const getParsedInviteData = useCallback(
|
||||
(payload: PayloadProps = []) =>
|
||||
payload?.map((data) => ({
|
||||
key: data.createdAt,
|
||||
name: data?.name,
|
||||
email: data.email,
|
||||
accessLevel: data.role,
|
||||
inviteLink: `${window.location.origin}${ROUTES.SIGN_UP}?token=${data.token}`,
|
||||
})),
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
getPendingInvitesResponse.status === 'success' &&
|
||||
getPendingInvitesResponse?.data?.payload
|
||||
) {
|
||||
const data = getParsedInviteData(
|
||||
getPendingInvitesResponse?.data?.payload || [],
|
||||
);
|
||||
setDataSource?.(data);
|
||||
}
|
||||
}, [
|
||||
getParsedInviteData,
|
||||
getPendingInvitesResponse?.data?.payload,
|
||||
getPendingInvitesResponse.status,
|
||||
setDataSource,
|
||||
]);
|
||||
|
||||
const onInviteClickHandler = useCallback(
|
||||
async (values: InviteMemberFormValues): Promise<void> => {
|
||||
try {
|
||||
setIsInvitingMembers?.(true);
|
||||
values?.members?.forEach(
|
||||
async (member): Promise<void> => {
|
||||
const { error, statusCode } = await sendInvite({
|
||||
email: member.email,
|
||||
name: member?.name,
|
||||
role: member.role,
|
||||
frontendBaseUrl: window.location.origin,
|
||||
});
|
||||
|
||||
if (statusCode !== 200) {
|
||||
notifications.error({
|
||||
message:
|
||||
error ||
|
||||
t('something_went_wrong', {
|
||||
ns: 'common',
|
||||
}),
|
||||
});
|
||||
} else if (statusCode === 200) {
|
||||
notifications.success({
|
||||
message: 'Invite sent successfully',
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
setTimeout(async () => {
|
||||
const { data, status } = await getPendingInvitesResponse.refetch();
|
||||
if (status === 'success' && data.payload) {
|
||||
setDataSource?.(getParsedInviteData(data?.payload || []));
|
||||
}
|
||||
setIsInvitingMembers?.(false);
|
||||
toggleModal(false);
|
||||
}, 2000);
|
||||
} catch (error) {
|
||||
notifications.error({
|
||||
message: t('something_went_wrong', {
|
||||
ns: 'common',
|
||||
}),
|
||||
});
|
||||
}
|
||||
},
|
||||
[
|
||||
getParsedInviteData,
|
||||
getPendingInvitesResponse,
|
||||
notifications,
|
||||
setDataSource,
|
||||
setIsInvitingMembers,
|
||||
t,
|
||||
toggleModal,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t('invite_team_members')}
|
||||
open={isInviteTeamMemberModalOpen}
|
||||
onCancel={(): void => toggleModal(false)}
|
||||
centered
|
||||
data-testid="invite-team-members-modal"
|
||||
destroyOnClose
|
||||
footer={[
|
||||
<Button key="back" onClick={(): void => toggleModal(false)} type="default">
|
||||
{t('cancel', {
|
||||
ns: 'common',
|
||||
})}
|
||||
</Button>,
|
||||
<Button
|
||||
key={t('invite_team_members').toString()}
|
||||
onClick={modalForm.submit}
|
||||
data-testid="invite-team-members-button"
|
||||
type="primary"
|
||||
disabled={isInvitingMembers}
|
||||
loading={isInvitingMembers}
|
||||
>
|
||||
{t('invite_team_members')}
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<InviteTeamMembers form={modalForm} onFinish={onInviteClickHandler} />
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
InviteUserModal.defaultProps = {
|
||||
setDataSource: (): void => {},
|
||||
shouldCallApi: false,
|
||||
};
|
||||
|
||||
export default InviteUserModal;
|
@ -1,9 +1,8 @@
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { Button, Form, Modal, Space, Typography } from 'antd';
|
||||
import { Button, Form, Space, Typography } from 'antd';
|
||||
import { ColumnsType } from 'antd/lib/table';
|
||||
import deleteInvite from 'api/user/deleteInvite';
|
||||
import getPendingInvites from 'api/user/getPendingInvites';
|
||||
import sendInvite from 'api/user/sendInvite';
|
||||
import { ResizeTable } from 'components/ResizeTable';
|
||||
import { INVITE_MEMBERS_HASH } from 'constants/app';
|
||||
import ROUTES from 'constants/routes';
|
||||
@ -19,7 +18,7 @@ import { PayloadProps } from 'types/api/user/getPendingInvites';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
import { ROLES } from 'types/roles';
|
||||
|
||||
import InviteTeamMembers from '../InviteTeamMembers';
|
||||
import InviteUserModal from '../InviteUserModal/InviteUserModal';
|
||||
import { TitleWrapper } from './styles';
|
||||
|
||||
function PendingInvitesContainer(): JSX.Element {
|
||||
@ -28,7 +27,6 @@ function PendingInvitesContainer(): JSX.Element {
|
||||
setIsInviteTeamMemberModalOpen,
|
||||
] = useState<boolean>(false);
|
||||
const [form] = Form.useForm<InviteMemberFormValues>();
|
||||
const [isInvitingMembers, setIsInvitingMembers] = useState<boolean>(false);
|
||||
const { t } = useTranslation(['organizationsettings', 'common']);
|
||||
const [state, setText] = useCopyToClipboard();
|
||||
const { notifications } = useNotifications();
|
||||
@ -191,83 +189,15 @@ function PendingInvitesContainer(): JSX.Element {
|
||||
},
|
||||
];
|
||||
|
||||
const onInviteClickHandler = useCallback(
|
||||
async (values: InviteMemberFormValues): Promise<void> => {
|
||||
try {
|
||||
setIsInvitingMembers(true);
|
||||
values.members.forEach(
|
||||
async (member): Promise<void> => {
|
||||
const { error, statusCode } = await sendInvite({
|
||||
email: member.email,
|
||||
name: member.name,
|
||||
role: member.role,
|
||||
frontendBaseUrl: window.location.origin,
|
||||
});
|
||||
|
||||
if (statusCode !== 200) {
|
||||
notifications.error({
|
||||
message:
|
||||
error ||
|
||||
t('something_went_wrong', {
|
||||
ns: 'common',
|
||||
}),
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
setTimeout(async () => {
|
||||
const { data, status } = await getPendingInvitesResponse.refetch();
|
||||
if (status === 'success' && data.payload) {
|
||||
setDataSource(getParsedInviteData(data?.payload || []));
|
||||
}
|
||||
setIsInvitingMembers(false);
|
||||
toggleModal(false);
|
||||
}, 2000);
|
||||
} catch (error) {
|
||||
notifications.error({
|
||||
message: t('something_went_wrong', {
|
||||
ns: 'common',
|
||||
}),
|
||||
});
|
||||
}
|
||||
},
|
||||
[
|
||||
getParsedInviteData,
|
||||
getPendingInvitesResponse,
|
||||
notifications,
|
||||
t,
|
||||
toggleModal,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Modal
|
||||
title={t('invite_team_members')}
|
||||
open={isInviteTeamMemberModalOpen}
|
||||
onCancel={(): void => toggleModal(false)}
|
||||
centered
|
||||
destroyOnClose
|
||||
footer={[
|
||||
<Button key="back" onClick={(): void => toggleModal(false)} type="default">
|
||||
{t('cancel', {
|
||||
ns: 'common',
|
||||
})}
|
||||
</Button>,
|
||||
<Button
|
||||
key={t('invite_team_members').toString()}
|
||||
onClick={form.submit}
|
||||
type="primary"
|
||||
disabled={isInvitingMembers}
|
||||
loading={isInvitingMembers}
|
||||
>
|
||||
{t('invite_team_members')}
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<InviteTeamMembers form={form} onFinish={onInviteClickHandler} />
|
||||
</Modal>
|
||||
<InviteUserModal
|
||||
form={form}
|
||||
isInviteTeamMemberModalOpen={isInviteTeamMemberModalOpen}
|
||||
setDataSource={setDataSource}
|
||||
toggleModal={toggleModal}
|
||||
shouldCallApi
|
||||
/>
|
||||
|
||||
<Space direction="vertical" size="middle">
|
||||
<TitleWrapper>
|
||||
|
@ -3,11 +3,19 @@ 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';
|
||||
@ -466,6 +474,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}
|
||||
|
@ -22,7 +22,7 @@ describe('PipelinePage container test', () => {
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should handle search', async () => {
|
||||
it.skip('should handle search', async () => {
|
||||
const setPipelineValue = jest.fn();
|
||||
const { getByPlaceholderText, container } = render(
|
||||
<MemoryRouter>
|
||||
|
@ -10,7 +10,7 @@ interface LeftToolbarActionsProps {
|
||||
selectedView: string;
|
||||
onToggleHistrogramVisibility: () => void;
|
||||
onChangeSelectedView: (view: SELECTED_VIEWS) => void;
|
||||
showHistogram: boolean;
|
||||
showFrequencyChart: boolean;
|
||||
}
|
||||
|
||||
const activeTab = 'active-tab';
|
||||
@ -22,7 +22,7 @@ export default function LeftToolbarActions({
|
||||
selectedView,
|
||||
onToggleHistrogramVisibility,
|
||||
onChangeSelectedView,
|
||||
showHistogram,
|
||||
showFrequencyChart,
|
||||
}: LeftToolbarActionsProps): JSX.Element {
|
||||
const { clickhouse, search, queryBuilder: QB } = items;
|
||||
|
||||
@ -71,11 +71,11 @@ export default function LeftToolbarActions({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="histogram-view-controller">
|
||||
<Typography>Histogram</Typography>
|
||||
<div className="frequency-chart-view-controller">
|
||||
<Typography>Frequency chart</Typography>
|
||||
<Switch
|
||||
size="small"
|
||||
checked={showHistogram}
|
||||
checked={showFrequencyChart}
|
||||
defaultChecked
|
||||
onChange={onToggleHistrogramVisibility}
|
||||
/>
|
||||
|
@ -37,7 +37,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.histogram-view-controller {
|
||||
.frequency-chart-view-controller {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 8px;
|
||||
|
@ -8,7 +8,7 @@ import RightToolbarActions from '../RightToolbarActions';
|
||||
describe('ToolbarActions', () => {
|
||||
it('LeftToolbarActions - renders correctly with default props', async () => {
|
||||
const handleChangeSelectedView = jest.fn();
|
||||
const handleToggleShowHistogram = jest.fn();
|
||||
const handleToggleShowFrequencyChart = jest.fn();
|
||||
const { queryByTestId } = render(
|
||||
<LeftToolbarActions
|
||||
items={{
|
||||
@ -32,8 +32,8 @@ describe('ToolbarActions', () => {
|
||||
}}
|
||||
selectedView={SELECTED_VIEWS.SEARCH}
|
||||
onChangeSelectedView={handleChangeSelectedView}
|
||||
onToggleHistrogramVisibility={handleToggleShowHistogram}
|
||||
showHistogram
|
||||
onToggleHistrogramVisibility={handleToggleShowFrequencyChart}
|
||||
showFrequencyChart
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByTestId('search-view')).toBeInTheDocument();
|
||||
@ -51,7 +51,7 @@ describe('ToolbarActions', () => {
|
||||
|
||||
it('renders - clickhouse view and test histogram toggle', async () => {
|
||||
const handleChangeSelectedView = jest.fn();
|
||||
const handleToggleShowHistogram = jest.fn();
|
||||
const handleToggleShowFrequencyChart = jest.fn();
|
||||
const { queryByTestId, getByRole } = render(
|
||||
<LeftToolbarActions
|
||||
items={{
|
||||
@ -76,8 +76,8 @@ describe('ToolbarActions', () => {
|
||||
}}
|
||||
selectedView={SELECTED_VIEWS.QUERY_BUILDER}
|
||||
onChangeSelectedView={handleChangeSelectedView}
|
||||
onToggleHistrogramVisibility={handleToggleShowHistogram}
|
||||
showHistogram
|
||||
onToggleHistrogramVisibility={handleToggleShowFrequencyChart}
|
||||
showFrequencyChart
|
||||
/>,
|
||||
);
|
||||
|
||||
@ -88,7 +88,7 @@ describe('ToolbarActions', () => {
|
||||
expect(handleChangeSelectedView).toBeCalled();
|
||||
|
||||
await userEvent.click(getByRole('switch'));
|
||||
expect(handleToggleShowHistogram).toBeCalled();
|
||||
expect(handleToggleShowFrequencyChart).toBeCalled();
|
||||
});
|
||||
|
||||
it('RightToolbarActions - render correctly with props', async () => {
|
||||
|
@ -191,6 +191,7 @@ export const GroupByFilter = memo(function GroupByFilter({
|
||||
labelInValue
|
||||
notFoundContent={isFetching ? <Spin size="small" /> : null}
|
||||
onChange={handleChange}
|
||||
data-testid="group-by"
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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 &&
|
||||
|
@ -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',
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
@ -7,6 +7,7 @@ import LogsError from 'container/LogsError/LogsError';
|
||||
import { LogsLoading } from 'container/LogsLoading/LogsLoading';
|
||||
import NoLogs from 'container/NoLogs/NoLogs';
|
||||
import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/config';
|
||||
import { TracesLoading } from 'container/TracesExplorer/TraceLoading/TraceLoading';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import GetMinMax from 'lib/getMinMax';
|
||||
@ -146,14 +147,17 @@ function TimeSeriesView({
|
||||
style={{ height: '100%', width: '100%' }}
|
||||
ref={graphRef}
|
||||
>
|
||||
{isLoading && <LogsLoading />}
|
||||
{isLoading &&
|
||||
(dataSource === DataSource.LOGS ? <LogsLoading /> : <TracesLoading />)}
|
||||
|
||||
{chartData &&
|
||||
chartData[0] &&
|
||||
chartData[0]?.length === 0 &&
|
||||
!isLoading &&
|
||||
!isError &&
|
||||
isFilterApplied && <EmptyLogsSearch />}
|
||||
isFilterApplied && (
|
||||
<EmptyLogsSearch dataSource={dataSource} panelType="TIME_SERIES" />
|
||||
)}
|
||||
|
||||
{chartData &&
|
||||
chartData[0] &&
|
||||
|
@ -14,6 +14,7 @@ import { convertDataValueToMs } from './utils';
|
||||
|
||||
function TimeSeriesViewContainer({
|
||||
dataSource = DataSource.TRACES,
|
||||
isFilterApplied,
|
||||
}: TimeSeriesViewProps): JSX.Element {
|
||||
const { stagedQuery, currentQuery, panelType } = useQueryBuilder();
|
||||
|
||||
@ -70,8 +71,7 @@ function TimeSeriesViewContainer({
|
||||
|
||||
return (
|
||||
<TimeSeriesView
|
||||
// TODO handle this when revamping trace explorer
|
||||
isFilterApplied={false}
|
||||
isFilterApplied={isFilterApplied}
|
||||
isError={isError}
|
||||
isLoading={isLoading}
|
||||
data={responseData}
|
||||
@ -83,6 +83,7 @@ function TimeSeriesViewContainer({
|
||||
|
||||
interface TimeSeriesViewProps {
|
||||
dataSource?: DataSource;
|
||||
isFilterApplied: boolean;
|
||||
}
|
||||
|
||||
TimeSeriesViewContainer.defaultProps = {
|
||||
|
@ -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>
|
||||
|
@ -13,6 +13,9 @@ describe('traces/getTreeLevelsCount', () => {
|
||||
children,
|
||||
serviceName: '',
|
||||
serviceColour: '',
|
||||
spanKind: '',
|
||||
statusCodeString: '',
|
||||
statusMessage: '',
|
||||
});
|
||||
|
||||
test('should return 0 for empty tree', () => {
|
||||
|
@ -37,6 +37,9 @@ test('loads and displays greeting', () => {
|
||||
event: [],
|
||||
hasError: false,
|
||||
parent: undefined,
|
||||
spanKind: '',
|
||||
statusCodeString: '',
|
||||
statusMessage: '',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
@ -4,6 +4,8 @@ import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import EmptyLogsSearch from 'container/EmptyLogsSearch/EmptyLogsSearch';
|
||||
import NoLogs from 'container/NoLogs/NoLogs';
|
||||
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||
import TraceExplorerControls from 'container/TracesExplorer/Controls';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
@ -20,11 +22,16 @@ import { AppState } from 'store/reducers';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import { TracesLoading } from '../TraceLoading/TraceLoading';
|
||||
import { defaultSelectedColumns, PER_PAGE_OPTIONS } from './configs';
|
||||
import { Container, ErrorText, tableStyles } from './styles';
|
||||
import { getListColumns, getTraceLink, transformDataWithDate } from './utils';
|
||||
|
||||
function ListView(): JSX.Element {
|
||||
interface ListViewProps {
|
||||
isFilterApplied: boolean;
|
||||
}
|
||||
|
||||
function ListView({ isFilterApplied }: ListViewProps): JSX.Element {
|
||||
const { stagedQuery, panelType } = useQueryBuilder();
|
||||
|
||||
const { selectedTime: globalSelectedTime, maxTime, minTime } = useSelector<
|
||||
@ -49,7 +56,7 @@ function ListView(): JSX.Element {
|
||||
QueryParams.pagination,
|
||||
);
|
||||
|
||||
const { data, isFetching, isError } = useGetQueryRange(
|
||||
const { data, isFetching, isLoading, isError } = useGetQueryRange(
|
||||
{
|
||||
query: stagedQuery || initialQueriesMap.traces,
|
||||
graphType: panelType || PANEL_TYPES.LIST,
|
||||
@ -122,18 +129,38 @@ function ListView(): JSX.Element {
|
||||
[columns, onDragColumns],
|
||||
);
|
||||
|
||||
const isDataPresent =
|
||||
!isLoading &&
|
||||
!isFetching &&
|
||||
!isError &&
|
||||
transformedQueryTableData.length === 0;
|
||||
|
||||
return (
|
||||
<Container>
|
||||
{transformedQueryTableData.length !== 0 && (
|
||||
<TraceExplorerControls
|
||||
isLoading={isFetching}
|
||||
totalCount={totalCount}
|
||||
config={config}
|
||||
perPageOptions={PER_PAGE_OPTIONS}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isError && <ErrorText>{data?.error || 'Something went wrong'}</ErrorText>}
|
||||
|
||||
{!isError && (
|
||||
{(isLoading || (isFetching && transformedQueryTableData.length === 0)) && (
|
||||
<TracesLoading />
|
||||
)}
|
||||
|
||||
{isDataPresent && !isFilterApplied && (
|
||||
<NoLogs dataSource={DataSource.TRACES} />
|
||||
)}
|
||||
|
||||
{isDataPresent && isFilterApplied && (
|
||||
<EmptyLogsSearch dataSource={DataSource.TRACES} panelType="LIST" />
|
||||
)}
|
||||
|
||||
{!isError && transformedQueryTableData.length !== 0 && (
|
||||
<ResizeTable
|
||||
tableLayout="fixed"
|
||||
pagination={false}
|
||||
|
@ -0,0 +1,19 @@
|
||||
.loading-traces {
|
||||
padding: 24px 0;
|
||||
height: 240px;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
|
||||
.loading-traces-content {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
|
||||
.loading-gif {
|
||||
height: 72px;
|
||||
margin-left: -24px;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
import './TraceLoading.styles.scss';
|
||||
|
||||
import { Typography } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
export function TracesLoading(): JSX.Element {
|
||||
const { t } = useTranslation('common');
|
||||
return (
|
||||
<div className="loading-traces">
|
||||
<div className="loading-traces-content">
|
||||
<img
|
||||
className="loading-gif"
|
||||
src="/Icons/loading-plane.gif"
|
||||
alt="wait-icon"
|
||||
/>
|
||||
|
||||
<Typography>
|
||||
{t('pending_data_placeholder', { dataSource: DataSource.TRACES })}
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -7,7 +7,6 @@ import { generatePath, Link } from 'react-router-dom';
|
||||
import { ListItem } from 'types/api/widgets/getQuery';
|
||||
|
||||
export const PER_PAGE_OPTIONS: number[] = [10, ...DEFAULT_PER_PAGE_OPTIONS];
|
||||
export const TRACES_DETAILS_LINK = 'https://signoz.io/docs/userguide/traces/';
|
||||
|
||||
export const columns: ColumnsType<ListItem['data']> = [
|
||||
{
|
||||
|
@ -4,6 +4,8 @@ import { DEFAULT_ENTITY_VERSION } from 'constants/app';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import EmptyLogsSearch from 'container/EmptyLogsSearch/EmptyLogsSearch';
|
||||
import NoLogs from 'container/NoLogs/NoLogs';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { Pagination } from 'hooks/queryPagination';
|
||||
@ -11,13 +13,20 @@ import useUrlQueryData from 'hooks/useUrlQueryData';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import DOCLINKS from 'utils/docLinks';
|
||||
|
||||
import TraceExplorerControls from '../Controls';
|
||||
import { columns, PER_PAGE_OPTIONS, TRACES_DETAILS_LINK } from './configs';
|
||||
import { TracesLoading } from '../TraceLoading/TraceLoading';
|
||||
import { columns, PER_PAGE_OPTIONS } from './configs';
|
||||
import { ActionsContainer, Container } from './styles';
|
||||
|
||||
function TracesView(): JSX.Element {
|
||||
interface TracesViewProps {
|
||||
isFilterApplied: boolean;
|
||||
}
|
||||
|
||||
function TracesView({ isFilterApplied }: TracesViewProps): JSX.Element {
|
||||
const { stagedQuery, panelType } = useQueryBuilder();
|
||||
|
||||
const { selectedTime: globalSelectedTime, maxTime, minTime } = useSelector<
|
||||
@ -29,7 +38,7 @@ function TracesView(): JSX.Element {
|
||||
QueryParams.pagination,
|
||||
);
|
||||
|
||||
const { data, isLoading } = useGetQueryRange(
|
||||
const { data, isLoading, isFetching, isError } = useGetQueryRange(
|
||||
{
|
||||
query: stagedQuery || initialQueriesMap.traces,
|
||||
graphType: panelType || PANEL_TYPES.TRACE,
|
||||
@ -65,10 +74,11 @@ function TracesView(): JSX.Element {
|
||||
|
||||
return (
|
||||
<Container>
|
||||
{(tableData || []).length !== 0 && (
|
||||
<ActionsContainer>
|
||||
<Typography>
|
||||
This tab only shows Root Spans. More details
|
||||
<Typography.Link href={TRACES_DETAILS_LINK} target="_blank">
|
||||
<Typography.Link href={DOCLINKS.TRACES_DETAILS_LINK} target="_blank">
|
||||
{' '}
|
||||
here
|
||||
</Typography.Link>
|
||||
@ -79,6 +89,27 @@ function TracesView(): JSX.Element {
|
||||
perPageOptions={PER_PAGE_OPTIONS}
|
||||
/>
|
||||
</ActionsContainer>
|
||||
)}
|
||||
|
||||
{(isLoading || (isFetching && (tableData || []).length === 0)) && (
|
||||
<TracesLoading />
|
||||
)}
|
||||
|
||||
{!isLoading &&
|
||||
!isFetching &&
|
||||
!isError &&
|
||||
!isFilterApplied &&
|
||||
(tableData || []).length === 0 && <NoLogs dataSource={DataSource.TRACES} />}
|
||||
|
||||
{!isLoading &&
|
||||
!isFetching &&
|
||||
(tableData || []).length === 0 &&
|
||||
!isError &&
|
||||
isFilterApplied && (
|
||||
<EmptyLogsSearch dataSource={DataSource.TRACES} panelType="TRACE" />
|
||||
)}
|
||||
|
||||
{(tableData || []).length !== 0 && (
|
||||
<ResizeTable
|
||||
loading={isLoading}
|
||||
columns={columns}
|
||||
@ -87,6 +118,7 @@ function TracesView(): JSX.Element {
|
||||
scroll={{ x: true }}
|
||||
pagination={false}
|
||||
/>
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
@ -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,6 +88,7 @@ function TracesTableComponent({
|
||||
return (
|
||||
<div className="traces-table">
|
||||
<div className="resize-table">
|
||||
<OverlayScrollbar>
|
||||
<Table
|
||||
pagination={false}
|
||||
tableLayout="fixed"
|
||||
@ -97,6 +100,7 @@ function TracesTableComponent({
|
||||
onRow={handleRow}
|
||||
sticky
|
||||
/>
|
||||
</OverlayScrollbar>
|
||||
</div>
|
||||
<div className="controller">
|
||||
<Controls
|
||||
|
@ -90,8 +90,14 @@ function KeyboardHotkeysProvider({
|
||||
(keyCombination: string, callback: () => void): void => {
|
||||
if (!shortcuts.current[keyCombination]) {
|
||||
shortcuts.current[keyCombination] = callback;
|
||||
} else if (process.env.NODE_ENV === 'development') {
|
||||
throw new Error(
|
||||
`This shortcut is already present in current scope :- ${keyCombination}`,
|
||||
);
|
||||
} else {
|
||||
throw new Error('This shortcut is already present in current scope');
|
||||
console.error(
|
||||
`This shortcut is already present in current scope :- ${keyCombination}`,
|
||||
);
|
||||
}
|
||||
},
|
||||
[shortcuts],
|
||||
|
@ -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,
|
||||
@ -57,6 +77,7 @@ const useCreateAlerts = (widget?: Widgets): VoidFunction => {
|
||||
});
|
||||
},
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
globalSelectedInterval,
|
||||
notifications,
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user