Compare commits

..

29 Commits

Author SHA1 Message Date
Rahul Keswani
e31f58a338 chore: added unit tests for help and support section 2024-07-22 13:28:22 +05:30
Srikanth Chekuri
3ecb2e35ef chore: use version v4 for export panel from explorer pages (#5438) 2024-07-12 18:49:24 +05:30
SagarRajput-7
9844dcdfb7 fix: added logic to keep sections uncollapsed for all filtered items (#5371) 2024-07-10 12:43:39 +05:30
SagarRajput-7
ddf5569ce9 fix: added null check on filters obj (#5419)
* fix: added null check on filters obj

* feat: added test cases of undefined filters and items

* feat: added comments
2024-07-10 11:56:11 +05:30
Nityananda Gohain
83455e614e fix: disable removing a selected field (#5457)
* fix: disable removing a selected field

* fix: comment updated with issue link

* fix: remove local db
2024-07-10 11:23:29 +05:30
Srikanth Chekuri
831de18464 fix: concurrent map writes to temporalityMap (#5432) 2024-07-10 11:00:28 +05:30
dependabot[bot]
3b2a811f7b chore(deps): bump google.golang.org/grpc from 1.64.0 to 1.64.1 (#5463)
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.64.0 to 1.64.1.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.64.0...v1.64.1)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-10 08:11:53 +05:30
Yunus M
2c7a5126fd update project maintainers (#5460) 2024-07-10 00:30:25 +05:30
Shaheer Kochai
87f1597d4e fix: prevent overwriting query expression and queryName on switching between panel types (#5430) 2024-07-09 08:13:35 +04:30
Shaheer Kochai
916663b4d5 fix: fix the explorer toolbar buttons padding (#5443) 2024-07-09 08:12:25 +04:30
Shaheer Kochai
b0e355eb64 fix: properly render \n and \t in log details + apply Geist Mono font to the logs (#5347)
* fix: properly render newline and tab in log details

* fix: change font family and add tab size to properly render \t

* feat: apply Geist Mono font to the logs
2024-07-09 08:11:46 +04:30
Shaheer Kochai
69a39531f0 Merge pull request #5440 from SigNoz/feat--add-react-query-dev-tools-in-dev-env
chore: add react-query devtools in development env
2024-07-08 20:01:21 +04:30
SagarRajput-7
9c9ed741b2 feat: changed name from 'Histogram' to 'Frequency chart' (#5369)
* feat: changed name from 'Histogram' to 'Frequence chart'

* feat: cdoe refactor and test case changes

* feat: added test case for frequency chart
2024-07-08 20:02:10 +05:30
SagarRajput-7
e6eaaa660a feat: added invite team member from onboarding flow (#5410)
* feat: added invite team member from onboarding flow

* feat: removed commented code and added text to strings-translations

* feat: added en-gb strings

* feat: added more text to strings

* feat: removed commented code and app.ts changes

* feat: added test case for onboarding and invite flow

* feat: added invite team member logEvents

* feat: resovled comments

* feat: cdoe refactor and test case changes
2024-07-08 19:50:29 +05:30
Vikrant Gupta
79eef5bb91 fix: clickhouse editor cursor sync issue (#5435) 2024-07-08 19:27:02 +05:30
Vikrant Gupta
4d64f1dede chore: better logging for duplicate keyboard shortcuts (#5425)
* chore: better logging for duplicate keyboard shortcuts

* chore: skip flaky test

* fix: make the shortcut error silent in prod
2024-07-08 19:25:50 +05:30
Vikrant Gupta
bf177882e6 fix: resize observer charts issue in alerts builder (#5436) 2024-07-08 19:24:05 +05:30
SagarRajput-7
f6b29999c9 fix: added right margin to facing issues btn on dashboad detail page (#5365)
* fix: added right padding to facing issues btn on dashboad detail page

* fix: added right margin instead of padding
2024-07-08 19:17:27 +05:30
Shaheer Kochai
75815897b0 Merge branch 'develop' into feat--add-react-query-dev-tools-in-dev-env 2024-07-08 10:53:14 +04:30
SagarRajput-7
c9309eecaa feat: added empty states for list, trace and timeSeried view in traces (#5290)
* feat: added empty states for list, trace and timeSeried view in traces

* feat: test case skip

* feat: fixed import order

* feat: added utm parameter link

* feat: added strings

* feat: resovled comments

* feat: added common doclinks util

* feat: test case updated:
2024-07-08 11:19:07 +05:30
ahmadshaheer1
4264fc0f3a feat: add react-query devtools in development env 2024-07-07 10:46:49 +04:30
Prashant Shahi
ef854910db Merge pull request #5437 from SigNoz/sync/signoz-0.49.1
Sync/signoz 0.49.1
2024-07-05 19:56:34 +05:30
Prashant Shahi
6b8b2ae761 Merge pull request #5429 from SigNoz/release/v0.49.x
Release/v0.49.1
2024-07-04 22:37:04 +05:30
Prashant Shahi
a48340a2ea Merge branch 'main' into release/v0.49.x 2024-07-04 22:27:41 +05:30
Prashant Shahi
e542d2ee09 chore(signoz): 📌 pin versions: SigNoz 0.49.1, SigNoz OtelCollector 0.102.2
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2024-07-04 22:24:57 +05:30
Prashant Shahi
08431131a9 Merge branch 'develop' into release/v0.49.x 2024-07-04 22:21:07 +05:30
Nityananda Gohain
1b0ec8ac43 fix: typecase support added for float to int (#5408) 2024-07-04 12:08:42 +05:30
Yunus M
2e0ddc7c7f chore: remove dynamic config invocation (#5416) 2024-07-04 01:07:55 +05:30
Prashant Shahi
858a0cb0de Merge pull request #5418 from SigNoz/release/v0.49.x
Release/v0.49.x
2024-07-03 18:54:14 +05:30
71 changed files with 1317 additions and 358 deletions

View File

@@ -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 />

View File

@@ -146,7 +146,7 @@ services:
condition: on-failure
query-service:
image: signoz/query-service:0.49.0
image: signoz/query-service:0.49.1
command:
[
"-config=/root/config/prometheus.yml",
@@ -199,7 +199,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector:
image: signoz/signoz-otel-collector:0.102.1
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.102.1
image: signoz/signoz-schema-migrator:0.102.2
deploy:
restart_policy:
condition: on-failure

View File

@@ -66,7 +66,7 @@ services:
- --storage.path=/data
otel-collector-migrator:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.1}
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.102.1
image: signoz/signoz-otel-collector:0.102.2
command:
[
"--config=/etc/otel-collector-config.yaml",

View File

@@ -164,7 +164,7 @@ services:
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
query-service:
image: signoz/query-service:${DOCKER_TAG:-0.49.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.49.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.102.1}
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.102.1}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.102.2}
container_name: signoz-otel-collector
command:
[

View File

@@ -164,7 +164,7 @@ services:
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
query-service:
image: signoz/query-service:${DOCKER_TAG:-0.49.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.49.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.102.1}
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.102.1}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.102.2}
container_name: signoz-otel-collector
command:
[

View File

@@ -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'],

Binary file not shown.

View 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"
}

View File

@@ -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 bits enough ⎯ were getting your {{dataSource}}!"
}

View 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"
}

View File

@@ -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);

View File

@@ -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;

View File

@@ -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,
]);

View File

@@ -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;
}

View File

@@ -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]);

View File

@@ -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>
);
}

View File

@@ -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}
/>

View File

@@ -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;

View File

@@ -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,

View File

@@ -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}
/>

View File

@@ -57,6 +57,8 @@
background: rgba(22, 25, 34, 0.4);
.value-field {
font-family: 'Geist Mono';
position: relative;
}

View File

@@ -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>

View File

@@ -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';
}
`;

View File

@@ -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}

View File

@@ -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 bits enough ⎯ were getting your logs!',
),
).toBeInTheDocument();
expect(queryByText('pending_data_placeholder')).toBeInTheDocument();
});
it('check error state', async () => {

View File

@@ -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 bits enough were getting your
logs!
{t('pending_data_placeholder', { dataSource: DataSource.LOGS })}
</Typography>
</div>
</div>

View File

@@ -58,6 +58,7 @@
display: flex;
justify-content: space-between;
align-items: center;
margin-right: 16px;
.dashboard-breadcrumbs {
height: 48px;

View File

@@ -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,

View File

@@ -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

View File

@@ -39,6 +39,7 @@
font-weight: 500;
line-height: 18px; /* 128.571% */
letter-spacing: -0.07px;
cursor: pointer;
}
}
}

View File

@@ -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 (

View File

@@ -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);
}
}

View File

@@ -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>
);
}

View File

@@ -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',
}),
);
});
});

View File

@@ -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);
}
}
}
}

View File

@@ -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">

View File

@@ -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>

View File

@@ -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;

View File

@@ -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>

View File

@@ -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>

View File

@@ -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}
/>

View File

@@ -37,7 +37,7 @@
}
}
.histogram-view-controller {
.frequency-chart-view-controller {
display: flex;
align-items: center;
padding-left: 8px;

View File

@@ -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 () => {

View File

@@ -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] &&

View File

@@ -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 = {

View File

@@ -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}

View File

@@ -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;
}
}
}

View File

@@ -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>
);
}

View File

@@ -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']> = [
{

View File

@@ -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>
);
}

View File

@@ -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],

View File

@@ -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>

View 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',
},
],
},
};

View File

@@ -1,6 +1,7 @@
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';
@@ -89,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)),
),
];

View File

@@ -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();
});
});

View File

@@ -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>

View 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();
});
});

View File

@@ -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),
)

View File

@@ -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'
? {

View File

@@ -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();
});
});
});

View File

@@ -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(

View File

@@ -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',

View File

@@ -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) */
}

View 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;

4
go.mod
View File

@@ -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.1
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

8
go.sum
View File

@@ -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.1 h1:RXzs/dA9IMFGi6mXecEFVvShWfilqx5cCEXmzzvVfK0=
github.com/SigNoz/signoz-otel-collector v0.102.1/go.mod h1:ISAXYhZenojCWg6CdDJtPMpfS6Zwc08+uoxH25tc6Y0=
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=
@@ -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=

View File

@@ -3575,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
}

View File

@@ -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 {

View File

@@ -192,7 +192,7 @@ services:
<<: *db-depend
otel-collector-migrator:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.1}
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.102.1
image: signoz/signoz-otel-collector:0.102.2
container_name: signoz-otel-collector
command:
[

View File

@@ -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 {

View File

@@ -275,6 +275,24 @@ var testValidateAndCastValueData = []struct {
want: nil,
wantErr: true,
},
{
name: "v3.AttributeKeyDataTypeInt64: valid float32",
args: args{
v: float32(1000),
dataType: v3.AttributeKeyDataTypeInt64,
},
want: int64(1000),
wantErr: false,
},
{
name: "v3.AttributeKeyDataTypeInt64: valid float64",
args: args{
v: float64(1000),
dataType: v3.AttributeKeyDataTypeInt64,
},
want: int64(1000),
wantErr: false,
},
}
// Test cases for ValidateAndCastValue function in pkg/query-service/utils/format.go