mirror of
https://github.com/SigNoz/signoz.git
synced 2026-05-06 02:20:31 +01:00
Compare commits
53 Commits
v0.49.0-cl
...
help-suppo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e31f58a338 | ||
|
|
3ecb2e35ef | ||
|
|
9844dcdfb7 | ||
|
|
ddf5569ce9 | ||
|
|
83455e614e | ||
|
|
831de18464 | ||
|
|
3b2a811f7b | ||
|
|
2c7a5126fd | ||
|
|
87f1597d4e | ||
|
|
916663b4d5 | ||
|
|
b0e355eb64 | ||
|
|
69a39531f0 | ||
|
|
9c9ed741b2 | ||
|
|
e6eaaa660a | ||
|
|
79eef5bb91 | ||
|
|
4d64f1dede | ||
|
|
bf177882e6 | ||
|
|
f6b29999c9 | ||
|
|
75815897b0 | ||
|
|
c9309eecaa | ||
|
|
4264fc0f3a | ||
|
|
ef854910db | ||
|
|
6b8b2ae761 | ||
|
|
a48340a2ea | ||
|
|
e542d2ee09 | ||
|
|
08431131a9 | ||
|
|
1b0ec8ac43 | ||
|
|
2e0ddc7c7f | ||
|
|
858a0cb0de | ||
|
|
216ad36234 | ||
|
|
6628abd435 | ||
|
|
7c81270ed9 | ||
|
|
81c3e6fa65 | ||
|
|
d215ce09b0 | ||
|
|
161a69fbe9 | ||
|
|
3ee51770fd | ||
|
|
932b7ddc69 | ||
|
|
6e466df89d | ||
|
|
326dec21fd | ||
|
|
b0b69c83db | ||
|
|
02106277a6 | ||
|
|
b34509215e | ||
|
|
fd603b8fdf | ||
|
|
c6e9eeeee6 | ||
|
|
97b66741a7 | ||
|
|
6b234da969 | ||
|
|
9dbef080c6 | ||
|
|
6c192f1242 | ||
|
|
537641000d | ||
|
|
4916cf5083 | ||
|
|
b57a24a177 | ||
|
|
a6e005e3a2 | ||
|
|
4d375e7cc3 |
@@ -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.46.0
|
||||
image: signoz/query-service:0.49.1
|
||||
command:
|
||||
[
|
||||
"-config=/root/config/prometheus.yml",
|
||||
@@ -186,7 +186,7 @@ services:
|
||||
<<: *db-depend
|
||||
|
||||
frontend:
|
||||
image: signoz/frontend:0.46.0
|
||||
image: signoz/frontend:0.48.0
|
||||
deploy:
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
@@ -199,7 +199,7 @@ services:
|
||||
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
||||
|
||||
otel-collector:
|
||||
image: signoz/signoz-otel-collector:0.88.24
|
||||
image: signoz/signoz-otel-collector:0.102.2
|
||||
command:
|
||||
[
|
||||
"--config=/etc/otel-collector-config.yaml",
|
||||
@@ -237,7 +237,7 @@ services:
|
||||
- query-service
|
||||
|
||||
otel-collector-migrator:
|
||||
image: signoz/signoz-schema-migrator:0.88.24
|
||||
image: signoz/signoz-schema-migrator:0.102.2
|
||||
deploy:
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
|
||||
@@ -66,7 +66,7 @@ services:
|
||||
- --storage.path=/data
|
||||
|
||||
otel-collector-migrator:
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.24}
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.2}
|
||||
container_name: otel-migrator
|
||||
command:
|
||||
- "--dsn=tcp://clickhouse:9000"
|
||||
@@ -81,7 +81,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`
|
||||
otel-collector:
|
||||
container_name: signoz-otel-collector
|
||||
image: signoz/signoz-otel-collector:0.88.24
|
||||
image: signoz/signoz-otel-collector:0.102.2
|
||||
command:
|
||||
[
|
||||
"--config=/etc/otel-collector-config.yaml",
|
||||
|
||||
@@ -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.46.0}
|
||||
image: signoz/query-service:${DOCKER_TAG:-0.49.1}
|
||||
container_name: signoz-query-service
|
||||
command:
|
||||
[
|
||||
@@ -204,7 +204,7 @@ services:
|
||||
<<: *db-depend
|
||||
|
||||
frontend:
|
||||
image: signoz/frontend:${DOCKER_TAG:-0.46.0}
|
||||
image: signoz/frontend:${DOCKER_TAG:-0.49.1}
|
||||
container_name: signoz-frontend
|
||||
restart: on-failure
|
||||
depends_on:
|
||||
@@ -216,7 +216,7 @@ services:
|
||||
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
||||
|
||||
otel-collector-migrator:
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.24}
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.2}
|
||||
container_name: otel-migrator
|
||||
command:
|
||||
- "--dsn=tcp://clickhouse:9000"
|
||||
@@ -230,7 +230,7 @@ services:
|
||||
|
||||
|
||||
otel-collector:
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.88.24}
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.102.2}
|
||||
container_name: signoz-otel-collector
|
||||
command:
|
||||
[
|
||||
|
||||
@@ -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.46.0}
|
||||
image: signoz/query-service:${DOCKER_TAG:-0.49.1}
|
||||
container_name: signoz-query-service
|
||||
command:
|
||||
[
|
||||
@@ -203,7 +203,7 @@ services:
|
||||
<<: *db-depend
|
||||
|
||||
frontend:
|
||||
image: signoz/frontend:${DOCKER_TAG:-0.46.0}
|
||||
image: signoz/frontend:${DOCKER_TAG:-0.49.1}
|
||||
container_name: signoz-frontend
|
||||
restart: on-failure
|
||||
depends_on:
|
||||
@@ -215,7 +215,7 @@ services:
|
||||
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
||||
|
||||
otel-collector-migrator:
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.24}
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.2}
|
||||
container_name: otel-migrator
|
||||
command:
|
||||
- "--dsn=tcp://clickhouse:9000"
|
||||
@@ -229,7 +229,7 @@ services:
|
||||
|
||||
|
||||
otel-collector:
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.88.24}
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.102.2}
|
||||
container_name: signoz-otel-collector
|
||||
command:
|
||||
[
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -88,7 +88,7 @@
|
||||
"lucide-react": "0.379.0",
|
||||
"mini-css-extract-plugin": "2.4.5",
|
||||
"papaparse": "5.4.1",
|
||||
"posthog-js": "1.140.1",
|
||||
"posthog-js": "1.142.1",
|
||||
"rc-tween-one": "3.0.6",
|
||||
"react": "18.2.0",
|
||||
"react-addons-update": "15.6.3",
|
||||
|
||||
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"
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { Table } from 'antd';
|
||||
import { ColumnsType } from 'antd/lib/table';
|
||||
import { dragColumnParams } from 'hooks/useDragColumns/configs';
|
||||
import { set } from 'lodash-es';
|
||||
import {
|
||||
SyntheticEvent,
|
||||
useCallback,
|
||||
@@ -59,15 +60,21 @@ function ResizeTable({
|
||||
[columnsData, onDragColumn, handleResize],
|
||||
);
|
||||
|
||||
const tableParams = useMemo(
|
||||
() => ({
|
||||
const tableParams = useMemo(() => {
|
||||
const props = {
|
||||
...restProps,
|
||||
components: { header: { cell: ResizableHeader } },
|
||||
columns: mergedColumns,
|
||||
pagination: { ...pagination, hideOnSinglePage: true },
|
||||
}),
|
||||
[mergedColumns, pagination, restProps],
|
||||
);
|
||||
};
|
||||
|
||||
set(
|
||||
props,
|
||||
'pagination',
|
||||
pagination ? { ...pagination, hideOnSinglePage: true } : false,
|
||||
);
|
||||
|
||||
return props;
|
||||
}, [mergedColumns, pagination, restProps]);
|
||||
|
||||
useEffect(() => {
|
||||
if (columns) {
|
||||
|
||||
@@ -6,7 +6,6 @@ import './AppLayout.styles.scss';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { Flex } from 'antd';
|
||||
import getLocalStorageKey from 'api/browser/localstorage/get';
|
||||
import getDynamicConfigs from 'api/dynamicConfigs/getDynamicConfigs';
|
||||
import getUserLatestVersion from 'api/user/getLatestVersion';
|
||||
import getUserVersion from 'api/user/getVersion';
|
||||
import cx from 'classnames';
|
||||
@@ -38,7 +37,6 @@ import { sideBarCollapse } from 'store/actions';
|
||||
import { AppState } from 'store/reducers';
|
||||
import AppActions from 'types/actions';
|
||||
import {
|
||||
UPDATE_CONFIGS,
|
||||
UPDATE_CURRENT_ERROR,
|
||||
UPDATE_CURRENT_VERSION,
|
||||
UPDATE_LATEST_VERSION,
|
||||
@@ -66,11 +64,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
const { pathname } = useLocation();
|
||||
const { t } = useTranslation(['titles']);
|
||||
|
||||
const [
|
||||
getUserVersionResponse,
|
||||
getUserLatestVersionResponse,
|
||||
getDynamicConfigsResponse,
|
||||
] = useQueries([
|
||||
const [getUserVersionResponse, getUserLatestVersionResponse] = useQueries([
|
||||
{
|
||||
queryFn: getUserVersion,
|
||||
queryKey: ['getUserVersion', user?.accessJwt],
|
||||
@@ -81,10 +75,6 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
queryKey: ['getUserLatestVersion', user?.accessJwt],
|
||||
enabled: isLoggedIn,
|
||||
},
|
||||
{
|
||||
queryFn: getDynamicConfigs,
|
||||
queryKey: ['getDynamicConfigs', user?.accessJwt],
|
||||
},
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -95,15 +85,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
if (getUserVersionResponse.status === 'idle' && isLoggedIn) {
|
||||
getUserVersionResponse.refetch();
|
||||
}
|
||||
if (getDynamicConfigsResponse.status === 'idle') {
|
||||
getDynamicConfigsResponse.refetch();
|
||||
}
|
||||
}, [
|
||||
getUserLatestVersionResponse,
|
||||
getUserVersionResponse,
|
||||
isLoggedIn,
|
||||
getDynamicConfigsResponse,
|
||||
]);
|
||||
}, [getUserLatestVersionResponse, getUserVersionResponse, isLoggedIn]);
|
||||
|
||||
const { children } = props;
|
||||
|
||||
@@ -111,7 +93,6 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
|
||||
const latestCurrentCounter = useRef(0);
|
||||
const latestVersionCounter = useRef(0);
|
||||
const latestConfigCounter = useRef(0);
|
||||
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
@@ -189,23 +170,6 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
getDynamicConfigsResponse.isFetched &&
|
||||
getDynamicConfigsResponse.isSuccess &&
|
||||
getDynamicConfigsResponse.data &&
|
||||
getDynamicConfigsResponse.data.payload &&
|
||||
latestConfigCounter.current === 0
|
||||
) {
|
||||
latestConfigCounter.current = 1;
|
||||
|
||||
dispatch({
|
||||
type: UPDATE_CONFIGS,
|
||||
payload: {
|
||||
configs: getDynamicConfigsResponse.data.payload,
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [
|
||||
dispatch,
|
||||
isLoggedIn,
|
||||
@@ -220,9 +184,6 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
getUserLatestVersionResponse.isFetched,
|
||||
getUserVersionResponse.isFetched,
|
||||
getUserLatestVersionResponse.isSuccess,
|
||||
getDynamicConfigsResponse.data,
|
||||
getDynamicConfigsResponse.isFetched,
|
||||
getDynamicConfigsResponse.isSuccess,
|
||||
notifications,
|
||||
]);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
@@ -70,6 +71,7 @@ function ExportPanelContainer({
|
||||
ns: 'dashboard',
|
||||
}),
|
||||
uploadedGrafana: false,
|
||||
version: ENTITY_VERSION_V4,
|
||||
});
|
||||
}, [t, createNewDashboard]);
|
||||
|
||||
|
||||
@@ -246,17 +246,19 @@ function ChartPreview({
|
||||
return (
|
||||
<ChartContainer>
|
||||
{headline}
|
||||
{(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%" />
|
||||
)}
|
||||
|
||||
<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 && (
|
||||
<GridPanelSwitch
|
||||
options={options}
|
||||
panelType={graphType}
|
||||
@@ -268,8 +270,8 @@ function ChartPreview({
|
||||
query={query || initialQueriesMap.metrics}
|
||||
yAxisUnit={yAxisUnit}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
</ChartContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ function FullView({
|
||||
globalSelectedInterval: globalSelectedTime,
|
||||
variables: getDashboardVariables(selectedDashboard?.data.variables),
|
||||
fillGaps: widget.fillSpans,
|
||||
formatForWeb: getGraphType(widget.panelTypes) === PANEL_TYPES.TABLE,
|
||||
formatForWeb: widget.panelTypes === PANEL_TYPES.TABLE,
|
||||
};
|
||||
}
|
||||
updatedQuery.builder.queryData[0].pageSize = 10;
|
||||
|
||||
@@ -109,7 +109,7 @@ function GridCardGraph({
|
||||
globalSelectedInterval,
|
||||
variables: getDashboardVariables(variables),
|
||||
fillGaps: widget.fillSpans,
|
||||
formatForWeb: getGraphType(widget.panelTypes) === PANEL_TYPES.TABLE,
|
||||
formatForWeb: widget.panelTypes === PANEL_TYPES.TABLE,
|
||||
};
|
||||
}
|
||||
updatedQuery.builder.queryData[0].pageSize = 10;
|
||||
|
||||
@@ -141,11 +141,6 @@ function ImportJSON({
|
||||
colors: {
|
||||
'editor.background': Color.BG_INK_300,
|
||||
},
|
||||
fontFamily: 'Space Mono',
|
||||
fontSize: 20,
|
||||
fontWeight: 'normal',
|
||||
lineHeight: 18,
|
||||
letterSpacing: -0.06,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -233,6 +228,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}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -63,10 +63,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();
|
||||
@@ -561,7 +561,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>
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-right: 16px;
|
||||
|
||||
.dashboard-breadcrumbs {
|
||||
height: 48px;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -12,6 +12,7 @@ function WidgetGraphContainer({
|
||||
queryResponse,
|
||||
setRequestData,
|
||||
selectedWidget,
|
||||
isLoadingPanelData,
|
||||
}: WidgetGraphContainerProps): JSX.Element {
|
||||
if (queryResponse.data && selectedGraph === PANEL_TYPES.BAR) {
|
||||
const sortedSeriesData = getSortedSeriesData(
|
||||
@@ -36,6 +37,10 @@ function WidgetGraphContainer({
|
||||
return <Spinner size="large" tip="Loading..." />;
|
||||
}
|
||||
|
||||
if (isLoadingPanelData) {
|
||||
return <Spinner size="large" tip="Loading..." />;
|
||||
}
|
||||
|
||||
if (
|
||||
selectedGraph !== PANEL_TYPES.LIST &&
|
||||
queryResponse.data?.payload.data?.result?.length === 0
|
||||
|
||||
@@ -17,6 +17,7 @@ function WidgetGraph({
|
||||
queryResponse,
|
||||
setRequestData,
|
||||
selectedWidget,
|
||||
isLoadingPanelData,
|
||||
}: WidgetGraphContainerProps): JSX.Element {
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
|
||||
@@ -43,6 +44,7 @@ function WidgetGraph({
|
||||
)}
|
||||
|
||||
<WidgetGraphComponent
|
||||
isLoadingPanelData={isLoadingPanelData}
|
||||
selectedGraph={selectedGraph}
|
||||
queryResponse={queryResponse}
|
||||
setRequestData={setRequestData}
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
import './LeftContainer.styles.scss';
|
||||
|
||||
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
|
||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { memo, useEffect, useState } from 'react';
|
||||
import { memo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { getGraphType } from 'utils/getGraphType';
|
||||
|
||||
import { WidgetGraphProps } from '../types';
|
||||
import ExplorerColumnsRenderer from './ExplorerColumnsRenderer';
|
||||
@@ -27,68 +24,17 @@ function LeftContainer({
|
||||
selectedTracesFields,
|
||||
setSelectedTracesFields,
|
||||
selectedWidget,
|
||||
selectedTime,
|
||||
requestData,
|
||||
setRequestData,
|
||||
isLoadingPanelData,
|
||||
}: WidgetGraphProps): JSX.Element {
|
||||
const { stagedQuery, redirectWithQueryBuilderData } = useQueryBuilder();
|
||||
const { stagedQuery } = useQueryBuilder();
|
||||
const { selectedDashboard } = useDashboard();
|
||||
|
||||
const { selectedTime: globalSelectedInterval } = useSelector<
|
||||
AppState,
|
||||
GlobalReducer
|
||||
>((state) => state.globalTime);
|
||||
|
||||
const [requestData, setRequestData] = useState<GetQueryResultsProps>(() => {
|
||||
if (selectedWidget && selectedGraph !== PANEL_TYPES.LIST) {
|
||||
return {
|
||||
selectedTime: selectedWidget?.timePreferance,
|
||||
graphType: getGraphType(selectedGraph || selectedWidget.panelTypes),
|
||||
query: stagedQuery || initialQueriesMap.metrics,
|
||||
globalSelectedInterval,
|
||||
formatForWeb:
|
||||
getGraphType(selectedGraph || selectedWidget.panelTypes) ===
|
||||
PANEL_TYPES.TABLE,
|
||||
variables: getDashboardVariables(selectedDashboard?.data.variables),
|
||||
};
|
||||
}
|
||||
const updatedQuery = { ...(stagedQuery || initialQueriesMap.metrics) };
|
||||
updatedQuery.builder.queryData[0].pageSize = 10;
|
||||
redirectWithQueryBuilderData(updatedQuery);
|
||||
return {
|
||||
query: updatedQuery,
|
||||
graphType: PANEL_TYPES.LIST,
|
||||
selectedTime: selectedTime.enum || 'GLOBAL_TIME',
|
||||
globalSelectedInterval,
|
||||
tableParams: {
|
||||
pagination: {
|
||||
offset: 0,
|
||||
limit: updatedQuery.builder.queryData[0].limit || 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (stagedQuery) {
|
||||
setRequestData((prev) => ({
|
||||
...prev,
|
||||
selectedTime: selectedTime.enum || prev.selectedTime,
|
||||
globalSelectedInterval,
|
||||
graphType: getGraphType(selectedGraph || selectedWidget.panelTypes),
|
||||
query: stagedQuery,
|
||||
fillGaps: selectedWidget.fillSpans || false,
|
||||
formatForWeb:
|
||||
getGraphType(selectedGraph || selectedWidget.panelTypes) ===
|
||||
PANEL_TYPES.TABLE,
|
||||
}));
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
stagedQuery,
|
||||
selectedTime,
|
||||
selectedWidget.fillSpans,
|
||||
globalSelectedInterval,
|
||||
]);
|
||||
|
||||
const queryResponse = useGetQueryRange(
|
||||
requestData,
|
||||
selectedDashboard?.data?.version || DEFAULT_ENTITY_VERSION,
|
||||
@@ -110,6 +56,7 @@ function LeftContainer({
|
||||
queryResponse={queryResponse}
|
||||
setRequestData={setRequestData}
|
||||
selectedWidget={selectedWidget}
|
||||
isLoadingPanelData={isLoadingPanelData}
|
||||
/>
|
||||
<QueryContainer className="query-section-left-container">
|
||||
<QuerySection selectedGraph={selectedGraph} queryResponse={queryResponse} />
|
||||
|
||||
@@ -7,7 +7,7 @@ import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn';
|
||||
import { chartHelpMessage } from 'components/facingIssueBtn/util';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { DashboardShortcuts } from 'constants/shortcuts/DashboardShortcuts';
|
||||
import { DEFAULT_BUCKET_COUNT } from 'container/PanelWrapper/constants';
|
||||
@@ -18,6 +18,8 @@ import useAxiosError from 'hooks/useAxiosError';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { MESSAGE, useIsFeatureDisabled } from 'hooks/useFeatureFlag';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
|
||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
import history from 'lib/history';
|
||||
import { defaultTo, isUndefined } from 'lodash-es';
|
||||
import { Check, X } from 'lucide-react';
|
||||
@@ -38,6 +40,8 @@ import { IField } from 'types/api/logs/fields';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { getGraphType, getGraphTypeForFormat } from 'utils/getGraphType';
|
||||
|
||||
import LeftContainer from './LeftContainer';
|
||||
import QueryTypeTag from './LeftContainer/QueryTypeTag';
|
||||
@@ -83,6 +87,10 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
const { featureResponse } = useSelector<AppState, AppReducer>(
|
||||
(state) => state.app,
|
||||
);
|
||||
const { selectedTime: globalSelectedInterval } = useSelector<
|
||||
AppState,
|
||||
GlobalReducer
|
||||
>((state) => state.globalTime);
|
||||
|
||||
const { widgets = [] } = selectedDashboard?.data || {};
|
||||
|
||||
@@ -278,6 +286,65 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
|
||||
const handleError = useAxiosError();
|
||||
|
||||
// this loading state is to take care of mismatch in the responses for table and other panels
|
||||
// hence while changing the query contains the older value and the processing logic fails
|
||||
const [isLoadingPanelData, setIsLoadingPanelData] = useState<boolean>(false);
|
||||
|
||||
// request data should be handled by the parent and the child components should consume the same
|
||||
// this has been moved here from the left container
|
||||
const [requestData, setRequestData] = useState<GetQueryResultsProps>(() => {
|
||||
if (selectedWidget && selectedGraph !== PANEL_TYPES.LIST) {
|
||||
return {
|
||||
selectedTime: selectedWidget?.timePreferance,
|
||||
graphType: getGraphType(selectedGraph || selectedWidget.panelTypes),
|
||||
query: stagedQuery || initialQueriesMap.metrics,
|
||||
globalSelectedInterval,
|
||||
formatForWeb:
|
||||
getGraphTypeForFormat(selectedGraph || selectedWidget.panelTypes) ===
|
||||
PANEL_TYPES.TABLE,
|
||||
variables: getDashboardVariables(selectedDashboard?.data.variables),
|
||||
};
|
||||
}
|
||||
const updatedQuery = { ...(stagedQuery || initialQueriesMap.metrics) };
|
||||
updatedQuery.builder.queryData[0].pageSize = 10;
|
||||
redirectWithQueryBuilderData(updatedQuery);
|
||||
return {
|
||||
query: updatedQuery,
|
||||
graphType: PANEL_TYPES.LIST,
|
||||
selectedTime: selectedTime.enum || 'GLOBAL_TIME',
|
||||
globalSelectedInterval,
|
||||
tableParams: {
|
||||
pagination: {
|
||||
offset: 0,
|
||||
limit: updatedQuery.builder.queryData[0].limit || 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (stagedQuery) {
|
||||
setIsLoadingPanelData(false);
|
||||
setRequestData((prev) => ({
|
||||
...prev,
|
||||
selectedTime: selectedTime.enum || prev.selectedTime,
|
||||
globalSelectedInterval,
|
||||
graphType: getGraphType(selectedGraph || selectedWidget.panelTypes),
|
||||
query: stagedQuery,
|
||||
fillGaps: selectedWidget.fillSpans || false,
|
||||
formatForWeb:
|
||||
getGraphTypeForFormat(selectedGraph || selectedWidget.panelTypes) ===
|
||||
PANEL_TYPES.TABLE,
|
||||
}));
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
stagedQuery,
|
||||
selectedTime,
|
||||
selectedWidget.fillSpans,
|
||||
globalSelectedInterval,
|
||||
]);
|
||||
|
||||
const onClickSaveHandler = useCallback(() => {
|
||||
if (!selectedDashboard) {
|
||||
return;
|
||||
@@ -402,6 +469,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
}, [dashboardId]);
|
||||
|
||||
const setGraphHandler = (type: PANEL_TYPES): void => {
|
||||
setIsLoadingPanelData(true);
|
||||
const updatedQuery = handleQueryChange(type as any, supersetQuery);
|
||||
setGraphType(type);
|
||||
redirectWithQueryBuilderData(
|
||||
@@ -527,6 +595,9 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
setSelectedTracesFields={setSelectedTracesFields}
|
||||
selectedWidget={selectedWidget}
|
||||
selectedTime={selectedTime}
|
||||
requestData={requestData}
|
||||
setRequestData={setRequestData}
|
||||
isLoadingPanelData={isLoadingPanelData}
|
||||
/>
|
||||
)}
|
||||
</LeftContainerWrapper>
|
||||
|
||||
@@ -24,6 +24,9 @@ export interface WidgetGraphProps {
|
||||
selectedWidget: Widgets;
|
||||
selectedGraph: PANEL_TYPES;
|
||||
selectedTime: timePreferance;
|
||||
requestData: GetQueryResultsProps;
|
||||
setRequestData: Dispatch<SetStateAction<GetQueryResultsProps>>;
|
||||
isLoadingPanelData: boolean;
|
||||
}
|
||||
|
||||
export type WidgetGraphContainerProps = {
|
||||
@@ -34,4 +37,5 @@ export type WidgetGraphContainerProps = {
|
||||
setRequestData: Dispatch<SetStateAction<GetQueryResultsProps>>;
|
||||
selectedGraph: PANEL_TYPES;
|
||||
selectedWidget: Widgets;
|
||||
isLoadingPanelData: boolean;
|
||||
};
|
||||
|
||||
@@ -372,8 +372,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ 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,
|
||||
@@ -25,8 +26,10 @@ export default function NoLogs({
|
||||
? 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,32 +411,47 @@ Thanks
|
||||
return (
|
||||
<div className="onboarding-module-steps">
|
||||
<div className="steps-container">
|
||||
<div className="steps-container-header">
|
||||
<div className="brand-logo" onClick={handleLogoClick}>
|
||||
<img src="/Logos/signoz-brand-logo.svg" alt="SigNoz" />
|
||||
<div>
|
||||
<div className="steps-container-header">
|
||||
<div className="brand-logo" onClick={handleLogoClick}>
|
||||
<img src="/Logos/signoz-brand-logo.svg" alt="SigNoz" />
|
||||
|
||||
<div className="brand-logo-name">SigNoz</div>
|
||||
<div className="brand-logo-name">SigNoz</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Space style={{ marginBottom: '24px' }}>
|
||||
<Button
|
||||
style={{ display: 'flex', alignItems: 'center' }}
|
||||
type="default"
|
||||
icon={<LeftCircleOutlined />}
|
||||
onClick={onReselectModule}
|
||||
>
|
||||
{selectedModule.title}
|
||||
</Button>
|
||||
</Space>
|
||||
|
||||
<Steps
|
||||
direction="vertical"
|
||||
size="small"
|
||||
status="finish"
|
||||
current={current}
|
||||
items={selectedModuleSteps}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Space style={{ marginBottom: '24px' }}>
|
||||
<Button
|
||||
style={{ display: 'flex', alignItems: 'center' }}
|
||||
type="default"
|
||||
icon={<LeftCircleOutlined />}
|
||||
onClick={onReselectModule}
|
||||
>
|
||||
{selectedModule.title}
|
||||
</Button>
|
||||
</Space>
|
||||
|
||||
<Steps
|
||||
direction="vertical"
|
||||
size="small"
|
||||
status="finish"
|
||||
current={current}
|
||||
items={selectedModuleSteps}
|
||||
/>
|
||||
<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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { act, render, screen, waitFor } from 'tests/test-utils';
|
||||
|
||||
import Members from '../Members';
|
||||
|
||||
describe('Organization Settings Page', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('render list of members', async () => {
|
||||
act(() => {
|
||||
render(<Members />);
|
||||
});
|
||||
|
||||
const title = await screen.findByText(/Members/i);
|
||||
expect(title).toBeInTheDocument();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('firstUser@test.io')).toBeInTheDocument(); // first item
|
||||
expect(screen.getByText('lastUser@test.io')).toBeInTheDocument(); // last item
|
||||
});
|
||||
});
|
||||
|
||||
// this is required as our edit/delete logic is dependent on the index and it will break with pagination enabled
|
||||
it('render list of members without pagination', async () => {
|
||||
render(<Members />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('firstUser@test.io')).toBeInTheDocument(); // first item
|
||||
expect(screen.getByText('lastUser@test.io')).toBeInTheDocument(); // last item
|
||||
|
||||
expect(
|
||||
document.querySelector('.ant-table-pagination'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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,7 +147,8 @@ function TimeSeriesView({
|
||||
style={{ height: '100%', width: '100%' }}
|
||||
ref={graphRef}
|
||||
>
|
||||
{isLoading && <LogsLoading />}
|
||||
{isLoading &&
|
||||
(dataSource === DataSource.LOGS ? <LogsLoading /> : <TracesLoading />)}
|
||||
|
||||
{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 = {
|
||||
|
||||
@@ -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,36 @@ function ListView(): JSX.Element {
|
||||
[columns, onDragColumns],
|
||||
);
|
||||
|
||||
const isDataPresent =
|
||||
!isLoading &&
|
||||
!isFetching &&
|
||||
!isError &&
|
||||
transformedQueryTableData.length === 0;
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<TraceExplorerControls
|
||||
isLoading={isFetching}
|
||||
totalCount={totalCount}
|
||||
config={config}
|
||||
perPageOptions={PER_PAGE_OPTIONS}
|
||||
/>
|
||||
{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 />}
|
||||
|
||||
{!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,28 +74,49 @@ function TracesView(): JSX.Element {
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<ActionsContainer>
|
||||
<Typography>
|
||||
This tab only shows Root Spans. More details
|
||||
<Typography.Link href={TRACES_DETAILS_LINK} target="_blank">
|
||||
{' '}
|
||||
here
|
||||
</Typography.Link>
|
||||
</Typography>
|
||||
<TraceExplorerControls
|
||||
isLoading={isLoading}
|
||||
totalCount={responseData?.length || 0}
|
||||
perPageOptions={PER_PAGE_OPTIONS}
|
||||
{(tableData || []).length !== 0 && (
|
||||
<ActionsContainer>
|
||||
<Typography>
|
||||
This tab only shows Root Spans. More details
|
||||
<Typography.Link href={DOCLINKS.TRACES_DETAILS_LINK} target="_blank">
|
||||
{' '}
|
||||
here
|
||||
</Typography.Link>
|
||||
</Typography>
|
||||
<TraceExplorerControls
|
||||
isLoading={isLoading}
|
||||
totalCount={responseData?.length || 0}
|
||||
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 />}
|
||||
|
||||
{(tableData || []).length !== 0 && (
|
||||
<ResizeTable
|
||||
loading={isLoading}
|
||||
columns={columns}
|
||||
tableLayout="fixed"
|
||||
dataSource={tableData}
|
||||
scroll={{ x: true }}
|
||||
pagination={false}
|
||||
/>
|
||||
</ActionsContainer>
|
||||
<ResizeTable
|
||||
loading={isLoading}
|
||||
columns={columns}
|
||||
tableLayout="fixed"
|
||||
dataSource={tableData}
|
||||
scroll={{ x: true }}
|
||||
pagination={false}
|
||||
/>
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -10,6 +10,7 @@ import posthog from 'posthog-js';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { HelmetProvider } from 'react-helmet-async';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
import { ReactQueryDevtools } from 'react-query/devtools';
|
||||
import { Provider } from 'react-redux';
|
||||
import store from 'store';
|
||||
|
||||
@@ -72,6 +73,9 @@ if (container) {
|
||||
<Provider store={store}>
|
||||
<AppRoutes />
|
||||
</Provider>
|
||||
{process.env.NODE_ENV === 'development' && (
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
)}
|
||||
</QueryClientProvider>
|
||||
</ThemeProvider>
|
||||
</HelmetProvider>
|
||||
|
||||
25
frontend/src/mocks-server/__mockdata__/invite_user.ts
Normal file
25
frontend/src/mocks-server/__mockdata__/invite_user.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
export const inviteUser = {
|
||||
status: 'success',
|
||||
data: {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
payload: [
|
||||
{
|
||||
email: 'jane@doe.com',
|
||||
name: 'Jane',
|
||||
token: 'testtoken',
|
||||
createdAt: 1715741587,
|
||||
role: 'VIEWER',
|
||||
organization: 'test',
|
||||
},
|
||||
{
|
||||
email: 'test+in@singoz.io',
|
||||
name: '',
|
||||
token: 'testtoken1',
|
||||
createdAt: 1720095913,
|
||||
role: 'VIEWER',
|
||||
organization: 'test',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
220
frontend/src/mocks-server/__mockdata__/members.ts
Normal file
220
frontend/src/mocks-server/__mockdata__/members.ts
Normal file
@@ -0,0 +1,220 @@
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
|
||||
export const membersResponse = [
|
||||
{
|
||||
id: '3223a874-5678458745786',
|
||||
name: 'John Doe',
|
||||
email: 'firstUser@test.io',
|
||||
createdAt: 1666357530,
|
||||
profilePictureURL: '',
|
||||
orgId: '1287612376312867312867',
|
||||
groupId: '5678458745786',
|
||||
role: 'ADMIN',
|
||||
organization: 'Test Inc',
|
||||
flags: null,
|
||||
},
|
||||
{
|
||||
id: '5e9681b1-5678458745786',
|
||||
name: 'Jane Doe',
|
||||
email: 'johndoe2@test.io',
|
||||
createdAt: 1666365394,
|
||||
profilePictureURL: '',
|
||||
orgId: '1287612376312867312867',
|
||||
groupId: '5678458745786',
|
||||
role: 'ADMIN',
|
||||
organization: 'Test Inc',
|
||||
flags: null,
|
||||
},
|
||||
{
|
||||
id: '11e8c55d-5678458745786',
|
||||
name: 'Alex',
|
||||
email: 'blah@test.io',
|
||||
createdAt: 1666366317,
|
||||
profilePictureURL: '',
|
||||
orgId: '1287612376312867312867',
|
||||
groupId: 'd878012367813286731aab62',
|
||||
role: 'VIEWER',
|
||||
organization: 'Test Inc',
|
||||
flags: null,
|
||||
},
|
||||
{
|
||||
id: '2ad2e404-5678458745786',
|
||||
name: 'Tom',
|
||||
email: 'johndoe4@test.io',
|
||||
createdAt: 1673441483,
|
||||
profilePictureURL: '',
|
||||
orgId: '1287612376312867312867',
|
||||
groupId: '5678458745786',
|
||||
role: 'ADMIN',
|
||||
organization: 'Test Inc',
|
||||
flags: null,
|
||||
},
|
||||
{
|
||||
id: '6f532456-5678458745786',
|
||||
name: 'Harry',
|
||||
email: 'harry@test.io',
|
||||
createdAt: 1691551672,
|
||||
profilePictureURL: '',
|
||||
orgId: '1287612376312867312867',
|
||||
groupId: '5678458745786',
|
||||
role: 'ADMIN',
|
||||
organization: 'Test Inc',
|
||||
flags: null,
|
||||
},
|
||||
{
|
||||
id: 'ae22fa73-5678458745786',
|
||||
name: 'Ron',
|
||||
email: 'ron@test.io',
|
||||
createdAt: 1691668239,
|
||||
profilePictureURL: '',
|
||||
orgId: '1287612376312867312867',
|
||||
groupId: '5678458745786',
|
||||
role: 'ADMIN',
|
||||
organization: 'Test Inc',
|
||||
flags: null,
|
||||
},
|
||||
{
|
||||
id: '3223a874-5678458745786',
|
||||
name: 'John Doe',
|
||||
email: 'johndoe@test.io',
|
||||
createdAt: 1666357530,
|
||||
profilePictureURL: '',
|
||||
orgId: '1287612376312867312867',
|
||||
groupId: '5678458745786',
|
||||
role: 'ADMIN',
|
||||
organization: 'Test Inc',
|
||||
flags: null,
|
||||
},
|
||||
{
|
||||
id: '5e9681b1-5678458745786',
|
||||
name: 'Jane Doe',
|
||||
email: 'johndoe2@test.io',
|
||||
createdAt: 1666365394,
|
||||
profilePictureURL: '',
|
||||
orgId: '1287612376312867312867',
|
||||
groupId: '5678458745786',
|
||||
role: 'ADMIN',
|
||||
organization: 'Test Inc',
|
||||
flags: null,
|
||||
},
|
||||
{
|
||||
id: '11e8c55d-5678458745786',
|
||||
name: 'Alex',
|
||||
email: 'blah@test.io',
|
||||
createdAt: 1666366317,
|
||||
profilePictureURL: '',
|
||||
orgId: '1287612376312867312867',
|
||||
groupId: 'd878012367813286731aab62',
|
||||
role: 'VIEWER',
|
||||
organization: 'Test Inc',
|
||||
flags: null,
|
||||
},
|
||||
{
|
||||
id: '2ad2e404-5678458745786',
|
||||
name: 'Tom',
|
||||
email: 'johndoe4@test.io',
|
||||
createdAt: 1673441483,
|
||||
profilePictureURL: '',
|
||||
orgId: '1287612376312867312867',
|
||||
groupId: '5678458745786',
|
||||
role: 'ADMIN',
|
||||
organization: 'Test Inc',
|
||||
flags: null,
|
||||
},
|
||||
{
|
||||
id: '6f532456-5678458745786',
|
||||
name: 'Harry',
|
||||
email: 'harry@test.io',
|
||||
createdAt: 1691551672,
|
||||
profilePictureURL: '',
|
||||
orgId: '1287612376312867312867',
|
||||
groupId: '5678458745786',
|
||||
role: 'ADMIN',
|
||||
organization: 'Test Inc',
|
||||
flags: null,
|
||||
},
|
||||
{
|
||||
id: 'ae22fa73-5678458745786',
|
||||
name: 'Ron',
|
||||
email: 'ron@test.io',
|
||||
createdAt: 1691668239,
|
||||
profilePictureURL: '',
|
||||
orgId: '1287612376312867312867',
|
||||
groupId: '5678458745786',
|
||||
role: 'ADMIN',
|
||||
organization: 'Test Inc',
|
||||
flags: null,
|
||||
},
|
||||
{
|
||||
id: '3223a874-5678458745786',
|
||||
name: 'John Doe',
|
||||
email: 'johndoe@test.io',
|
||||
createdAt: 1666357530,
|
||||
profilePictureURL: '',
|
||||
orgId: '1287612376312867312867',
|
||||
groupId: '5678458745786',
|
||||
role: 'ADMIN',
|
||||
organization: 'Test Inc',
|
||||
flags: null,
|
||||
},
|
||||
{
|
||||
id: '5e9681b1-5678458745786',
|
||||
name: 'Jane Doe',
|
||||
email: 'johndoe2@test.io',
|
||||
createdAt: 1666365394,
|
||||
profilePictureURL: '',
|
||||
orgId: '1287612376312867312867',
|
||||
groupId: '5678458745786',
|
||||
role: 'ADMIN',
|
||||
organization: 'Test Inc',
|
||||
flags: null,
|
||||
},
|
||||
{
|
||||
id: '11e8c55d-5678458745786',
|
||||
name: 'Alex',
|
||||
email: 'blah@test.io',
|
||||
createdAt: 1666366317,
|
||||
profilePictureURL: '',
|
||||
orgId: '1287612376312867312867',
|
||||
groupId: 'd878012367813286731aab62',
|
||||
role: 'VIEWER',
|
||||
organization: 'Test Inc',
|
||||
flags: null,
|
||||
},
|
||||
{
|
||||
id: '2ad2e404-5678458745786',
|
||||
name: 'Tom',
|
||||
email: 'johndoe4@test.io',
|
||||
createdAt: 1673441483,
|
||||
profilePictureURL: '',
|
||||
orgId: '1287612376312867312867',
|
||||
groupId: '5678458745786',
|
||||
role: 'ADMIN',
|
||||
organization: 'Test Inc',
|
||||
flags: null,
|
||||
},
|
||||
{
|
||||
id: '6f532456-5678458745786',
|
||||
name: 'Harry',
|
||||
email: 'harry@test.io',
|
||||
createdAt: 1691551672,
|
||||
profilePictureURL: '',
|
||||
orgId: '1287612376312867312867',
|
||||
groupId: '5678458745786',
|
||||
role: 'ADMIN',
|
||||
organization: 'Test Inc',
|
||||
flags: null,
|
||||
},
|
||||
{
|
||||
id: 'ae22fa73-5678458745786',
|
||||
name: 'Ron',
|
||||
email: 'lastUser@test.io',
|
||||
createdAt: 1691668239,
|
||||
profilePictureURL: '',
|
||||
orgId: '1287612376312867312867',
|
||||
groupId: '5678458745786',
|
||||
role: 'ADMIN',
|
||||
organization: 'Test Inc',
|
||||
flags: null,
|
||||
},
|
||||
];
|
||||
@@ -1,7 +1,9 @@
|
||||
import { rest } from 'msw';
|
||||
|
||||
import { billingSuccessResponse } from './__mockdata__/billing';
|
||||
import { inviteUser } from './__mockdata__/invite_user';
|
||||
import { licensesSuccessResponse } from './__mockdata__/licenses';
|
||||
import { membersResponse } from './__mockdata__/members';
|
||||
import { queryRangeSuccessResponse } from './__mockdata__/query_range';
|
||||
import { serviceSuccessResponse } from './__mockdata__/services';
|
||||
import { topLevelOperationSuccessResponse } from './__mockdata__/top_level_operations';
|
||||
@@ -25,6 +27,9 @@ export const handlers = [
|
||||
res(ctx.status(200), ctx.json(topLevelOperationSuccessResponse)),
|
||||
),
|
||||
|
||||
rest.get('http://localhost/api/v1/orgUsers/*', (req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(membersResponse)),
|
||||
),
|
||||
rest.get(
|
||||
'http://localhost/api/v3/autocomplete/attribute_keys',
|
||||
(req, res, ctx) => {
|
||||
@@ -85,4 +90,11 @@ export const handlers = [
|
||||
rest.get('http://localhost/api/v1/billing', (req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(billingSuccessResponse)),
|
||||
),
|
||||
|
||||
rest.get('http://localhost/api/v1/invite', (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(inviteUser)),
|
||||
),
|
||||
rest.post('http://localhost/api/v1/invite', (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(inviteUser)),
|
||||
),
|
||||
];
|
||||
|
||||
@@ -35,12 +35,14 @@ jest.mock(
|
||||
return <div>Time Series Chart</div>;
|
||||
},
|
||||
);
|
||||
|
||||
const frequencyChartContent = 'Frequency chart content';
|
||||
jest.mock(
|
||||
'container/LogsExplorerChart',
|
||||
() =>
|
||||
// eslint-disable-next-line func-names, @typescript-eslint/explicit-function-return-type, react/display-name
|
||||
function () {
|
||||
return <div>Histogram Chart</div>;
|
||||
return <div>{frequencyChartContent}</div>;
|
||||
},
|
||||
);
|
||||
|
||||
@@ -83,13 +85,13 @@ describe('Logs Explorer Tests', () => {
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
// check the presence of histogram chart
|
||||
expect(getByText('Histogram Chart')).toBeInTheDocument();
|
||||
// check the presence of frequency chart content
|
||||
expect(getByText(frequencyChartContent)).toBeInTheDocument();
|
||||
|
||||
// toggle the chart and check it gets removed from the DOM
|
||||
const histogramToggle = getByRole('switch');
|
||||
await userEvent.click(histogramToggle);
|
||||
expect(queryByText('Histogram Chart')).not.toBeInTheDocument();
|
||||
expect(queryByText(frequencyChartContent)).not.toBeInTheDocument();
|
||||
|
||||
// check the presence of search bar and query builder and absence of clickhouse
|
||||
const searchView = getByTestId('search-view');
|
||||
@@ -229,4 +231,33 @@ describe('Logs Explorer Tests', () => {
|
||||
const aggrInterval = queryAllByText('AGGREGATION INTERVAL');
|
||||
expect(aggrInterval.length).toBe(2);
|
||||
});
|
||||
|
||||
test('frequency chart visibility and switch toggle', async () => {
|
||||
const { getByRole, queryByText } = render(
|
||||
<MemoryRouter initialEntries={[logExplorerRoute]}>
|
||||
<Provider store={store}>
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<MockQueryClientProvider>
|
||||
<QueryBuilderProvider>
|
||||
<LogsExplorer />,
|
||||
</QueryBuilderProvider>
|
||||
</MockQueryClientProvider>
|
||||
</I18nextProvider>
|
||||
</Provider>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
// check the presence of Frequency Chart
|
||||
expect(queryByText('Frequency chart')).toBeInTheDocument();
|
||||
|
||||
// check the default state of the histogram toggle
|
||||
const histogramToggle = getByRole('switch');
|
||||
expect(histogramToggle).toBeInTheDocument();
|
||||
expect(histogramToggle).toBeChecked();
|
||||
expect(queryByText(frequencyChartContent)).toBeInTheDocument();
|
||||
|
||||
// toggle the chart and check it gets removed from the DOM
|
||||
await userEvent.click(histogramToggle);
|
||||
expect(queryByText(frequencyChartContent)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,15 +16,15 @@ import { WrapperStyled } from './styles';
|
||||
import { SELECTED_VIEWS } from './utils';
|
||||
|
||||
function LogsExplorer(): JSX.Element {
|
||||
const [showHistogram, setShowHistogram] = useState(true);
|
||||
const [showFrequencyChart, setShowFrequencyChart] = useState(true);
|
||||
const [selectedView, setSelectedView] = useState<SELECTED_VIEWS>(
|
||||
SELECTED_VIEWS.SEARCH,
|
||||
);
|
||||
|
||||
const { handleRunQuery, currentQuery } = useQueryBuilder();
|
||||
|
||||
const handleToggleShowHistogram = (): void => {
|
||||
setShowHistogram(!showHistogram);
|
||||
const handleToggleShowFrequencyChart = (): void => {
|
||||
setShowFrequencyChart(!showFrequencyChart);
|
||||
};
|
||||
|
||||
const handleChangeSelectedView = (view: SELECTED_VIEWS): void => {
|
||||
@@ -78,8 +78,8 @@ function LogsExplorer(): JSX.Element {
|
||||
items={toolbarViews}
|
||||
selectedView={selectedView}
|
||||
onChangeSelectedView={handleChangeSelectedView}
|
||||
onToggleHistrogramVisibility={handleToggleShowHistogram}
|
||||
showHistogram={showHistogram}
|
||||
onToggleHistrogramVisibility={handleToggleShowFrequencyChart}
|
||||
showFrequencyChart={showFrequencyChart}
|
||||
/>
|
||||
}
|
||||
rightActions={<RightToolbarActions onStageRunQuery={handleRunQuery} />}
|
||||
@@ -96,7 +96,7 @@ function LogsExplorer(): JSX.Element {
|
||||
<div className="logs-explorer-views">
|
||||
<LogsExplorerViews
|
||||
selectedView={selectedView}
|
||||
showHistogram={showHistogram}
|
||||
showFrequencyChart={showFrequencyChart}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
135
frontend/src/pages/Support/Support.test.tsx
Normal file
135
frontend/src/pages/Support/Support.test.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
import { Calendar } from 'antd';
|
||||
import { Book, Cable, Github, MessageSquare, Slack } from 'lucide-react';
|
||||
import { fireEvent, render } from 'tests/test-utils';
|
||||
|
||||
import Support from './Support';
|
||||
|
||||
const launchChat = 'Launch chat';
|
||||
const useAnalyticsMock = jest.fn();
|
||||
const useHistoryMock = jest.fn();
|
||||
|
||||
jest.mock('hooks/analytics/useAnalytics', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(() => ({ trackEvent: useAnalyticsMock })),
|
||||
}));
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
useHistory: useHistoryMock,
|
||||
}));
|
||||
|
||||
const supportChannels = [
|
||||
{
|
||||
key: 'documentation',
|
||||
name: 'Documentation',
|
||||
icon: <Book />,
|
||||
title: 'Find answers in the documentation.',
|
||||
url: 'https://signoz.io/docs/',
|
||||
btnText: 'Visit docs',
|
||||
},
|
||||
{
|
||||
key: 'github',
|
||||
name: 'Github',
|
||||
icon: <Github />,
|
||||
title: 'Create an issue on GitHub to report bugs or request new features.',
|
||||
url: 'https://github.com/SigNoz/signoz/issues',
|
||||
btnText: 'Create issue',
|
||||
},
|
||||
{
|
||||
key: 'slack_community',
|
||||
name: 'Slack Community',
|
||||
icon: <Slack />,
|
||||
title: 'Get support from the SigNoz community on Slack.',
|
||||
url: 'https://signoz.io/slack',
|
||||
btnText: 'Join Slack',
|
||||
},
|
||||
{
|
||||
key: 'chat',
|
||||
name: 'Chat',
|
||||
icon: <MessageSquare />,
|
||||
title: 'Get quick support directly from the team.',
|
||||
url: '',
|
||||
btnText: launchChat,
|
||||
},
|
||||
{
|
||||
key: 'schedule_call',
|
||||
name: 'Schedule a call',
|
||||
icon: <Calendar />,
|
||||
title: 'Schedule a call with the founders.',
|
||||
url: 'https://calendly.com/pranay-signoz/signoz-intro-calls',
|
||||
btnText: 'Schedule call',
|
||||
},
|
||||
{
|
||||
key: 'slack_connect',
|
||||
name: 'Slack Connect',
|
||||
icon: <Cable />,
|
||||
title: 'Get a dedicated support channel for your team.',
|
||||
url: '',
|
||||
btnText: 'Request Slack connect',
|
||||
},
|
||||
];
|
||||
|
||||
describe('Help and Support renders correctly', () => {
|
||||
it('should render the support page with all support channels', () => {
|
||||
const { getByText } = render(<Support />);
|
||||
expect(getByText('Support')).toBeInTheDocument();
|
||||
expect(
|
||||
getByText(
|
||||
'We are here to help in case of questions or issues. Pick the channel that is most convenient for you.',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
supportChannels.forEach((channel) => {
|
||||
expect(getByText(channel.name)).toBeInTheDocument();
|
||||
expect(getByText(channel.title)).toBeInTheDocument();
|
||||
expect(getByText(channel.btnText)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
it('should trigger correct handler function when channel button is clicked', () => {
|
||||
const { getByText } = render(<Support />);
|
||||
const button = getByText('Visit docs');
|
||||
const windowOpenMock = jest.spyOn(window, 'open').mockImplementation();
|
||||
fireEvent.click(button);
|
||||
expect(windowOpenMock).toHaveBeenCalledWith(
|
||||
'https://signoz.io/docs/',
|
||||
'_blank',
|
||||
);
|
||||
});
|
||||
|
||||
it('should open Intercom chat widget when Chat channel is clicked', () => {
|
||||
window.Intercom = jest.fn();
|
||||
const { getByText } = render(<Support />);
|
||||
const button = getByText(launchChat);
|
||||
fireEvent.click(button);
|
||||
expect(window.Intercom).toHaveBeenCalledWith('show');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Handle channels with null or undefined properties', () => {
|
||||
it('should handle window.Intercom being undefined or null', () => {
|
||||
window.Intercom = null;
|
||||
const { getByText } = render(<Support />);
|
||||
const button = getByText(launchChat);
|
||||
fireEvent.click(button);
|
||||
expect(window.Intercom).toBeNull();
|
||||
});
|
||||
it('should handle missing or undefined history.location.state', () => {
|
||||
const trackEvent = jest.fn();
|
||||
useHistoryMock.mockReturnValue({ location: {} });
|
||||
render(<Support />);
|
||||
expect(trackEvent).not.toHaveBeenCalled();
|
||||
});
|
||||
it('should handle missing or undefined channel.url', () => {
|
||||
const openSpy = jest.spyOn(window, 'open').mockImplementation();
|
||||
const { getByText } = render(<Support />);
|
||||
fireEvent.click(getByText(launchChat));
|
||||
expect(openSpy).not.toHaveBeenCalled();
|
||||
openSpy.mockRestore();
|
||||
});
|
||||
it('should handle missing or undefined channel.name', () => {
|
||||
const trackEvent = jest.fn();
|
||||
const handleChannelClick = jest.fn();
|
||||
const channelWithoutName = { key: 'chat', url: '' };
|
||||
render(<Support />);
|
||||
handleChannelClick(channelWithoutName);
|
||||
expect(trackEvent).not.toHaveBeenCalledWith();
|
||||
});
|
||||
});
|
||||
@@ -56,7 +56,7 @@ export function Filter(props: FilterProps): JSX.Element {
|
||||
return {} as FilterType;
|
||||
}
|
||||
|
||||
return filters.items
|
||||
return (filters.items || [])
|
||||
.filter((item) =>
|
||||
Object.keys(AllTraceFilterKeyValue).includes(item.key?.key as string),
|
||||
)
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import './Filter.styles.scss';
|
||||
|
||||
import { Button, Collapse, Divider } from 'antd';
|
||||
import { Dispatch, MouseEvent, SetStateAction } from 'react';
|
||||
import {
|
||||
Dispatch,
|
||||
MouseEvent,
|
||||
SetStateAction,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import { DurationSection } from './DurationSection';
|
||||
import {
|
||||
@@ -18,9 +25,29 @@ interface SectionProps {
|
||||
setSelectedFilters: Dispatch<SetStateAction<FilterType | undefined>>;
|
||||
handleRun: (props?: HandleRunProps) => void;
|
||||
}
|
||||
|
||||
export function Section(props: SectionProps): JSX.Element {
|
||||
const { panelName, setSelectedFilters, selectedFilters, handleRun } = props;
|
||||
|
||||
const defaultOpenPanes = useMemo(
|
||||
() =>
|
||||
Array.from(
|
||||
new Set([
|
||||
...Object.keys(selectedFilters || {}),
|
||||
'hasError',
|
||||
'durationNano',
|
||||
'serviceName',
|
||||
]),
|
||||
),
|
||||
[selectedFilters],
|
||||
);
|
||||
|
||||
const [activeKeys, setActiveKeys] = useState<string[]>(defaultOpenPanes);
|
||||
|
||||
useEffect(() => {
|
||||
setActiveKeys(defaultOpenPanes);
|
||||
}, [defaultOpenPanes]);
|
||||
|
||||
const onClearHandler = (e: MouseEvent): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
@@ -41,11 +68,8 @@ export function Section(props: SectionProps): JSX.Element {
|
||||
<Collapse
|
||||
bordered={false}
|
||||
className="collapseContainer"
|
||||
defaultActiveKey={
|
||||
['hasError', 'durationNano', 'serviceName'].includes(panelName)
|
||||
? panelName
|
||||
: undefined
|
||||
}
|
||||
activeKey={activeKeys}
|
||||
onChange={(keys): void => setActiveKeys(keys as string[])}
|
||||
items={[
|
||||
panelName === 'durationNano'
|
||||
? {
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import {
|
||||
initialQueriesMap,
|
||||
initialQueryBuilderFormValues,
|
||||
} from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import * as compositeQueryHook from 'hooks/queryBuilder/useGetCompositeQueryParam';
|
||||
import { render } from 'tests/test-utils';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { Filter } from '../Filter/Filter';
|
||||
import { AllTraceFilterKeyValue } from '../Filter/filterUtils';
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: (): { pathname: string } => ({
|
||||
pathname: `${process.env.FRONTEND_API_ENDPOINT}${ROUTES.TRACES_EXPLORER}/`,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('uplot', () => {
|
||||
const paths = {
|
||||
spline: jest.fn(),
|
||||
bars: jest.fn(),
|
||||
};
|
||||
|
||||
const uplotMock = jest.fn(() => ({
|
||||
paths,
|
||||
}));
|
||||
|
||||
return {
|
||||
paths,
|
||||
default: uplotMock,
|
||||
};
|
||||
});
|
||||
|
||||
const compositeQuery: Query = {
|
||||
...initialQueriesMap.traces,
|
||||
builder: {
|
||||
...initialQueriesMap.traces.builder,
|
||||
queryData: [
|
||||
{
|
||||
...initialQueryBuilderFormValues,
|
||||
filters: {
|
||||
items: [
|
||||
{
|
||||
id: '95564eb1',
|
||||
key: {
|
||||
key: 'name',
|
||||
dataType: DataTypes.String,
|
||||
type: 'tag',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
id: 'name--string--tag--true',
|
||||
},
|
||||
op: 'in',
|
||||
value: ['HTTP GET /customer'],
|
||||
},
|
||||
{
|
||||
id: '3337951c',
|
||||
key: {
|
||||
key: 'serviceName',
|
||||
dataType: DataTypes.String,
|
||||
type: 'tag',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
id: 'serviceName--string--tag--true',
|
||||
},
|
||||
op: 'in',
|
||||
value: ['demo-app'],
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
describe('TracesExplorer - ', () => {
|
||||
it('test edge cases of undefined filters', async () => {
|
||||
jest.spyOn(compositeQueryHook, 'useGetCompositeQueryParam').mockReturnValue({
|
||||
...compositeQuery,
|
||||
builder: {
|
||||
...compositeQuery.builder,
|
||||
queryData: compositeQuery.builder.queryData.map(
|
||||
(item) =>
|
||||
({
|
||||
...item,
|
||||
filters: undefined,
|
||||
} as any),
|
||||
),
|
||||
},
|
||||
});
|
||||
|
||||
const { getByText } = render(<Filter setOpen={jest.fn()} />);
|
||||
|
||||
// we should have all the filters
|
||||
Object.values(AllTraceFilterKeyValue).forEach((filter) => {
|
||||
expect(getByText(filter)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('test edge cases of undefined filters - items', async () => {
|
||||
jest.spyOn(compositeQueryHook, 'useGetCompositeQueryParam').mockReturnValue({
|
||||
...compositeQuery,
|
||||
builder: {
|
||||
...compositeQuery.builder,
|
||||
queryData: compositeQuery.builder.queryData.map(
|
||||
(item) =>
|
||||
({
|
||||
...item,
|
||||
filters: {
|
||||
...item.filters,
|
||||
items: undefined,
|
||||
},
|
||||
} as any),
|
||||
),
|
||||
},
|
||||
});
|
||||
|
||||
const { getByText } = render(<Filter setOpen={jest.fn()} />);
|
||||
|
||||
// we should have all the filters
|
||||
Object.values(AllTraceFilterKeyValue).forEach((filter) => {
|
||||
expect(getByText(filter)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -23,7 +23,7 @@ import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
||||
import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import history from 'lib/history';
|
||||
import { cloneDeep, set } from 'lodash-es';
|
||||
import { cloneDeep, isEmpty, set } from 'lodash-es';
|
||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
@@ -62,6 +62,12 @@ function TracesExplorer(): JSX.Element {
|
||||
|
||||
const currentTab = panelType || PANEL_TYPES.LIST;
|
||||
|
||||
const listQuery = useMemo(() => {
|
||||
if (!stagedQuery || stagedQuery.builder.queryData.length < 1) return null;
|
||||
|
||||
return stagedQuery.builder.queryData.find((item) => !item.disabled) || null;
|
||||
}, [stagedQuery]);
|
||||
|
||||
const isMultipleQueries = useMemo(
|
||||
() =>
|
||||
currentQuery.builder.queryData.length > 1 ||
|
||||
@@ -101,6 +107,7 @@ function TracesExplorer(): JSX.Element {
|
||||
|
||||
const tabsItems = getTabsItems({
|
||||
isListViewDisabled: isMultipleQueries || isGroupByExist,
|
||||
isFilterApplied: !isEmpty(listQuery?.filters.items),
|
||||
});
|
||||
|
||||
const exportDefaultQuery = useMemo(
|
||||
|
||||
@@ -9,10 +9,12 @@ import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
interface GetTabsItemsProps {
|
||||
isListViewDisabled: boolean;
|
||||
isFilterApplied: boolean;
|
||||
}
|
||||
|
||||
export const getTabsItems = ({
|
||||
isListViewDisabled,
|
||||
isFilterApplied,
|
||||
}: GetTabsItemsProps): TabsProps['items'] => [
|
||||
{
|
||||
label: (
|
||||
@@ -23,7 +25,7 @@ export const getTabsItems = ({
|
||||
/>
|
||||
),
|
||||
key: PANEL_TYPES.LIST,
|
||||
children: <ListView />,
|
||||
children: <ListView isFilterApplied={isFilterApplied} />,
|
||||
disabled: isListViewDisabled,
|
||||
},
|
||||
{
|
||||
@@ -35,13 +37,18 @@ export const getTabsItems = ({
|
||||
/>
|
||||
),
|
||||
key: PANEL_TYPES.TRACE,
|
||||
children: <TracesView />,
|
||||
children: <TracesView isFilterApplied={isFilterApplied} />,
|
||||
disabled: isListViewDisabled,
|
||||
},
|
||||
{
|
||||
label: <TabLabel label="Time Series" isDisabled={false} />,
|
||||
key: PANEL_TYPES.TIME_SERIES,
|
||||
children: <TimeSeriesView dataSource={DataSource.TRACES} />,
|
||||
children: (
|
||||
<TimeSeriesView
|
||||
dataSource={DataSource.TRACES}
|
||||
isFilterApplied={isFilterApplied}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: 'Table View',
|
||||
|
||||
@@ -266,3 +266,10 @@ modal - 1000
|
||||
notifications - 2050
|
||||
|
||||
*/
|
||||
|
||||
@font-face {
|
||||
font-family: 'Geist Mono';
|
||||
src: local('Geist Mono'),
|
||||
url('../public/fonts/GeistMonoVF.woff2') format('woff');
|
||||
/* Add other formats if needed (e.g., woff2, truetype, opentype, svg) */
|
||||
}
|
||||
|
||||
@@ -42,6 +42,15 @@ const mockStored = (role?: string): any =>
|
||||
accessJwt: '',
|
||||
refreshJwt: '',
|
||||
},
|
||||
org: [
|
||||
{
|
||||
createdAt: 0,
|
||||
hasOptedUpdates: false,
|
||||
id: 'xyz',
|
||||
isAnonymous: false,
|
||||
name: 'Test Inc. - India',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
9
frontend/src/utils/docLinks.ts
Normal file
9
frontend/src/utils/docLinks.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
const DOCLINKS = {
|
||||
TRACES_EXPLORER_EMPTY_STATE:
|
||||
'https://signoz.io/docs/instrumentation/overview/?utm_source=product&utm_medium=traces-explorer-empty-state',
|
||||
USER_GUIDE: 'https://signoz.io/docs/userguide/',
|
||||
TRACES_DETAILS_LINK:
|
||||
'https://signoz.io/docs/product-features/trace-explorer/?utm_source=product&utm_medium=traces-explorer-trace-tab#traces-view',
|
||||
};
|
||||
|
||||
export default DOCLINKS;
|
||||
@@ -10,3 +10,6 @@ export const getGraphType = (panelType: PANEL_TYPES): PANEL_TYPES => {
|
||||
}
|
||||
return panelType;
|
||||
};
|
||||
|
||||
export const getGraphTypeForFormat = (panelType: PANEL_TYPES): PANEL_TYPES =>
|
||||
panelType;
|
||||
|
||||
@@ -6195,11 +6195,11 @@ brace-expansion@^2.0.1:
|
||||
balanced-match "^1.0.0"
|
||||
|
||||
braces@^3.0.2, braces@~3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz"
|
||||
integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
|
||||
integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
|
||||
dependencies:
|
||||
fill-range "^7.0.1"
|
||||
fill-range "^7.1.1"
|
||||
|
||||
broadcast-channel@^3.4.1:
|
||||
version "3.7.0"
|
||||
@@ -8808,10 +8808,10 @@ file-saver@^2.0.2:
|
||||
resolved "https://registry.yarnpkg.com/file-saver/-/file-saver-2.0.5.tgz#d61cfe2ce059f414d899e9dd6d4107ee25670c38"
|
||||
integrity sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==
|
||||
|
||||
fill-range@^7.0.1:
|
||||
version "7.0.1"
|
||||
resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz"
|
||||
integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
|
||||
fill-range@^7.1.1:
|
||||
version "7.1.1"
|
||||
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292"
|
||||
integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==
|
||||
dependencies:
|
||||
to-regex-range "^5.0.1"
|
||||
|
||||
@@ -13705,13 +13705,14 @@ postcss@8.4.38, postcss@^8.0.0, postcss@^8.1.1, postcss@^8.3.7, postcss@^8.4.21,
|
||||
picocolors "^1.0.0"
|
||||
source-map-js "^1.2.0"
|
||||
|
||||
posthog-js@1.140.1:
|
||||
version "1.140.1"
|
||||
resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.140.1.tgz#34efc0d326fa5fcf7950106f350fb4f0e73b2da6"
|
||||
integrity sha512-UeKuAtQSvbzmTCzNVaauku8F194EYwAP33WrRrWZlDlMNbMy7GKcZOgKbr7jZqnha7FlVlHrWk+Rpyr1zCFhPQ==
|
||||
posthog-js@1.142.1:
|
||||
version "1.142.1"
|
||||
resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.142.1.tgz#3b91229732938c5c76b5ee6d410698a267e073e9"
|
||||
integrity sha512-yqeWTWitlb0sCaH5v6s7UJ+pPspzf/lkzPaSE5pMMXRM2i2KNsMoZEAZqbPCW8fQ8QL6lHs6d8PLjHrvbR288w==
|
||||
dependencies:
|
||||
fflate "^0.4.8"
|
||||
preact "^10.19.3"
|
||||
web-vitals "^4.0.1"
|
||||
|
||||
preact@^10.19.3:
|
||||
version "10.22.0"
|
||||
@@ -17218,6 +17219,11 @@ web-vitals@^0.2.4:
|
||||
resolved "https://registry.npmjs.org/web-vitals/-/web-vitals-0.2.4.tgz"
|
||||
integrity sha512-6BjspCO9VriYy12z356nL6JBS0GYeEcA457YyRzD+dD6XYCQ75NKhcOHUMHentOE7OcVCIXXDvOm0jKFfQG2Gg==
|
||||
|
||||
web-vitals@^4.0.1:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/web-vitals/-/web-vitals-4.2.0.tgz#008949ab79717a68ccaaa3c4371cbc7bbbd78a92"
|
||||
integrity sha512-ohj72kbtVWCpKYMxcbJ+xaOBV3En76hW47j52dG+tEGG36LZQgfFw5yHl9xyjmosy3XUMn8d/GBUAy4YPM839w==
|
||||
|
||||
web-worker@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.npmjs.org/web-worker/-/web-worker-1.2.0.tgz"
|
||||
@@ -17632,14 +17638,14 @@ write-file-atomic@^4.0.2:
|
||||
signal-exit "^3.0.7"
|
||||
|
||||
ws@^7.3.1, ws@^7.4.6:
|
||||
version "7.5.9"
|
||||
resolved "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz"
|
||||
integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==
|
||||
version "7.5.10"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9"
|
||||
integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==
|
||||
|
||||
ws@^8.13.0:
|
||||
version "8.13.0"
|
||||
resolved "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz"
|
||||
integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==
|
||||
version "8.17.1"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b"
|
||||
integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==
|
||||
|
||||
xhr-request@^1.0.1:
|
||||
version "1.1.0"
|
||||
|
||||
4
go.mod
4
go.mod
@@ -6,7 +6,7 @@ require (
|
||||
github.com/ClickHouse/clickhouse-go/v2 v2.20.0
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.2
|
||||
github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd
|
||||
github.com/SigNoz/signoz-otel-collector v0.102.0
|
||||
github.com/SigNoz/signoz-otel-collector v0.102.2
|
||||
github.com/SigNoz/zap_otlp/zap_otlp_encoder v0.0.0-20230822164844-1b861a431974
|
||||
github.com/SigNoz/zap_otlp/zap_otlp_sync v0.0.0-20230822164844-1b861a431974
|
||||
github.com/antonmedv/expr v1.15.3
|
||||
@@ -70,7 +70,7 @@ require (
|
||||
golang.org/x/net v0.26.0
|
||||
golang.org/x/oauth2 v0.21.0
|
||||
golang.org/x/text v0.16.0
|
||||
google.golang.org/grpc v1.64.0
|
||||
google.golang.org/grpc v1.64.1
|
||||
google.golang.org/protobuf v1.34.1
|
||||
gopkg.in/segmentio/analytics-go.v3 v3.1.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
|
||||
12
go.sum
12
go.sum
@@ -64,8 +64,8 @@ github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd h1:Bk43AsDYe0fhkb
|
||||
github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd/go.mod h1:nxRcH/OEdM8QxzH37xkGzomr1O0JpYBRS6pwjsWW6Pc=
|
||||
github.com/SigNoz/prometheus v1.11.1 h1:roM8ugYf4UxaeKKujEeBvoX7ybq3IrS+TB26KiRtIJg=
|
||||
github.com/SigNoz/prometheus v1.11.1/go.mod h1:uv4mQwZQtx7y4GQ6EdHOi8Wsk07uHNn2XHd1zM85m6I=
|
||||
github.com/SigNoz/signoz-otel-collector v0.102.0 h1:v6ap+gdvrKklMwU+M9FJgrn28vN0YxrINl3kvdcLonA=
|
||||
github.com/SigNoz/signoz-otel-collector v0.102.0/go.mod h1:kCx5BfzDujq6C0+kotiqLp5COG2ut4Cb039+55rbWE0=
|
||||
github.com/SigNoz/signoz-otel-collector v0.102.2 h1:SmjsBZjMjTVVpuOlfJXlsDJQbdefQP/9Wz3CyzSuZuU=
|
||||
github.com/SigNoz/signoz-otel-collector v0.102.2/go.mod h1:ISAXYhZenojCWg6CdDJtPMpfS6Zwc08+uoxH25tc6Y0=
|
||||
github.com/SigNoz/zap_otlp v0.1.0 h1:T7rRcFN87GavY8lDGZj0Z3Xv6OhJA6Pj3I9dNPmqvRc=
|
||||
github.com/SigNoz/zap_otlp v0.1.0/go.mod h1:lcHvbDbRgvDnPxo9lDlaL1JK2PyOyouP/C3ynnYIvyo=
|
||||
github.com/SigNoz/zap_otlp/zap_otlp_encoder v0.0.0-20230822164844-1b861a431974 h1:PKVgdf83Yw+lZJbFtNGBgqXiXNf3+kOXW2qZ7Ms7OaY=
|
||||
@@ -378,8 +378,8 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/hashicorp/go-plugin v1.0.1/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn3cQptSMzBuY=
|
||||
github.com/hashicorp/go-retryablehttp v0.5.4/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.4 h1:ZQgVdpTdAL7WpMIwLzCfbalOcSUdkDZnpUv3/+BxzFA=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.4/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
|
||||
github.com/hashicorp/go-rootcerts v1.0.1/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
|
||||
github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=
|
||||
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
|
||||
@@ -1197,8 +1197,8 @@ google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM
|
||||
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
|
||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
|
||||
google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY=
|
||||
google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg=
|
||||
google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA=
|
||||
google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
|
||||
@@ -53,7 +53,6 @@ import (
|
||||
"go.signoz.io/signoz/pkg/query-service/interfaces"
|
||||
"go.signoz.io/signoz/pkg/query-service/model"
|
||||
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
|
||||
"go.signoz.io/signoz/pkg/query-service/rules"
|
||||
"go.signoz.io/signoz/pkg/query-service/telemetry"
|
||||
"go.signoz.io/signoz/pkg/query-service/utils"
|
||||
)
|
||||
@@ -3420,36 +3419,6 @@ func countPanelsInDashboard(data map[string]interface{}) model.DashboardsInfo {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ClickHouseReader) GetAlertsInfo(ctx context.Context) (*model.AlertsInfo, error) {
|
||||
alertsInfo := model.AlertsInfo{}
|
||||
// fetch alerts from rules db
|
||||
query := "SELECT data FROM rules"
|
||||
var alertsData []string
|
||||
err := r.localDB.Select(&alertsData, query)
|
||||
if err != nil {
|
||||
zap.L().Error("Error in processing sql query", zap.Error(err))
|
||||
return &alertsInfo, err
|
||||
}
|
||||
for _, alert := range alertsData {
|
||||
var rule rules.GettableRule
|
||||
err = json.Unmarshal([]byte(alert), &rule)
|
||||
if err != nil {
|
||||
zap.L().Error("invalid rule data", zap.Error(err))
|
||||
continue
|
||||
}
|
||||
if rule.AlertType == "LOGS_BASED_ALERT" {
|
||||
alertsInfo.LogsBasedAlerts = alertsInfo.LogsBasedAlerts + 1
|
||||
} else if rule.AlertType == "METRIC_BASED_ALERT" {
|
||||
alertsInfo.MetricBasedAlerts = alertsInfo.MetricBasedAlerts + 1
|
||||
} else if rule.AlertType == "TRACES_BASED_ALERT" {
|
||||
alertsInfo.TracesBasedAlerts = alertsInfo.TracesBasedAlerts + 1
|
||||
}
|
||||
alertsInfo.TotalAlerts = alertsInfo.TotalAlerts + 1
|
||||
}
|
||||
|
||||
return &alertsInfo, nil
|
||||
}
|
||||
|
||||
func (r *ClickHouseReader) GetSavedViewsInfo(ctx context.Context) (*model.SavedViewsInfo, error) {
|
||||
savedViewsInfo := model.SavedViewsInfo{}
|
||||
savedViews, err := explorer.GetViews()
|
||||
@@ -3606,38 +3575,42 @@ func (r *ClickHouseReader) UpdateLogField(ctx context.Context, field *model.Upda
|
||||
}
|
||||
|
||||
} else {
|
||||
// We are not allowing to delete a materialized column
|
||||
// For more details please check https://github.com/SigNoz/signoz/issues/4566
|
||||
return model.ForbiddenError(errors.New("Removing a selected field is not allowed, please reach out to support."))
|
||||
|
||||
// Delete the index first
|
||||
query := fmt.Sprintf("ALTER TABLE %s.%s ON CLUSTER %s DROP INDEX IF EXISTS %s_idx`", r.logsDB, r.logsLocalTable, r.cluster, strings.TrimSuffix(colname, "`"))
|
||||
err := r.db.Exec(ctx, query)
|
||||
if err != nil {
|
||||
return &model.ApiError{Err: err, Typ: model.ErrorInternal}
|
||||
}
|
||||
// query := fmt.Sprintf("ALTER TABLE %s.%s ON CLUSTER %s DROP INDEX IF EXISTS %s_idx`", r.logsDB, r.logsLocalTable, r.cluster, strings.TrimSuffix(colname, "`"))
|
||||
// err := r.db.Exec(ctx, query)
|
||||
// if err != nil {
|
||||
// return &model.ApiError{Err: err, Typ: model.ErrorInternal}
|
||||
// }
|
||||
|
||||
for _, table := range []string{r.logsTable, r.logsLocalTable} {
|
||||
// drop materialized column from logs table
|
||||
query := "ALTER TABLE %s.%s ON CLUSTER %s DROP COLUMN IF EXISTS %s "
|
||||
err := r.db.Exec(ctx, fmt.Sprintf(query,
|
||||
r.logsDB, table,
|
||||
r.cluster,
|
||||
colname,
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
return &model.ApiError{Err: err, Typ: model.ErrorInternal}
|
||||
}
|
||||
// for _, table := range []string{r.logsTable, r.logsLocalTable} {
|
||||
// // drop materialized column from logs table
|
||||
// query := "ALTER TABLE %s.%s ON CLUSTER %s DROP COLUMN IF EXISTS %s "
|
||||
// err := r.db.Exec(ctx, fmt.Sprintf(query,
|
||||
// r.logsDB, table,
|
||||
// r.cluster,
|
||||
// colname,
|
||||
// ),
|
||||
// )
|
||||
// if err != nil {
|
||||
// return &model.ApiError{Err: err, Typ: model.ErrorInternal}
|
||||
// }
|
||||
|
||||
// drop exists column on logs table
|
||||
query = "ALTER TABLE %s.%s ON CLUSTER %s DROP COLUMN IF EXISTS %s_exists` "
|
||||
err = r.db.Exec(ctx, fmt.Sprintf(query,
|
||||
r.logsDB, table,
|
||||
r.cluster,
|
||||
strings.TrimSuffix(colname, "`"),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
return &model.ApiError{Err: err, Typ: model.ErrorInternal}
|
||||
}
|
||||
}
|
||||
// // drop exists column on logs table
|
||||
// query = "ALTER TABLE %s.%s ON CLUSTER %s DROP COLUMN IF EXISTS %s_exists` "
|
||||
// err = r.db.Exec(ctx, fmt.Sprintf(query,
|
||||
// r.logsDB, table,
|
||||
// r.cluster,
|
||||
// strings.TrimSuffix(colname, "`"),
|
||||
// ),
|
||||
// )
|
||||
// if err != nil {
|
||||
// return &model.ApiError{Err: err, Typ: model.ErrorInternal}
|
||||
// }
|
||||
// }
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -83,6 +83,7 @@ type APIHandler struct {
|
||||
// querying the v4 table on low cardinal temporality column
|
||||
// should be fast but we can still avoid the query if we have the data in memory
|
||||
temporalityMap map[string]map[v3.Temporality]bool
|
||||
temporalityMux sync.Mutex
|
||||
|
||||
maxIdleConns int
|
||||
maxOpenConns int
|
||||
@@ -455,6 +456,9 @@ func (aH *APIHandler) getRule(w http.ResponseWriter, r *http.Request) {
|
||||
// populateTemporality adds the temporality to the query if it is not present
|
||||
func (aH *APIHandler) populateTemporality(ctx context.Context, qp *v3.QueryRangeParamsV3) error {
|
||||
|
||||
aH.temporalityMux.Lock()
|
||||
defer aH.temporalityMux.Unlock()
|
||||
|
||||
missingTemporality := make([]string, 0)
|
||||
metricNameToTemporality := make(map[string]map[v3.Temporality]bool)
|
||||
if qp.CompositeQuery != nil && len(qp.CompositeQuery.BuilderQueries) > 0 {
|
||||
|
||||
@@ -142,6 +142,11 @@ func checkDuplicateString(pipeline []string) bool {
|
||||
for _, processor := range pipeline {
|
||||
name := processor
|
||||
if _, ok := exists[name]; ok {
|
||||
zap.L().Error(
|
||||
"duplicate processor name detected in generated collector config for log pipelines",
|
||||
zap.String("processor", processor),
|
||||
zap.Any("pipeline", pipeline),
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,10 @@ import (
|
||||
"testing"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.signoz.io/signoz/pkg/query-service/constants"
|
||||
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
var buildProcessorTestData = []struct {
|
||||
@@ -204,3 +207,89 @@ func TestBuildLogsPipeline(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPipelineAliasCollisionsDontResultInDuplicateCollectorProcessors(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
baseConf := []byte(`
|
||||
receivers:
|
||||
memory:
|
||||
id: in-memory-receiver
|
||||
exporters:
|
||||
memory:
|
||||
id: in-memory-exporter
|
||||
service:
|
||||
pipelines:
|
||||
logs:
|
||||
receivers:
|
||||
- memory
|
||||
processors: []
|
||||
exporters:
|
||||
- memory
|
||||
`)
|
||||
|
||||
makeTestPipeline := func(name string, alias string) Pipeline {
|
||||
return Pipeline{
|
||||
OrderId: 1,
|
||||
Name: name,
|
||||
Alias: alias,
|
||||
Enabled: true,
|
||||
Filter: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "method",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
Operator: "=",
|
||||
Value: "GET",
|
||||
},
|
||||
},
|
||||
},
|
||||
Config: []PipelineOperator{
|
||||
{
|
||||
ID: "regex",
|
||||
Type: "regex_parser",
|
||||
Enabled: true,
|
||||
Name: "regex parser",
|
||||
ParseFrom: "attributes.test_regex_target",
|
||||
ParseTo: "attributes",
|
||||
Regex: `^\s*(?P<json_data>{.*})\s*$`,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
testPipelines := []Pipeline{
|
||||
makeTestPipeline("test pipeline 1", "pipeline-alias"),
|
||||
makeTestPipeline("test pipeline 2", "pipeline-alias"),
|
||||
}
|
||||
|
||||
recommendedConfYaml, apiErr := GenerateCollectorConfigWithPipelines(
|
||||
baseConf, testPipelines,
|
||||
)
|
||||
require.Nil(apiErr, fmt.Sprintf("couldn't generate config recommendation: %v", apiErr))
|
||||
|
||||
var recommendedConf map[string]interface{}
|
||||
err := yaml.Unmarshal(recommendedConfYaml, &recommendedConf)
|
||||
require.Nil(err, "couldn't unmarshal recommended config")
|
||||
|
||||
logsProcessors := recommendedConf["service"].(map[string]any)["pipelines"].(map[string]any)["logs"].(map[string]any)["processors"].([]any)
|
||||
|
||||
require.Equal(
|
||||
len(logsProcessors), len(testPipelines),
|
||||
"test pipelines not included in recommended config as expected",
|
||||
)
|
||||
|
||||
recommendedConfYaml2, apiErr := GenerateCollectorConfigWithPipelines(
|
||||
baseConf, testPipelines,
|
||||
)
|
||||
require.Nil(apiErr, fmt.Sprintf("couldn't generate config recommendation again: %v", apiErr))
|
||||
require.Equal(
|
||||
string(recommendedConfYaml), string(recommendedConfYaml2),
|
||||
"collector config should not change across recommendations for same set of pipelines",
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ func CollectorConfProcessorName(p Pipeline) string {
|
||||
func PreparePipelineProcessor(pipelines []Pipeline) (map[string]interface{}, []string, error) {
|
||||
processors := map[string]interface{}{}
|
||||
names := []string{}
|
||||
for _, v := range pipelines {
|
||||
for pipelineIdx, v := range pipelines {
|
||||
if !v.Enabled {
|
||||
continue
|
||||
}
|
||||
@@ -70,6 +70,12 @@ func PreparePipelineProcessor(pipelines []Pipeline) (map[string]interface{}, []s
|
||||
Operators: v.Config,
|
||||
}
|
||||
name := CollectorConfProcessorName(v)
|
||||
|
||||
// Ensure name is unique
|
||||
if _, nameExists := processors[name]; nameExists {
|
||||
name = fmt.Sprintf("%s-%d", name, pipelineIdx)
|
||||
}
|
||||
|
||||
processors[name] = processor
|
||||
names = append(names, name)
|
||||
}
|
||||
|
||||
@@ -803,76 +803,3 @@ func TestContainsFilterIsCaseInsensitive(t *testing.T) {
|
||||
_, test2Exists := result[0].Attributes_string["test2"]
|
||||
require.False(test2Exists)
|
||||
}
|
||||
|
||||
func TestTemporaryWorkaroundForSupportingAttribsContainingDots(t *testing.T) {
|
||||
// TODO(Raj): Remove this after dots are supported
|
||||
|
||||
require := require.New(t)
|
||||
|
||||
testPipeline := Pipeline{
|
||||
OrderId: 1,
|
||||
Name: "pipeline1",
|
||||
Alias: "pipeline1",
|
||||
Enabled: true,
|
||||
Filter: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "k8s_deployment_name",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeResource,
|
||||
},
|
||||
Operator: "=",
|
||||
Value: "ingress",
|
||||
},
|
||||
},
|
||||
},
|
||||
Config: []PipelineOperator{
|
||||
{
|
||||
ID: "add",
|
||||
Type: "add",
|
||||
Enabled: true,
|
||||
Name: "add",
|
||||
Field: "attributes.test",
|
||||
Value: "test-value",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
testLogs := []model.SignozLog{{
|
||||
Timestamp: uint64(time.Now().UnixNano()),
|
||||
Body: "test log",
|
||||
Attributes_string: map[string]string{},
|
||||
Resources_string: map[string]string{
|
||||
"k8s_deployment_name": "ingress",
|
||||
},
|
||||
SeverityText: entry.Info.String(),
|
||||
SeverityNumber: uint8(entry.Info),
|
||||
SpanID: "",
|
||||
TraceID: "",
|
||||
}, {
|
||||
Timestamp: uint64(time.Now().UnixNano()),
|
||||
Body: "test log",
|
||||
Attributes_string: map[string]string{},
|
||||
Resources_string: map[string]string{
|
||||
"k8s.deployment.name": "ingress",
|
||||
},
|
||||
SeverityText: entry.Info.String(),
|
||||
SeverityNumber: uint8(entry.Info),
|
||||
SpanID: "",
|
||||
TraceID: "",
|
||||
}}
|
||||
|
||||
result, collectorWarnAndErrorLogs, err := SimulatePipelinesProcessing(
|
||||
context.Background(),
|
||||
[]Pipeline{testPipeline},
|
||||
testLogs,
|
||||
)
|
||||
require.Nil(err)
|
||||
require.Equal(0, len(collectorWarnAndErrorLogs), strings.Join(collectorWarnAndErrorLogs, "\n"))
|
||||
require.Equal(2, len(result))
|
||||
for _, processedLog := range result {
|
||||
require.Equal(processedLog.Attributes_string["test"], "test-value")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,9 @@ func ParseLogFilterParams(r *http.Request) (*model.LogsFilterParams, error) {
|
||||
res.OrderBy = val[0]
|
||||
}
|
||||
if val, ok := params[ORDER]; ok {
|
||||
res.Order = val[0]
|
||||
if val[0] == ASC || val[0] == DESC {
|
||||
res.Order = val[0]
|
||||
}
|
||||
}
|
||||
if val, ok := params["q"]; ok {
|
||||
res.Query = val[0]
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package logs
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
@@ -432,3 +434,51 @@ func TestGenerateSQLQuery(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var parseLogFilterParams = []struct {
|
||||
Name string
|
||||
ReqParams string
|
||||
ExpectedLogFilterParams *model.LogsFilterParams
|
||||
}{
|
||||
{
|
||||
Name: "test with proper order by",
|
||||
ReqParams: "order=desc&q=service.name='myservice'&limit=10",
|
||||
ExpectedLogFilterParams: &model.LogsFilterParams{
|
||||
Limit: 10,
|
||||
OrderBy: "timestamp",
|
||||
Order: DESC,
|
||||
Query: "service.name='myservice'",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "test with proper order by asc",
|
||||
ReqParams: "order=asc&q=service.name='myservice'&limit=10",
|
||||
ExpectedLogFilterParams: &model.LogsFilterParams{
|
||||
Limit: 10,
|
||||
OrderBy: "timestamp",
|
||||
Order: ASC,
|
||||
Query: "service.name='myservice'",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "test with incorrect order by",
|
||||
ReqParams: "order=undefined&q=service.name='myservice'&limit=10",
|
||||
ExpectedLogFilterParams: &model.LogsFilterParams{
|
||||
Limit: 10,
|
||||
OrderBy: "timestamp",
|
||||
Order: DESC,
|
||||
Query: "service.name='myservice'",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func TestParseLogFilterParams(t *testing.T) {
|
||||
for _, test := range parseLogFilterParams {
|
||||
Convey(test.Name, t, func() {
|
||||
req := httptest.NewRequest(http.MethodGet, "/logs?"+test.ReqParams, nil)
|
||||
params, err := ParseLogFilterParams(req)
|
||||
So(err, ShouldBeNil)
|
||||
So(params, ShouldEqual, test.ExpectedLogFilterParams)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +73,6 @@ type Reader interface {
|
||||
LiveTailLogsV3(ctx context.Context, query string, timestampStart uint64, idStart string, client *v3.LogsLiveTailClient)
|
||||
|
||||
GetDashboardsInfo(ctx context.Context) (*model.DashboardsInfo, error)
|
||||
GetAlertsInfo(ctx context.Context) (*model.AlertsInfo, error)
|
||||
GetSavedViewsInfo(ctx context.Context) (*model.SavedViewsInfo, error)
|
||||
GetTotalSpans(ctx context.Context) (uint64, error)
|
||||
GetTotalLogs(ctx context.Context) (uint64, error)
|
||||
|
||||
@@ -401,8 +401,11 @@ type CompositeQuery struct {
|
||||
PromQueries map[string]*PromQuery `json:"promQueries,omitempty"`
|
||||
PanelType PanelType `json:"panelType"`
|
||||
QueryType QueryType `json:"queryType"`
|
||||
Unit string `json:"unit,omitempty"`
|
||||
FillGaps bool `json:"fillGaps,omitempty"`
|
||||
// Unit for the time series data shown in the graph
|
||||
// This is used in alerts to format the value and threshold
|
||||
Unit string `json:"unit,omitempty"`
|
||||
// FillGaps is used to fill the gaps in the time series data
|
||||
FillGaps bool `json:"fillGaps,omitempty"`
|
||||
}
|
||||
|
||||
func (c *CompositeQuery) EnabledQueries() int {
|
||||
|
||||
@@ -46,6 +46,9 @@ func fillGap(series *v3.Series, start, end, step int64) *v3.Series {
|
||||
|
||||
// TODO(srikanthccv): can WITH FILL be perfect substitute for all cases https://clickhouse.com/docs/en/sql-reference/statements/select/order-by#order-by-expr-with-fill-modifier
|
||||
func FillGaps(results []*v3.Result, params *v3.QueryRangeParamsV3) {
|
||||
if params.CompositeQuery.PanelType != v3.PanelTypeGraph {
|
||||
return
|
||||
}
|
||||
for _, result := range results {
|
||||
// A `result` item in `results` contains the query result for individual query.
|
||||
// If there are no series in the result, we add empty series and `fillGap` adds all zeros
|
||||
|
||||
@@ -43,6 +43,7 @@ func TestFillGaps(t *testing.T) {
|
||||
Start: 1000,
|
||||
End: 5000,
|
||||
CompositeQuery: &v3.CompositeQuery{
|
||||
PanelType: v3.PanelTypeGraph,
|
||||
BuilderQueries: map[string]*v3.BuilderQuery{
|
||||
"query1": {
|
||||
QueryName: "query1",
|
||||
@@ -82,6 +83,7 @@ func TestFillGaps(t *testing.T) {
|
||||
Start: 1000,
|
||||
End: 5000,
|
||||
CompositeQuery: &v3.CompositeQuery{
|
||||
PanelType: v3.PanelTypeGraph,
|
||||
BuilderQueries: map[string]*v3.BuilderQuery{
|
||||
"query1": {
|
||||
QueryName: "query1",
|
||||
@@ -121,6 +123,7 @@ func TestFillGaps(t *testing.T) {
|
||||
Start: 1000,
|
||||
End: 5000,
|
||||
CompositeQuery: &v3.CompositeQuery{
|
||||
PanelType: v3.PanelTypeGraph,
|
||||
BuilderQueries: map[string]*v3.BuilderQuery{
|
||||
"query1": {
|
||||
QueryName: "query1",
|
||||
@@ -142,6 +145,39 @@ func TestFillGaps(t *testing.T) {
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Single series with gaps and panel type is not graph",
|
||||
results: []*v3.Result{
|
||||
createResult("query1", []*v3.Series{
|
||||
createSeries([]v3.Point{
|
||||
{Timestamp: 1000, Value: 1.0},
|
||||
{Timestamp: 3000, Value: 3.0},
|
||||
}),
|
||||
}),
|
||||
},
|
||||
params: &v3.QueryRangeParamsV3{
|
||||
Start: 1000,
|
||||
End: 5000,
|
||||
CompositeQuery: &v3.CompositeQuery{
|
||||
PanelType: v3.PanelTypeList,
|
||||
BuilderQueries: map[string]*v3.BuilderQuery{
|
||||
"query1": {
|
||||
QueryName: "query1",
|
||||
Expression: "query1",
|
||||
StepInterval: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []*v3.Result{
|
||||
createResult("query1", []*v3.Series{
|
||||
createSeries([]v3.Point{
|
||||
{Timestamp: 1000, Value: 1.0},
|
||||
{Timestamp: 3000, Value: 3.0},
|
||||
}),
|
||||
}),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Execute test cases
|
||||
|
||||
@@ -53,49 +53,35 @@ func Parse(filters *v3.FilterSet) (string, error) {
|
||||
return "", fmt.Errorf("operator not supported")
|
||||
}
|
||||
|
||||
// TODO(Raj): Remove the use of dot replaced alternative when key
|
||||
// contains underscore after dots are supported in keys
|
||||
names := []string{getName(v.Key)}
|
||||
if strings.Contains(v.Key.Key, "_") {
|
||||
dotKey := v.Key
|
||||
dotKey.Key = strings.Replace(v.Key.Key, "_", ".", -1)
|
||||
names = append(names, getName(dotKey))
|
||||
}
|
||||
name := getName(v.Key)
|
||||
|
||||
filterParts := []string{}
|
||||
for _, name := range names {
|
||||
var filter string
|
||||
var filter string
|
||||
|
||||
switch v.Operator {
|
||||
// uncomment following lines when new version of expr is used
|
||||
// case v3.FilterOperatorIn, v3.FilterOperatorNotIn:
|
||||
// filter = fmt.Sprintf("%s %s list%s", name, logOperatorsToExpr[v.Operator], exprFormattedValue(v.Value))
|
||||
switch v.Operator {
|
||||
// uncomment following lines when new version of expr is used
|
||||
// case v3.FilterOperatorIn, v3.FilterOperatorNotIn:
|
||||
// filter = fmt.Sprintf("%s %s list%s", name, logOperatorsToExpr[v.Operator], exprFormattedValue(v.Value))
|
||||
|
||||
case v3.FilterOperatorExists, v3.FilterOperatorNotExists:
|
||||
filter = fmt.Sprintf("%s %s %s", exprFormattedValue(v.Key.Key), logOperatorsToExpr[v.Operator], getTypeName(v.Key.Type))
|
||||
case v3.FilterOperatorExists, v3.FilterOperatorNotExists:
|
||||
filter = fmt.Sprintf("%s %s %s", exprFormattedValue(v.Key.Key), logOperatorsToExpr[v.Operator], getTypeName(v.Key.Type))
|
||||
|
||||
default:
|
||||
filter = fmt.Sprintf("%s %s %s", name, logOperatorsToExpr[v.Operator], exprFormattedValue(v.Value))
|
||||
default:
|
||||
filter = fmt.Sprintf("%s %s %s", name, logOperatorsToExpr[v.Operator], exprFormattedValue(v.Value))
|
||||
|
||||
if v.Operator == v3.FilterOperatorContains || v.Operator == v3.FilterOperatorNotContains {
|
||||
// `contains` and `ncontains` should be case insensitive to match how they work when querying logs.
|
||||
filter = fmt.Sprintf(
|
||||
"lower(%s) %s lower(%s)",
|
||||
name, logOperatorsToExpr[v.Operator], exprFormattedValue(v.Value),
|
||||
)
|
||||
}
|
||||
|
||||
// Avoid running operators on nil values
|
||||
if v.Operator != v3.FilterOperatorEqual && v.Operator != v3.FilterOperatorNotEqual {
|
||||
filter = fmt.Sprintf("%s != nil && %s", name, filter)
|
||||
}
|
||||
if v.Operator == v3.FilterOperatorContains || v.Operator == v3.FilterOperatorNotContains {
|
||||
// `contains` and `ncontains` should be case insensitive to match how they work when querying logs.
|
||||
filter = fmt.Sprintf(
|
||||
"lower(%s) %s lower(%s)",
|
||||
name, logOperatorsToExpr[v.Operator], exprFormattedValue(v.Value),
|
||||
)
|
||||
}
|
||||
|
||||
filterParts = append(filterParts, filter)
|
||||
// Avoid running operators on nil values
|
||||
if v.Operator != v3.FilterOperatorEqual && v.Operator != v3.FilterOperatorNotEqual {
|
||||
filter = fmt.Sprintf("%s != nil && %s", name, filter)
|
||||
}
|
||||
}
|
||||
|
||||
filter := strings.Join(filterParts, " || ")
|
||||
|
||||
// check if the filter is a correct expression language
|
||||
_, err := expr.Compile(filter)
|
||||
if err != nil {
|
||||
|
||||
@@ -2,6 +2,7 @@ package rules
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
@@ -9,6 +10,7 @@ import (
|
||||
"github.com/jmoiron/sqlx"
|
||||
"go.signoz.io/signoz/pkg/query-service/auth"
|
||||
"go.signoz.io/signoz/pkg/query-service/common"
|
||||
"go.signoz.io/signoz/pkg/query-service/model"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
@@ -43,6 +45,9 @@ type RuleDB interface {
|
||||
|
||||
// GetAllPlannedMaintenance fetches the maintenance definitions from db
|
||||
GetAllPlannedMaintenance(ctx context.Context) ([]PlannedMaintenance, error)
|
||||
|
||||
// used for internal telemetry
|
||||
GetAlertsInfo(ctx context.Context) (*model.AlertsInfo, error)
|
||||
}
|
||||
|
||||
type StoredRule struct {
|
||||
@@ -295,3 +300,33 @@ func (r *ruleDB) EditPlannedMaintenance(ctx context.Context, maintenance Planned
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (r *ruleDB) GetAlertsInfo(ctx context.Context) (*model.AlertsInfo, error) {
|
||||
alertsInfo := model.AlertsInfo{}
|
||||
// fetch alerts from rules db
|
||||
query := "SELECT data FROM rules"
|
||||
var alertsData []string
|
||||
err := r.Select(&alertsData, query)
|
||||
if err != nil {
|
||||
zap.L().Error("Error in processing sql query", zap.Error(err))
|
||||
return &alertsInfo, err
|
||||
}
|
||||
for _, alert := range alertsData {
|
||||
var rule GettableRule
|
||||
err = json.Unmarshal([]byte(alert), &rule)
|
||||
if err != nil {
|
||||
zap.L().Error("invalid rule data", zap.Error(err))
|
||||
continue
|
||||
}
|
||||
if rule.AlertType == "LOGS_BASED_ALERT" {
|
||||
alertsInfo.LogsBasedAlerts = alertsInfo.LogsBasedAlerts + 1
|
||||
} else if rule.AlertType == "METRIC_BASED_ALERT" {
|
||||
alertsInfo.MetricBasedAlerts = alertsInfo.MetricBasedAlerts + 1
|
||||
} else if rule.AlertType == "TRACES_BASED_ALERT" {
|
||||
alertsInfo.TracesBasedAlerts = alertsInfo.TracesBasedAlerts + 1
|
||||
}
|
||||
alertsInfo.TotalAlerts = alertsInfo.TotalAlerts + 1
|
||||
}
|
||||
|
||||
return &alertsInfo, nil
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import (
|
||||
"go.signoz.io/signoz/pkg/query-service/interfaces"
|
||||
"go.signoz.io/signoz/pkg/query-service/model"
|
||||
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
|
||||
"go.signoz.io/signoz/pkg/query-service/telemetry"
|
||||
"go.signoz.io/signoz/pkg/query-service/utils/labels"
|
||||
)
|
||||
|
||||
@@ -112,6 +113,8 @@ func NewManager(o *ManagerOptions) (*Manager, error) {
|
||||
|
||||
db := NewRuleDB(o.DBConn)
|
||||
|
||||
telemetry.GetInstance().SetAlertsInfoCallback(db.GetAlertsInfo)
|
||||
|
||||
m := &Manager{
|
||||
tasks: map[string]Task{},
|
||||
rules: map[string]Rule{},
|
||||
|
||||
@@ -111,13 +111,22 @@ func (r *PromRule) Condition() *RuleCondition {
|
||||
return r.ruleCondition
|
||||
}
|
||||
|
||||
// targetVal returns the target value for the rule condition
|
||||
// when the y-axis and target units are non-empty, it
|
||||
// converts the target value to the y-axis unit
|
||||
func (r *PromRule) targetVal() float64 {
|
||||
if r.ruleCondition == nil || r.ruleCondition.Target == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
// get the converter for the target unit
|
||||
unitConverter := converter.FromUnit(converter.Unit(r.ruleCondition.TargetUnit))
|
||||
value := unitConverter.Convert(converter.Value{F: *r.ruleCondition.Target, U: converter.Unit(r.ruleCondition.TargetUnit)}, converter.Unit(r.Unit()))
|
||||
// convert the target value to the y-axis unit
|
||||
value := unitConverter.Convert(converter.Value{
|
||||
F: *r.ruleCondition.Target,
|
||||
U: converter.Unit(r.ruleCondition.TargetUnit),
|
||||
}, converter.Unit(r.Unit()))
|
||||
|
||||
return value.F
|
||||
}
|
||||
|
||||
@@ -370,8 +379,7 @@ func (r *PromRule) Eval(ctx context.Context, ts time.Time, queriers *Queriers) (
|
||||
}
|
||||
zap.L().Debug("alerting for series", zap.String("name", r.Name()), zap.Any("series", series))
|
||||
|
||||
thresholdFormatter := formatter.FromUnit(r.ruleCondition.TargetUnit)
|
||||
threshold := thresholdFormatter.Format(r.targetVal(), r.ruleCondition.TargetUnit)
|
||||
threshold := valueFormatter.Format(r.targetVal(), r.Unit())
|
||||
|
||||
tmplData := AlertTemplateData(l, valueFormatter.Format(alertSmpl.F, r.Unit()), threshold)
|
||||
// Inject some convenience variables that are easier to remember for users
|
||||
|
||||
@@ -165,13 +165,22 @@ func (r *ThresholdRule) PreferredChannels() []string {
|
||||
return r.preferredChannels
|
||||
}
|
||||
|
||||
// targetVal returns the target value for the rule condition
|
||||
// when the y-axis and target units are non-empty, it
|
||||
// converts the target value to the y-axis unit
|
||||
func (r *ThresholdRule) targetVal() float64 {
|
||||
if r.ruleCondition == nil || r.ruleCondition.Target == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
// get the converter for the target unit
|
||||
unitConverter := converter.FromUnit(converter.Unit(r.ruleCondition.TargetUnit))
|
||||
value := unitConverter.Convert(converter.Value{F: *r.ruleCondition.Target, U: converter.Unit(r.ruleCondition.TargetUnit)}, converter.Unit(r.Unit()))
|
||||
// convert the target value to the y-axis unit
|
||||
value := unitConverter.Convert(converter.Value{
|
||||
F: *r.ruleCondition.Target,
|
||||
U: converter.Unit(r.ruleCondition.TargetUnit),
|
||||
}, converter.Unit(r.Unit()))
|
||||
|
||||
return value.F
|
||||
}
|
||||
|
||||
@@ -874,8 +883,7 @@ func (r *ThresholdRule) Eval(ctx context.Context, ts time.Time, queriers *Querie
|
||||
}
|
||||
|
||||
value := valueFormatter.Format(smpl.V, r.Unit())
|
||||
thresholdFormatter := formatter.FromUnit(r.ruleCondition.TargetUnit)
|
||||
threshold := thresholdFormatter.Format(r.targetVal(), r.ruleCondition.TargetUnit)
|
||||
threshold := valueFormatter.Format(r.targetVal(), r.Unit())
|
||||
zap.L().Debug("Alert template data for rule", zap.String("name", r.Name()), zap.String("formatter", valueFormatter.Name()), zap.String("value", value), zap.String("threshold", threshold))
|
||||
|
||||
tmplData := AlertTemplateData(l, value, threshold)
|
||||
|
||||
@@ -185,6 +185,12 @@ type Telemetry struct {
|
||||
patTokenUser bool
|
||||
countUsers int8
|
||||
mutex sync.RWMutex
|
||||
|
||||
alertsInfoCallback func(ctx context.Context) (*model.AlertsInfo, error)
|
||||
}
|
||||
|
||||
func (a *Telemetry) SetAlertsInfoCallback(callback func(ctx context.Context) (*model.AlertsInfo, error)) {
|
||||
a.alertsInfoCallback = callback
|
||||
}
|
||||
|
||||
func createTelemetry() {
|
||||
@@ -310,7 +316,7 @@ func createTelemetry() {
|
||||
telemetry.SendEvent(TELEMETRY_EVENT_HEART_BEAT, data, user.Email, false, false)
|
||||
}
|
||||
}
|
||||
alertsInfo, err := telemetry.reader.GetAlertsInfo(context.Background())
|
||||
alertsInfo, err := telemetry.alertsInfoCallback(context.Background())
|
||||
if err == nil {
|
||||
dashboardsInfo, err := telemetry.reader.GetDashboardsInfo(context.Background())
|
||||
if err == nil {
|
||||
|
||||
@@ -192,7 +192,7 @@ services:
|
||||
<<: *db-depend
|
||||
|
||||
otel-collector-migrator:
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.24}
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.2}
|
||||
container_name: otel-migrator
|
||||
command:
|
||||
- "--dsn=tcp://clickhouse:9000"
|
||||
@@ -205,7 +205,7 @@ services:
|
||||
# condition: service_healthy
|
||||
|
||||
otel-collector:
|
||||
image: signoz/signoz-otel-collector:0.88.24
|
||||
image: signoz/signoz-otel-collector:0.102.2
|
||||
container_name: signoz-otel-collector
|
||||
command:
|
||||
[
|
||||
|
||||
@@ -91,6 +91,10 @@ func ValidateAndCastValue(v interface{}, dataType v3.AttributeKeyDataType) (inte
|
||||
return x, nil
|
||||
case int, int64:
|
||||
return x, nil
|
||||
case float32:
|
||||
return int64(x), nil
|
||||
case float64:
|
||||
return int64(x), nil
|
||||
case string:
|
||||
int64val, err := strconv.ParseInt(x, 10, 64)
|
||||
if err != nil {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user