Compare commits

..

76 Commits

Author SHA1 Message Date
Piyush Singariya
19f4cde52e fix: package test fixed 2026-03-17 13:46:58 +05:30
srikanthccv
fcce8b4008 chore: text search tests 2026-03-17 09:08:02 +05:30
srikanthccv
c2c2678125 chore: keep message contained to field mapper 2026-03-17 08:55:37 +05:30
srikanthccv
73dc095b79 Merge branch 'main' into keep-message-contained 2026-03-17 05:11:24 +05:30
Piyush Singariya
b2adbb510b fix: remove the exception checking 2026-03-16 19:46:13 +05:30
Piyush Singariya
4f7ec4c057 revert: remove unused JSON Field datatype 2026-03-16 16:24:38 +05:30
Piyush Singariya
9c5a5488e2 fix: fallback expr switch case 2026-03-16 16:22:14 +05:30
Ashwin Bhatkal
09b6382820 chore: separate dashboard slider from dashboard provider + refactor (#10572)
* chore: separate dashboard slider from dashboard provider + refactor

* chore: resolve self comments
2026-03-16 08:12:09 +00:00
Ashwin Bhatkal
9689b847f0 chore: add slack notification on dequeue from merge queue (#10580)
* chore: add slack notification on merge queue failure

* chore: break type

* chore: update yaml

* chore: update yaml

* chore: update yaml

* chore: update yaml

* chore: update yaml

* chore: update yaml

* chore: update yaml

* chore: resolve comments
2026-03-16 07:12:19 +00:00
Vishal Sharma
15e5938e95 fix: add allInOneLightMode SVG for light mode (#10589) 2026-03-16 06:59:28 +00:00
Abhi kumar
c5ef455283 fix: added fix for panel setting scrollbar issue (#10587)
Some checks failed
build-staging / prepare (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
* fix: added fix for panel setting scrollbar issue

* fix: added changes for panel switch
2026-03-13 19:30:49 +00:00
Piyush Singariya
9ee23eae06 Merge branch 'main' into merge-json-col-fields 2026-03-13 23:43:27 +05:30
Piyush Singariya
0f36479787 Merge branch 'main' into merge-json-col-fields 2026-03-13 19:55:05 +05:30
Piyush Singariya
95a9a24875 fix: message field key search in JSON Logs (#10577)
* feat: work in progress

* fix: test run success

* fix: in progress

* fix: excluding message from metadata fetch

* test: cleared

* fix: key name in metadata

* fix: uncomment tests

* chore: change to method for staticfields

* fix: remove confusing comments; remove usage of logical keyword

* chore: shift method above business logic

* chore: changes based on review

* fix: comments in metadata_store.go
2026-03-13 19:16:05 +05:30
Piyush Singariya
7d1e39037c chore: minor changes based on review 2026-03-13 13:06:56 +05:30
Piyush Singariya
54f104db5f fix: cursor comments 2026-03-13 12:15:12 +05:30
Piyush Singariya
ab0852bbfb feat: update message as typehint in JSON Column (#10545) 2026-03-11 14:50:59 +05:30
Piyush Singariya
4c7aba680e fix: lint error 2026-03-10 14:00:12 +05:30
Piyush Singariya
23c247a1ba Merge branch 'main' into merge-json-col-fields 2026-03-10 13:28:30 +05:30
Piyush Singariya
4777b13ddf fix: shift warning attachment to getKeySelectors 2026-03-03 13:27:23 +05:30
Piyush Singariya
2d3060bac4 chore: remove unnecessary change 2026-02-25 12:09:30 +05:30
Piyush Singariya
9101d51920 Merge branch 'main' into merge-json-col-fields 2026-02-23 12:50:11 +05:30
Piyush Singariya
82b82b0208 chore: addressing comments from Nitya 2026-02-23 12:49:48 +05:30
Nityananda Gohain
51bd760d9a Merge branch 'main' into merge-json-col-fields 2026-02-20 15:53:31 +05:30
Piyush Singariya
2a492cc783 fix: change warning to a const to fix tests 2026-02-20 14:54:16 +05:30
Piyush Singariya
24afdad36c fix: append warnings from fieldkeys 2026-02-20 14:51:13 +05:30
Piyush Singariya
5d20019207 fix: body.message not being mapped correctly 2026-02-20 14:26:12 +05:30
Piyush Singariya
1963d5811d Merge branch 'main' into merge-json-col-fields 2026-02-20 10:08:25 +05:30
Piyush Singariya
15cfccad74 revert: change ReadMultiple is needed 2026-02-20 10:08:11 +05:30
Piyush Singariya
a0399560e3 revert: remvoing unused function 2026-02-19 16:33:12 +05:30
Piyush Singariya
265e337d5c Merge branch 'main' into merge-json-col-fields 2026-02-19 16:29:55 +05:30
Piyush Singariya
bb8c874755 fix: go lint 2026-02-17 17:19:24 +05:30
Piyush Singariya
13cbe03d64 fix: tests 2026-02-17 16:58:00 +05:30
Piyush Singariya
93621c29b7 fix: go mod changes 2026-02-17 16:29:00 +05:30
Piyush Singariya
2c691b5a75 fix: test fixed 2026-02-17 16:28:54 +05:30
Piyush Singariya
cd7e1bb114 Merge branch 'main' into merge-json-col-fields 2026-02-17 16:22:58 +05:30
Piyush Singariya
a1d2ec8b8a fix: remove unused function 2026-02-17 16:18:38 +05:30
Piyush Singariya
8bbafb52d5 fix: go.mod required changes 2026-02-17 16:16:17 +05:30
Piyush Singariya
075cfab463 feat: mapping body_v2.message:string map to body 2026-02-17 13:26:34 +05:30
Piyush Singariya
86bccaac0c test: blocked on pr #10153 2026-02-16 15:24:31 +05:30
Piyush Singariya
de1aac63c0 revert: more unrelated change 2026-02-16 13:19:49 +05:30
Piyush Singariya
14fe8745b5 Merge branch 'main' into merge-json-col-fields 2026-02-16 13:14:39 +05:30
Piyush Singariya
4013c7ee03 revert: few unrelated changes 2026-02-16 13:13:07 +05:30
Piyush Singariya
0d34360e0b fix: handle datatype collision 2026-01-30 12:17:28 +05:30
srikanthccv
d204c89dec Merge branch 'main' into merge-json-col-fields 2026-01-30 02:12:14 +05:30
Piyush Singariya
8dd33c1ab7 Merge branch 'main' into merge-json-col-fields 2026-01-29 19:57:22 +05:30
Piyush Singariya
8e5c3d5ae1 chore: merge json fields 2026-01-29 16:46:00 +05:30
Piyush Singariya
d45bb52f33 Merge branch 'has-jsonqb' into merge-json-col-fields 2026-01-29 13:21:13 +05:30
Piyush Singariya
e71818292d fix: go test flakiness 2026-01-29 10:17:53 +05:30
Piyush Singariya
37557f7f24 Merge branch 'main' into has-jsonqb 2026-01-29 09:12:22 +05:30
Piyush Singariya
27ff102660 Merge branch 'main' into has-jsonqb 2026-01-28 17:44:48 +05:30
Piyush Singariya
cb2aa4cffd fix: tests 2026-01-28 17:42:17 +05:30
Piyush Singariya
58d1d84ec7 test: fix 2026-01-28 15:34:22 +05:30
Piyush Singariya
d8e116a7bc fix: merge conflict 2026-01-28 15:15:50 +05:30
Piyush Singariya
6a48bdc37e Merge branch 'main' into has-jsonqb 2026-01-28 15:15:17 +05:30
Piyush Singariya
ffb62432f8 chore: var renamed 2026-01-28 14:42:51 +05:30
Piyush Singariya
57c51f070c fix: merge json body columns together 2026-01-28 14:36:15 +05:30
Piyush Singariya
36becfc7a2 fix: removed comment 2026-01-27 13:20:52 +05:30
Piyush Singariya
8e71de09f3 fix: remove unnecessary bool checking 2026-01-27 13:16:30 +05:30
Piyush Singariya
56de92de73 fix: changes based on review from Srikanth 2026-01-27 13:12:32 +05:30
Piyush Singariya
62b10f8e77 Merge branch 'main' into has-jsonqb 2026-01-27 10:04:32 +05:30
Piyush Singariya
20b53d7856 fix: review based on tushar 2026-01-27 10:04:15 +05:30
Piyush Singariya
8f2c506304 fix: json qb test fix 2026-01-22 22:00:06 +05:30
Srikanth Chekuri
7b5b9027dd Merge branch 'main' into has-jsonqb 2026-01-22 20:19:39 +05:30
Piyush Singariya
b77f97fcb7 fix: tests 2026-01-22 17:24:26 +05:30
Piyush Singariya
62942a4162 fix: tests 2026-01-22 15:47:20 +05:30
Piyush Singariya
349bbbbf1d Merge branch 'main' into has-jsonqb 2026-01-22 12:36:45 +05:30
Piyush Singariya
1966a7a5f6 fix: empty filteredArrays condition 2026-01-22 12:36:29 +05:30
Piyush Singariya
a4eed9ff13 fix: build json plans in metadata 2026-01-22 12:33:51 +05:30
Piyush Singariya
24d1ee33b5 revert: gitignore change 2026-01-22 10:39:02 +05:30
Srikanth Chekuri
3402203021 Merge branch 'main' into has-jsonqb 2026-01-21 13:47:39 +05:30
Piyush Singariya
e8e4897cc8 fix: tests GroupBy 2026-01-20 12:10:44 +05:30
Piyush Singariya
96fb88aaee fix: ignored .vscode in gitignore 2026-01-20 11:47:34 +05:30
Piyush Singariya
5a00e6c2cd Merge branch 'main' into has-jsonqb 2026-01-20 11:44:32 +05:30
Piyush Singariya
e2500cff7d fix: tests expected queries and values 2026-01-20 11:40:56 +05:30
Piyush Singariya
4864c3bc37 feat: has JSON QB 2026-01-20 11:23:50 +05:30
53 changed files with 3404 additions and 980 deletions

59
.github/workflows/mergequeueci.yaml vendored Normal file
View File

@@ -0,0 +1,59 @@
name: mergequeueci
on:
pull_request:
types:
- dequeued
jobs:
notify:
runs-on: ubuntu-latest
steps:
- name: alert
uses: slackapi/slack-github-action@v2.1.1
with:
webhook: ${{ secrets.SLACK_MERGE_QUEUE_WEBHOOK }}
webhook-type: incoming-webhook
payload: |
{
"text": ":x: PR removed from merge queue",
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": ":x: PR Removed from Merge Queue"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*<${{ github.event.pull_request.html_url }}|PR #${{ github.event.pull_request.number }}: ${{ github.event.pull_request.title }}>*"
}
},
{
"type": "divider"
},
{
"type": "section",
"fields": [
{
"type": "mrkdwn",
"text": "*Author*\n@${{ github.event.pull_request.user.login }}"
}
]
}
]
}
- name: comment
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
PR_URL: ${{ github.event.pull_request.html_url }}
run: |
gh api repos/${{ github.repository }}/issues/$PR_NUMBER/comments \
-f body="> :x: **PR removed from merge queue**
>
> @$PR_AUTHOR your PR was removed from the merge queue. Fix the issue and re-queue when ready."

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 214 KiB

View File

@@ -1,5 +1,6 @@
// ** Helpers
import { MetrictypesTypeDTO } from 'api/generated/services/sigNoz.schemas';
import { defaultTraceSelectedColumns } from 'container/OptionsMenu/constants';
import { createIdFromObjectFields } from 'lib/createIdFromObjectFields';
import { createNewBuilderItemName } from 'lib/newQueryBuilder/createNewBuilderItemName';
import { IAttributeValuesResponse } from 'types/api/queryBuilder/getAttributesValues';
@@ -548,3 +549,49 @@ export const DATA_TYPE_VS_ATTRIBUTE_VALUES_KEY: Record<
[DataTypes.ArrayBool]: 'boolAttributeValues',
[DataTypes.EMPTY]: 'stringAttributeValues',
};
export const listViewInitialLogQuery: Query = {
...initialQueriesMap.logs,
builder: {
...initialQueriesMap.logs.builder,
queryData: [
{
...initialQueriesMap.logs.builder.queryData[0],
aggregateOperator: LogsAggregatorOperator.NOOP,
orderBy: [{ columnName: 'timestamp', order: 'desc' }],
offset: 0,
pageSize: 100,
},
],
},
};
export const PANEL_TYPES_INITIAL_QUERY: Record<PANEL_TYPES, Query> = {
[PANEL_TYPES.TIME_SERIES]: initialQueriesMap.metrics,
[PANEL_TYPES.VALUE]: initialQueriesMap.metrics,
[PANEL_TYPES.TABLE]: initialQueriesMap.metrics,
[PANEL_TYPES.LIST]: listViewInitialLogQuery,
[PANEL_TYPES.TRACE]: initialQueriesMap.traces,
[PANEL_TYPES.BAR]: initialQueriesMap.metrics,
[PANEL_TYPES.PIE]: initialQueriesMap.metrics,
[PANEL_TYPES.HISTOGRAM]: initialQueriesMap.metrics,
[PANEL_TYPES.EMPTY_WIDGET]: initialQueriesMap.metrics,
};
export const listViewInitialTraceQuery: Query = {
// it should be the above commented query
...initialQueriesMap.traces,
builder: {
...initialQueriesMap.traces.builder,
queryData: [
{
...initialQueriesMap.traces.builder.queryData[0],
aggregateOperator: LogsAggregatorOperator.NOOP,
orderBy: [{ columnName: 'timestamp', order: 'desc' }],
offset: 0,
pageSize: 10,
selectColumns: defaultTraceSelectedColumns,
},
],
},
};

View File

@@ -1,50 +0,0 @@
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import { defaultTraceSelectedColumns } from 'container/OptionsMenu/constants';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { LogsAggregatorOperator } from 'types/common/queryBuilder';
export const PANEL_TYPES_INITIAL_QUERY = {
[PANEL_TYPES.TIME_SERIES]: initialQueriesMap.metrics,
[PANEL_TYPES.VALUE]: initialQueriesMap.metrics,
[PANEL_TYPES.TABLE]: initialQueriesMap.metrics,
[PANEL_TYPES.LIST]: initialQueriesMap.logs,
[PANEL_TYPES.TRACE]: initialQueriesMap.traces,
[PANEL_TYPES.BAR]: initialQueriesMap.metrics,
[PANEL_TYPES.PIE]: initialQueriesMap.metrics,
[PANEL_TYPES.HISTOGRAM]: initialQueriesMap.metrics,
[PANEL_TYPES.EMPTY_WIDGET]: initialQueriesMap.metrics,
};
export const listViewInitialLogQuery: Query = {
...initialQueriesMap.logs,
builder: {
...initialQueriesMap.logs.builder,
queryData: [
{
...initialQueriesMap.logs.builder.queryData[0],
aggregateOperator: LogsAggregatorOperator.NOOP,
orderBy: [{ columnName: 'timestamp', order: 'desc' }],
offset: 0,
pageSize: 100,
},
],
},
};
export const listViewInitialTraceQuery: Query = {
// it should be the above commented query
...initialQueriesMap.traces,
builder: {
...initialQueriesMap.traces.builder,
queryData: [
{
...initialQueriesMap.traces.builder.queryData[0],
aggregateOperator: LogsAggregatorOperator.NOOP,
orderBy: [{ columnName: 'timestamp', order: 'desc' }],
offset: 0,
pageSize: 10,
selectColumns: defaultTraceSelectedColumns,
},
],
},
};

View File

@@ -1,94 +0,0 @@
import { Card, Modal } from 'antd';
import logEvent from 'api/common/logEvent';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import createQueryParams from 'lib/createQueryParams';
import history from 'lib/history';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { LogsAggregatorOperator } from 'types/common/queryBuilder';
import { v4 as uuid } from 'uuid';
import { PANEL_TYPES_INITIAL_QUERY } from './constants';
import menuItems from './menuItems';
import { Text } from './styles';
import './ComponentSlider.styles.scss';
function DashboardGraphSlider(): JSX.Element {
const { handleToggleDashboardSlider, isDashboardSliderOpen } = useDashboard();
const onClickHandler = (name: PANEL_TYPES) => (): void => {
const id = uuid();
handleToggleDashboardSlider(false);
logEvent('Dashboard Detail: New panel type selected', {
// dashboardId: '',
// dashboardName: '',
// numberOfPanels: 0, // todo - at this point we don't know these attributes
panelType: name,
widgetId: id,
});
const queryParamsLog = {
graphType: name,
widgetId: id,
[QueryParams.compositeQuery]: JSON.stringify({
...PANEL_TYPES_INITIAL_QUERY[name],
builder: {
...PANEL_TYPES_INITIAL_QUERY[name].builder,
queryData: [
{
...PANEL_TYPES_INITIAL_QUERY[name].builder.queryData[0],
aggregateOperator: LogsAggregatorOperator.NOOP,
orderBy: [{ columnName: 'timestamp', order: 'desc' }],
offset: 0,
pageSize: 100,
},
],
},
}),
};
const queryParams = {
graphType: name,
widgetId: id,
[QueryParams.compositeQuery]: JSON.stringify(
PANEL_TYPES_INITIAL_QUERY[name],
),
};
if (name === PANEL_TYPES.LIST) {
history.push(
`${history.location.pathname}/new?${createQueryParams(queryParamsLog)}`,
);
} else {
history.push(
`${history.location.pathname}/new?${createQueryParams(queryParams)}`,
);
}
};
const handleCardClick = (panelType: PANEL_TYPES): void => {
onClickHandler(panelType)();
};
return (
<Modal
open={isDashboardSliderOpen}
onCancel={(): void => {
handleToggleDashboardSlider(false);
}}
rootClassName="graph-selection"
footer={null}
title="New Panel"
>
<div className="panel-selection">
{menuItems.map(({ name, icon, display }) => (
<Card onClick={(): void => handleCardClick(name)} id={name} key={name}>
{icon}
<Text>{display}</Text>
</Card>
))}
</div>
</Modal>
);
}
export default DashboardGraphSlider;

View File

@@ -1,41 +0,0 @@
import { Card as CardComponent, Typography } from 'antd';
import styled from 'styled-components';
export const Container = styled.div`
display: flex;
justify-content: right;
gap: 8px;
margin-bottom: 12px;
`;
export const Card = styled(CardComponent)`
min-height: 80px;
min-width: 120px;
overflow-y: auto;
cursor: pointer;
transition: transform 0.2s;
.ant-card-body {
padding: 12px;
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
.ant-typography {
font-size: 12px;
font-weight: 600;
}
}
&:hover {
transform: scale(1.05);
border: 1px solid var(--bg-robin-400);
}
`;
export const Text = styled(Typography)`
text-align: center;
margin-top: 1rem;
`;

View File

@@ -182,9 +182,7 @@ describe('Dashboard landing page actions header tests', () => {
(useLocation as jest.Mock).mockReturnValue(mockLocation);
const mockContextValue: IDashboardContext = {
isDashboardSliderOpen: false,
isDashboardLocked: false,
handleToggleDashboardSlider: jest.fn(),
handleDashboardLockToggle: jest.fn(),
dashboardResponse: {} as IDashboardContext['dashboardResponse'],
selectedDashboard: (getDashboardById.data as unknown) as Dashboard,

View File

@@ -40,6 +40,7 @@ import {
} from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { usePanelTypeSelectionModalStore } from 'providers/Dashboard/helpers/panelTypeSelectionModalHelper';
import { sortLayout } from 'providers/Dashboard/util';
import { DashboardData } from 'types/api/dashboard/getAll';
import { Props } from 'types/api/dashboard/update';
@@ -48,10 +49,10 @@ import { ComponentTypes } from 'utils/permission';
import { v4 as uuid } from 'uuid';
import DashboardHeader from '../components/DashboardHeader/DashboardHeader';
import DashboardGraphSlider from '../ComponentsSlider';
import DashboardSettings from '../DashboardSettings';
import { Base64Icons } from '../DashboardSettings/General/utils';
import DashboardVariableSelection from '../DashboardVariablesSelection';
import PanelTypeSelectionModal from '../PanelTypeSelectionModal';
import SettingsDrawer from './SettingsDrawer';
import { VariablesSettingsTab } from './types';
import {
@@ -69,6 +70,9 @@ interface DashboardDescriptionProps {
// eslint-disable-next-line sonarjs/cognitive-complexity
function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
const { handle } = props;
const setIsPanelTypeSelectionModalOpen = usePanelTypeSelectionModalStore(
(s) => s.setIsPanelTypeSelectionModalOpen,
);
const {
selectedDashboard,
panelMap,
@@ -77,7 +81,6 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
setLayouts,
isDashboardLocked,
setSelectedDashboard,
handleToggleDashboardSlider,
handleDashboardLockToggle,
} = useDashboard();
@@ -145,14 +148,14 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
const [addPanelPermission] = useComponentPermission(permissions, userRole);
const onEmptyWidgetHandler = useCallback(() => {
handleToggleDashboardSlider(true);
setIsPanelTypeSelectionModalOpen(true);
logEvent('Dashboard Detail: Add new panel clicked', {
dashboardId: selectedDashboard?.id,
dashboardName: selectedDashboard?.data.title,
numberOfPanels: selectedDashboard?.data.widgets?.length,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [handleToggleDashboardSlider]);
}, [setIsPanelTypeSelectionModalOpen]);
const handleLockDashboardToggle = (): void => {
setIsDashbordSettingsOpen(false);
@@ -521,7 +524,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
<DashboardVariableSelection />
</section>
)}
<DashboardGraphSlider />
<PanelTypeSelectionModal />
<Modal
open={isRenameDashboardOpen}

View File

@@ -1,4 +1,4 @@
.graph-selection {
.panel-type-selection-modal {
.ant-modal-content {
width: 515px;
max-height: 646px;
@@ -76,6 +76,11 @@
content: none;
}
}
.panel-type-text {
text-align: center;
margin-top: 1rem;
}
}
}
@@ -114,7 +119,7 @@
}
.lightMode {
.graph-selection {
.panel-type-selection-modal {
.ant-modal-content {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);

View File

@@ -0,0 +1,68 @@
import { memo } from 'react';
import { Card, Modal, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES, PANEL_TYPES_INITIAL_QUERY } from 'constants/queryBuilder';
import createQueryParams from 'lib/createQueryParams';
import history from 'lib/history';
import { usePanelTypeSelectionModalStore } from 'providers/Dashboard/helpers/panelTypeSelectionModalHelper';
import { v4 as uuid } from 'uuid';
import { PanelTypesWithData } from './menuItems';
import './PanelTypeSelectionModal.styles.scss';
function PanelTypeSelectionModal(): JSX.Element {
const {
isPanelTypeSelectionModalOpen,
setIsPanelTypeSelectionModalOpen,
} = usePanelTypeSelectionModalStore();
const onClickHandler = (name: PANEL_TYPES) => (): void => {
const id = uuid();
setIsPanelTypeSelectionModalOpen(false);
logEvent('Dashboard Detail: New panel type selected', {
panelType: name,
widgetId: id,
});
const queryParams = {
graphType: name,
widgetId: id,
[QueryParams.compositeQuery]: JSON.stringify(
PANEL_TYPES_INITIAL_QUERY[name],
),
};
history.push(
`${history.location.pathname}/new?${createQueryParams(queryParams)}`,
);
};
const handleCardClick = (panelType: PANEL_TYPES): void => {
onClickHandler(panelType)();
};
return (
<Modal
open={isPanelTypeSelectionModalOpen}
onCancel={(): void => {
setIsPanelTypeSelectionModalOpen(false);
}}
rootClassName="panel-type-selection-modal"
footer={null}
title="New Panel"
>
<div className="panel-selection">
{PanelTypesWithData.map(({ name, icon, display }) => (
<Card onClick={(): void => handleCardClick(name)} id={name} key={name}>
{icon}
<Typography className="panel-type-text">{display}</Typography>
</Card>
))}
</div>
</Modal>
);
}
export default memo(PanelTypeSelectionModal);

View File

@@ -9,7 +9,7 @@ import {
Table,
} from 'lucide-react';
const Items: ItemsProps[] = [
export const PanelTypesWithData: ItemsProps[] = [
{
name: PANEL_TYPES.TIME_SERIES,
icon: <LineChart size={16} color={Color.BG_ROBIN_400} />,
@@ -52,5 +52,3 @@ export interface ItemsProps {
icon: JSX.Element;
display: string;
}
export default Items;

View File

@@ -9,17 +9,18 @@ import DashboardSettings from 'container/DashboardContainer/DashboardSettings';
import useComponentPermission from 'hooks/useComponentPermission';
import { useAppContext } from 'providers/App/App';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { usePanelTypeSelectionModalStore } from 'providers/Dashboard/helpers/panelTypeSelectionModalHelper';
import { ROLES, USER_ROLES } from 'types/roles';
import { ComponentTypes } from 'utils/permission';
import './DashboardEmptyState.styles.scss';
export default function DashboardEmptyState(): JSX.Element {
const {
selectedDashboard,
isDashboardLocked,
handleToggleDashboardSlider,
} = useDashboard();
const setIsPanelTypeSelectionModalOpen = usePanelTypeSelectionModalStore(
(s) => s.setIsPanelTypeSelectionModalOpen,
);
const { selectedDashboard, isDashboardLocked } = useDashboard();
const variablesSettingsTabHandle = useRef<VariablesSettingsTab>(null);
const [isSettingsDrawerOpen, setIsSettingsDrawerOpen] = useState<boolean>(
@@ -41,14 +42,14 @@ export default function DashboardEmptyState(): JSX.Element {
const [addPanelPermission] = useComponentPermission(permissions, userRole);
const onEmptyWidgetHandler = useCallback(() => {
handleToggleDashboardSlider(true);
setIsPanelTypeSelectionModalOpen(true);
logEvent('Dashboard Detail: Add new panel clicked', {
dashboardId: selectedDashboard?.id,
dashboardName: selectedDashboard?.data.title,
numberOfPanels: selectedDashboard?.data.widgets?.length,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [handleToggleDashboardSlider]);
}, [setIsPanelTypeSelectionModalOpen]);
const onConfigureClick = useCallback((): void => {
setIsSettingsDrawerOpen(true);

View File

@@ -2,7 +2,7 @@ import { useCallback } from 'react';
import { Select, Typography } from 'antd';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import GraphTypes from 'container/DashboardContainer/ComponentsSlider/menuItems';
import { PanelTypesWithData } from 'container/DashboardContainer/PanelTypeSelectionModal/menuItems';
import { handleQueryChange } from 'container/NewWidget/utils';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
@@ -59,7 +59,7 @@ function PanelTypeSelector({
data-testid="panel-change-select"
disabled={disabled}
>
{GraphTypes.map((item) => (
{PanelTypesWithData.map((item) => (
<Option key={item.name} value={item.name}>
<div className="view-panel-select-option">
<div className="icon">{item.icon}</div>

View File

@@ -5,6 +5,7 @@ import useComponentPermission from 'hooks/useComponentPermission';
import { EllipsisIcon, PenLine, Plus, X } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { usePanelTypeSelectionModalStore } from 'providers/Dashboard/helpers/panelTypeSelectionModalHelper';
import { setSelectedRowWidgetId } from 'providers/Dashboard/helpers/selectedRowWidgetIdHelper';
import { ROLES, USER_ROLES } from 'types/roles';
import { ComponentTypes } from 'utils/permission';
@@ -34,11 +35,11 @@ export function WidgetRowHeader(props: WidgetRowHeaderProps): JSX.Element {
} = props;
const [isRowSettingsOpen, setIsRowSettingsOpen] = useState<boolean>(false);
const {
handleToggleDashboardSlider,
selectedDashboard,
isDashboardLocked,
} = useDashboard();
const setIsPanelTypeSelectionModalOpen = usePanelTypeSelectionModalStore(
(s) => s.setIsPanelTypeSelectionModalOpen,
);
const { selectedDashboard, isDashboardLocked } = useDashboard();
const permissions: ComponentTypes[] = ['add_panel'];
const { user } = useAppContext();
@@ -87,7 +88,7 @@ export function WidgetRowHeader(props: WidgetRowHeaderProps): JSX.Element {
}
setSelectedRowWidgetId(selectedDashboard.id, id);
handleToggleDashboardSlider(true);
setIsPanelTypeSelectionModalOpen(true);
}}
>
New Panel

View File

@@ -15,6 +15,7 @@ import ROUTES from 'constants/routes';
import { getMetricsListQuery } from 'container/MetricsExplorer/Summary/utils';
import { useGetMetricsList } from 'hooks/metricsExplorer/useGetMetricsList';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useIsDarkMode } from 'hooks/useDarkMode';
import history from 'lib/history';
import cloneDeep from 'lodash-es/cloneDeep';
import { AnimatePresence } from 'motion/react';
@@ -43,6 +44,7 @@ const homeInterval = 30 * 60 * 1000;
// eslint-disable-next-line sonarjs/cognitive-complexity
export default function Home(): JSX.Element {
const { user } = useAppContext();
const isDarkMode = useIsDarkMode();
const [startTime, setStartTime] = useState<number | null>(null);
const [endTime, setEndTime] = useState<number | null>(null);
@@ -680,7 +682,11 @@ export default function Home(): JSX.Element {
<div className="checklist-img-container">
<img
src="/Images/allInOne.svg"
src={
isDarkMode
? '/Images/allInOne.svg'
: '/Images/allInOneLightMode.svg'
}
alt="checklist-img"
className="checklist-img"
/>

View File

@@ -1,5 +1,7 @@
.column-unit-selector {
margin-top: 16px;
display: flex;
flex-direction: column;
gap: 8px;
.heading {
color: var(--bg-vanilla-400);
@@ -30,6 +32,11 @@
width: 100%;
}
}
&-content {
display: flex;
flex-direction: column;
gap: 12px;
}
}
.lightMode {

View File

@@ -72,22 +72,24 @@ export function ColumnUnitSelector(
return (
<section className="column-unit-selector">
<Typography.Text className="heading">Column Units</Typography.Text>
{aggregationQueries.map(({ value, label }) => {
const baseQueryName = value.split('.')[0];
return (
<YAxisUnitSelectorV2
value={columnUnits[value] || ''}
onSelect={(unitValue: string): void =>
handleColumnUnitSelect(value, unitValue)
}
fieldLabel={label}
key={value}
selectedQueryName={baseQueryName}
// Update the column unit value automatically only in create mode
shouldUpdateYAxisUnit={isNewDashboard}
/>
);
})}
<div className="column-unit-selector-content">
{aggregationQueries.map(({ value, label }) => {
const baseQueryName = value.split('.')[0];
return (
<YAxisUnitSelectorV2
value={columnUnits[value] || ''}
onSelect={(unitValue: string): void =>
handleColumnUnitSelect(value, unitValue)
}
fieldLabel={label}
key={value}
selectedQueryName={baseQueryName}
// Update the column unit value automatically only in create mode
shouldUpdateYAxisUnit={isNewDashboard}
/>
);
})}
</div>
</section>
);
}

View File

@@ -56,9 +56,6 @@ describe('ContextLinks Component', () => {
/>,
);
// Check that the component renders
expect(screen.getByText('Context Links')).toBeInTheDocument();
// Check that the add button is present
expect(
screen.getByRole('button', { name: /context link/i }),

View File

@@ -14,7 +14,7 @@ import {
verticalListSortingStrategy,
} from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { Button, Modal, Typography } from 'antd';
import { Button, Modal } from 'antd';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { GripVertical, Pencil, Plus, Trash2 } from 'lucide-react';
import {
@@ -134,11 +134,16 @@ function ContextLinks({
return (
<div className="context-links-container">
<Typography.Text className="context-links-text">
Context Links
</Typography.Text>
<div className="context-links-list">
<Button
type="default"
className="add-context-link-button"
icon={<Plus size={12} />}
style={{ width: '100%' }}
onClick={handleAddContextLink}
>
Add Context Link
</Button>
<OverlayScrollbar>
<DndContext
sensors={sensors}
@@ -160,16 +165,6 @@ function ContextLinks({
</SortableContext>
</DndContext>
</OverlayScrollbar>
{/* button to add context link */}
<Button
type="primary"
className="add-context-link-button"
icon={<Plus size={12} />}
onClick={handleAddContextLink}
>
Context Link
</Button>
</div>
<Modal

View File

@@ -2,7 +2,6 @@
display: flex;
flex-direction: column;
gap: 16px;
margin: 12px;
}
.context-links-text {
@@ -110,10 +109,7 @@
}
.add-context-link-button {
display: flex;
align-items: center;
margin: auto;
width: fit-content;
width: 100%;
}
.lightMode {

View File

@@ -1,6 +1,7 @@
.right-container {
display: flex;
flex-direction: column;
padding-bottom: 48px;
.header {
display: flex;
@@ -24,14 +25,14 @@
letter-spacing: -0.07px;
}
}
.name-description {
.control-container {
display: flex;
flex-direction: column;
padding: 12px 12px 16px 12px;
border-top: 1px solid var(--bg-slate-500);
border-bottom: 1px solid var(--bg-slate-500);
gap: 8px;
}
.name-description {
padding: 0 0 4px 0;
.typography {
color: var(--bg-vanilla-400);
@@ -88,9 +89,6 @@
.panel-config {
display: flex;
flex-direction: column;
padding: 12px 12px 16px 12px;
gap: 8px;
border-bottom: 1px solid var(--bg-slate-500);
.typography {
color: var(--bg-vanilla-400);
@@ -104,6 +102,7 @@
}
.panel-type-select {
width: 100%;
.ant-select-selector {
display: flex;
height: 32px;
@@ -137,7 +136,6 @@
}
.fill-gaps {
margin-top: 16px;
display: flex;
padding: 12px;
justify-content: space-between;
@@ -156,31 +154,24 @@
letter-spacing: 0.52px;
text-transform: uppercase;
}
.fill-gaps-text-description {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 400;
opacity: 0.6;
line-height: 16px; /* 133.333% */
}
}
.log-scale,
.decimal-precision-selector {
margin-top: 16px;
display: flex;
justify-content: space-between;
flex-direction: column;
gap: 8px;
}
.decimal-precision-selector,
.legend-position {
margin-top: 16px;
display: flex;
justify-content: space-between;
flex-direction: column;
gap: 8px;
}
.legend-colors {
margin-top: 16px;
}
.panel-time-text {
margin-top: 16px;
color: var(--bg-vanilla-400);
font-family: 'Space Mono';
font-size: 13px;
@@ -193,7 +184,6 @@
.y-axis-unit-selector,
.y-axis-unit-selector-v2 {
margin-top: 16px;
display: flex;
flex-direction: column;
gap: 8px;
@@ -278,11 +268,8 @@
}
.stack-chart {
margin-top: 16px;
display: flex;
flex-direction: row;
justify-content: space-between;
gap: 8px;
.label {
color: var(--bg-vanilla-400);
font-family: 'Space Mono';
@@ -296,11 +283,6 @@
}
.bucket-config {
margin-top: 16px;
display: flex;
flex-direction: column;
gap: 8px;
.label {
color: var(--bg-vanilla-400);
font-family: 'Space Mono';
@@ -352,16 +334,13 @@
}
}
.context-links {
border-bottom: 1px solid var(--bg-slate-500);
}
.alerts {
display: flex;
padding: 12px;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid var(--bg-slate-500);
padding: 12px;
min-height: 44px;
border-top: 1px solid var(--bg-slate-500);
cursor: pointer;
.left-section {
@@ -387,6 +366,16 @@
color: var(--bg-vanilla-400);
}
}
.context-links {
padding: 12px 12px 16px 12px;
border-bottom: 1px solid var(--bg-slate-500);
}
.thresholds-section {
padding: 12px 12px 16px 12px;
border-top: 1px solid var(--bg-slate-500);
}
}
.select-option {
@@ -418,9 +407,6 @@
}
.name-description {
border-top: 1px solid var(--bg-vanilla-300);
border-bottom: 1px solid var(--bg-vanilla-300);
.typography {
color: var(--bg-ink-400);
}
@@ -441,8 +427,6 @@
}
.panel-config {
border-bottom: 1px solid var(--bg-vanilla-300);
.typography {
color: var(--bg-ink-400);
}
@@ -478,6 +462,9 @@
.fill-gaps-text {
color: var(--bg-ink-400);
}
.fill-gaps-text-description {
color: var(--bg-ink-400);
}
}
.bucket-config {
@@ -530,7 +517,7 @@
}
.alerts {
border-bottom: 1px solid var(--bg-vanilla-300);
border-top: 1px solid var(--bg-vanilla-300);
.left-section {
.bell-icon {
@@ -549,6 +536,10 @@
.context-links {
border-bottom: 1px solid var(--bg-vanilla-300);
}
.thresholds-section {
border-top: 1px solid var(--bg-vanilla-300);
}
}
.select-option {

View File

@@ -1,5 +1,4 @@
.threshold-selector-container {
padding: 12px;
padding-bottom: 80px;
.threshold-select {

View File

@@ -1,10 +1,10 @@
import { useCallback } from 'react';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { Typography } from 'antd';
import { Button } from 'antd';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useGetQueryLabels } from 'hooks/useGetQueryLabels';
import { Antenna, Plus } from 'lucide-react';
import { Plus } from 'lucide-react';
import { v4 as uuid } from 'uuid';
import Threshold from './Threshold';
@@ -68,11 +68,14 @@ function ThresholdSelector({
<DndProvider backend={HTML5Backend}>
<div className="threshold-selector-container">
<div className="threshold-select" onClick={addThresholdHandler}>
<div className="left-section">
<Antenna size={14} className="icon" />
<Typography.Text className="text">Thresholds</Typography.Text>
</div>
<Plus size={14} onClick={addThresholdHandler} className="icon" />
<Button
type="default"
icon={<Plus size={14} />}
style={{ width: '100%' }}
onClick={addThresholdHandler}
>
Add Threshold
</Button>
</div>
{thresholds.map((threshold, idx) => (
<Threshold

View File

@@ -0,0 +1,68 @@
.settings-section {
border-top: 1px solid var(--bg-slate-500);
}
.settings-section-header {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 12px 12px;
min-height: 44px;
background: transparent;
border: none;
outline: none;
cursor: pointer;
.settings-section-title {
display: flex;
align-items: center;
gap: 8px;
color: var(--bg-vanilla-400);
font-family: 'Space Mono';
font-size: 13px;
font-weight: 400;
text-transform: uppercase;
}
.chevron-icon {
color: var(--bg-vanilla-400);
transition: transform 0.2s ease-in-out;
&.open {
transform: rotate(180deg);
}
}
}
.settings-section-content {
padding: 0 12px 0 12px;
display: flex;
flex-direction: column;
gap: 20px;
max-height: 0;
overflow: hidden;
opacity: 0;
transition: max-height 0.25s ease, opacity 0.25s ease, padding 0.25s ease;
&.open {
padding-bottom: 24px;
max-height: 1000px;
opacity: 1;
}
}
.lightMode {
.settings-section-header {
.chevron-icon {
color: var(--bg-ink-400);
}
.settings-section-title {
color: var(--bg-ink-400);
}
}
.settings-section {
border-top: 1px solid var(--bg-vanilla-300);
}
}

View File

@@ -0,0 +1,51 @@
import { ReactNode, useState } from 'react';
import { ChevronDown } from 'lucide-react';
import './SettingsSection.styles.scss';
export interface SettingsSectionProps {
title: string;
defaultOpen?: boolean;
children: ReactNode;
icon?: ReactNode;
}
function SettingsSection({
title,
defaultOpen = false,
children,
icon,
}: SettingsSectionProps): JSX.Element {
const [isOpen, setIsOpen] = useState(defaultOpen);
const toggleOpen = (): void => {
setIsOpen((prev) => !prev);
};
return (
<section className="settings-section">
<button
type="button"
className="settings-section-header"
onClick={toggleOpen}
>
<span className="settings-section-title">
{icon ? icon : null} {title}
</span>
<ChevronDown
size={16}
className={isOpen ? 'chevron-icon open' : 'chevron-icon'}
/>
</button>
<div
className={
isOpen ? 'settings-section-content open' : 'settings-section-content'
}
>
{children}
</div>
</section>
);
}
export default SettingsSection;

View File

@@ -14,23 +14,30 @@ import {
Input,
InputNumber,
Select,
Space,
Switch,
Typography,
} from 'antd';
import { PrecisionOption, PrecisionOptionsEnum } from 'components/Graph/types';
import TimePreference from 'components/TimePreferenceDropDown';
import { PANEL_TYPES, PanelDisplay } from 'constants/queryBuilder';
import GraphTypes, {
import {
ItemsProps,
} from 'container/DashboardContainer/ComponentsSlider/menuItems';
PanelTypesWithData,
} from 'container/DashboardContainer/PanelTypeSelectionModal/menuItems';
import { useDashboardVariables } from 'hooks/dashboard/useDashboardVariables';
import useCreateAlerts from 'hooks/queryBuilder/useCreateAlerts';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import {
Antenna,
Axis3D,
ConciergeBell,
Layers,
LayoutDashboard,
LineChart,
Link,
Pencil,
Plus,
SlidersHorizontal,
Spline,
SquareArrowOutUpRight,
} from 'lucide-react';
@@ -46,6 +53,7 @@ import { DataSource } from 'types/common/queryBuilder';
import { popupContainer } from 'utils/selectPopupContainer';
import { ColumnUnitSelector } from './ColumnUnitSelector/ColumnUnitSelector';
import SettingsSection from './components/SettingsSection/SettingsSection';
import {
panelTypeVsBucketConfig,
panelTypeVsColumnUnitPreferences,
@@ -143,7 +151,7 @@ function RightContainer({
);
const selectedGraphType =
GraphTypes.find((e) => e.name === selectedGraph)?.display || '';
PanelTypesWithData.find((e) => e.name === selectedGraph)?.display || '';
const onCreateAlertsHandler = useCreateAlerts(selectedWidget, 'panelView');
@@ -169,7 +177,7 @@ function RightContainer({
const { currentQuery } = useQueryBuilder();
const [graphTypes, setGraphTypes] = useState<ItemsProps[]>(GraphTypes);
const [graphTypes, setGraphTypes] = useState<ItemsProps[]>(PanelTypesWithData);
const dashboardVariableOptions = useMemo<VariableOption[]>(() => {
return Object.entries(dashboardVariables).map(([, value]) => ({
@@ -178,6 +186,21 @@ function RightContainer({
}));
}, [dashboardVariables]);
const isAxisSectionVisible = useMemo(() => allowSoftMinMax || allowLogScale, [
allowSoftMinMax,
allowLogScale,
]);
const isFormattingSectionVisible = useMemo(
() => allowYAxisUnit || allowDecimalPrecision || allowPanelColumnPreference,
[allowYAxisUnit, allowDecimalPrecision, allowPanelColumnPreference],
);
const isLegendSectionVisible = useMemo(
() => allowLegendPosition || allowLegendColors,
[allowLegendPosition, allowLegendColors],
);
const updateCursorAndDropdown = (value: string, pos: number): void => {
setCursorPos(pos);
const lastDollar = value.lastIndexOf('$', pos - 1);
@@ -193,6 +216,15 @@ function RightContainer({
}, 0);
};
const decimapPrecisionOptions = useMemo(() => {
return [
{ label: '0 decimals', value: PrecisionOptionsEnum.ZERO },
{ label: '1 decimal', value: PrecisionOptionsEnum.ONE },
{ label: '2 decimals', value: PrecisionOptionsEnum.TWO },
{ label: '3 decimals', value: PrecisionOptionsEnum.THREE },
];
}, []);
const handleInputCursor = (): void => {
const pos = inputRef.current?.input?.selectionStart ?? 0;
updateCursorAndDropdown(inputValue, pos);
@@ -241,7 +273,7 @@ function RightContainer({
prev.filter((graph) => graph.name !== PANEL_TYPES.LIST),
);
} else {
setGraphTypes(GraphTypes);
setGraphTypes(PanelTypesWithData);
}
}, [currentQuery]);
@@ -263,269 +295,297 @@ function RightContainer({
<div className="right-container">
<section className="header">
<div className="purple-dot" />
<Typography.Text className="header-text">Panel details</Typography.Text>
<Typography.Text className="header-text">Panel Settings</Typography.Text>
</section>
<section className="name-description">
<Typography.Text className="typography">Name</Typography.Text>
<AutoComplete
options={dashboardVariableOptions}
value={inputValue}
onChange={onInputChange}
onSelect={onSelect}
filterOption={filterOption}
style={{ width: '100%' }}
getPopupContainer={popupContainer}
placeholder="Enter the panel name here..."
open={autoCompleteOpen}
>
<Input
rootClassName="name-input"
ref={inputRef}
onSelect={handleInputCursor}
onClick={handleInputCursor}
onBlur={(): void => setAutoCompleteOpen(false)}
/>
</AutoComplete>
<Typography.Text className="typography">Description</Typography.Text>
<TextArea
placeholder="Enter the panel description here..."
bordered
allowClear
value={description}
onChange={(event): void =>
onChangeHandler(setDescription, event.target.value)
}
rootClassName="description-input"
/>
</section>
<section className="panel-config">
<Typography.Text className="typography">Panel Type</Typography.Text>
<Select
onChange={setGraphHandler}
value={selectedGraph}
style={{ width: '100%' }}
className="panel-type-select"
data-testid="panel-change-select"
data-stacking-state={stackedBarChart ? 'true' : 'false'}
>
{graphTypes.map((item) => (
<Option key={item.name} value={item.name}>
<div className="select-option">
<div className="icon">{item.icon}</div>
<Typography.Text className="display">{item.display}</Typography.Text>
</div>
</Option>
))}
</Select>
{allowFillSpans && (
<Space className="fill-gaps">
<Typography className="fill-gaps-text">Fill gaps</Typography>
<Switch
checked={isFillSpans}
size="small"
onChange={(checked): void => setIsFillSpans(checked)}
<SettingsSection title="General" defaultOpen icon={<Pencil size={14} />}>
<section className="name-description control-container">
<Typography.Text className="typography">Name</Typography.Text>
<AutoComplete
options={dashboardVariableOptions}
value={inputValue}
onChange={onInputChange}
onSelect={onSelect}
filterOption={filterOption}
style={{ width: '100%' }}
getPopupContainer={popupContainer}
placeholder="Enter the panel name here..."
open={autoCompleteOpen}
>
<Input
rootClassName="name-input"
ref={inputRef}
onSelect={handleInputCursor}
onClick={handleInputCursor}
onBlur={(): void => setAutoCompleteOpen(false)}
/>
</Space>
)}
{allowPanelTimePreference && (
<>
<Typography.Text className="panel-time-text">
Panel Time Preference
</Typography.Text>
<TimePreference
{...{
selectedTime,
setSelectedTime,
}}
/>
</>
)}
{allowPanelColumnPreference && (
<ColumnUnitSelector
columnUnits={columnUnits}
setColumnUnits={setColumnUnits}
isNewDashboard={isNewDashboard}
/>
)}
{allowYAxisUnit && (
<DashboardYAxisUnitSelectorWrapper
onSelect={setYAxisUnit}
value={yAxisUnit || ''}
fieldLabel={
selectedGraphType === PanelDisplay.VALUE ||
selectedGraphType === PanelDisplay.PIE
? 'Unit'
: 'Y Axis Unit'
</AutoComplete>
<Typography.Text className="typography">Description</Typography.Text>
<TextArea
placeholder="Enter the panel description here..."
bordered
allowClear
value={description}
onChange={(event): void =>
onChangeHandler(setDescription, event.target.value)
}
// Only update the y-axis unit value automatically in create mode
shouldUpdateYAxisUnit={isNewDashboard}
rootClassName="description-input"
/>
)}
</section>
</SettingsSection>
{allowDecimalPrecision && (
<section className="decimal-precision-selector">
<Typography.Text className="typography">
Decimal Precision
</Typography.Text>
<section className="panel-config">
<SettingsSection
title="Visualization"
defaultOpen
icon={<LayoutDashboard size={14} />}
>
<section className="panel-type control-container">
<Typography.Text className="typography">Panel Type</Typography.Text>
<Select
options={[
{ label: '0 decimals', value: PrecisionOptionsEnum.ZERO },
{ label: '1 decimal', value: PrecisionOptionsEnum.ONE },
{ label: '2 decimals', value: PrecisionOptionsEnum.TWO },
{ label: '3 decimals', value: PrecisionOptionsEnum.THREE },
{ label: '4 decimals', value: PrecisionOptionsEnum.FOUR },
{ label: 'Full Precision', value: PrecisionOptionsEnum.FULL },
]}
value={decimalPrecision}
style={{ width: '100%' }}
onChange={setGraphHandler}
value={selectedGraph}
className="panel-type-select"
defaultValue={PrecisionOptionsEnum.TWO}
onChange={(val: PrecisionOption): void => setDecimalPrecision(val)}
/>
data-testid="panel-change-select"
data-stacking-state={stackedBarChart ? 'true' : 'false'}
>
{graphTypes.map((item) => (
<Option key={item.name} value={item.name}>
<div className="select-option">
<div className="icon">{item.icon}</div>
<Typography.Text className="display">{item.display}</Typography.Text>
</div>
</Option>
))}
</Select>
</section>
)}
{allowSoftMinMax && (
<section className="soft-min-max">
<section className="container">
<Typography.Text className="text">Soft Min</Typography.Text>
<InputNumber
type="number"
value={softMin}
onChange={softMinHandler}
rootClassName="input"
{allowPanelTimePreference && (
<section className="panel-time-preference control-container">
<Typography.Text className="panel-time-text">
Panel Time Preference
</Typography.Text>
<TimePreference
{...{
selectedTime,
setSelectedTime,
}}
/>
</section>
<section className="container">
<Typography.Text className="text">Soft Max</Typography.Text>
<InputNumber
value={softMax}
type="number"
rootClassName="input"
onChange={softMaxHandler}
)}
{allowStackingBarChart && (
<section className="stack-chart control-container">
<Typography.Text className="label">Stack series</Typography.Text>
<Switch
checked={stackedBarChart}
size="small"
onChange={(checked): void => setStackedBarChart(checked)}
/>
</section>
</section>
)}
{allowFillSpans && (
<section className="fill-gaps">
<div className="fill-gaps-text-container">
<Typography className="fill-gaps-text">Fill gaps</Typography>
<Typography.Text className="fill-gaps-text-description">
Fill gaps in data with 0 for continuity
</Typography.Text>
</div>
<Switch
checked={isFillSpans}
size="small"
onChange={(checked): void => setIsFillSpans(checked)}
/>
</section>
)}
</SettingsSection>
{isFormattingSectionVisible && (
<SettingsSection
title="Formatting & Units"
icon={<SlidersHorizontal size={14} />}
>
{allowYAxisUnit && (
<DashboardYAxisUnitSelectorWrapper
onSelect={setYAxisUnit}
value={yAxisUnit || ''}
fieldLabel={
selectedGraphType === PanelDisplay.VALUE ||
selectedGraphType === PanelDisplay.PIE
? 'Unit'
: 'Y Axis Unit'
}
// Only update the y-axis unit value automatically in create mode
shouldUpdateYAxisUnit={isNewDashboard}
/>
)}
{allowDecimalPrecision && (
<section className="decimal-precision-selector control-container">
<Typography.Text className="typography">
Decimal Precision
</Typography.Text>
<Select
options={decimapPrecisionOptions}
value={decimalPrecision}
style={{ width: '100%' }}
className="panel-type-select"
defaultValue={PrecisionOptionsEnum.TWO}
onChange={(val: PrecisionOption): void => setDecimalPrecision(val)}
/>
</section>
)}
{allowPanelColumnPreference && (
<ColumnUnitSelector
columnUnits={columnUnits}
setColumnUnits={setColumnUnits}
isNewDashboard={isNewDashboard}
/>
)}
</SettingsSection>
)}
{allowStackingBarChart && (
<section className="stack-chart">
<Typography.Text className="label">Stack series</Typography.Text>
<Switch
checked={stackedBarChart}
size="small"
onChange={(checked): void => setStackedBarChart(checked)}
/>
</section>
{isAxisSectionVisible && (
<SettingsSection title="Axes" icon={<Axis3D size={14} />}>
{allowSoftMinMax && (
<section className="soft-min-max">
<section className="container">
<Typography.Text className="text">Soft Min</Typography.Text>
<InputNumber
type="number"
value={softMin}
onChange={softMinHandler}
rootClassName="input"
/>
</section>
<section className="container">
<Typography.Text className="text">Soft Max</Typography.Text>
<InputNumber
value={softMax}
type="number"
rootClassName="input"
onChange={softMaxHandler}
/>
</section>
</section>
)}
{allowLogScale && (
<section className="log-scale control-container">
<Typography.Text className="typography">Y Axis Scale</Typography.Text>
<Select
onChange={(value): void =>
setIsLogScale(value === LogScale.LOGARITHMIC)
}
value={isLogScale ? LogScale.LOGARITHMIC : LogScale.LINEAR}
style={{ width: '100%' }}
className="panel-type-select"
defaultValue={LogScale.LINEAR}
>
<Option value={LogScale.LINEAR}>
<div className="select-option">
<div className="icon">
<LineChart size={16} />
</div>
<Typography.Text className="display">Linear</Typography.Text>
</div>
</Option>
<Option value={LogScale.LOGARITHMIC}>
<div className="select-option">
<div className="icon">
<Spline size={16} />
</div>
<Typography.Text className="display">Logarithmic</Typography.Text>
</div>
</Option>
</Select>
</section>
)}
</SettingsSection>
)}
{isLegendSectionVisible && (
<SettingsSection title="Legend" icon={<Layers size={14} />}>
{allowLegendPosition && (
<section className="legend-position control-container">
<Typography.Text className="typography">Position</Typography.Text>
<Select
onChange={(value: LegendPosition): void => setLegendPosition(value)}
value={legendPosition}
style={{ width: '100%' }}
className="panel-type-select"
defaultValue={LegendPosition.BOTTOM}
>
<Option value={LegendPosition.BOTTOM}>
<div className="select-option">
<Typography.Text className="display">Bottom</Typography.Text>
</div>
</Option>
<Option value={LegendPosition.RIGHT}>
<div className="select-option">
<Typography.Text className="display">Right</Typography.Text>
</div>
</Option>
</Select>
</section>
)}
{allowLegendColors && (
<section className="legend-colors">
<LegendColors
customLegendColors={customLegendColors}
setCustomLegendColors={setCustomLegendColors}
queryResponse={queryResponse}
/>
</section>
)}
</SettingsSection>
)}
{allowBucketConfig && (
<section className="bucket-config">
<Typography.Text className="label">Number of buckets</Typography.Text>
<InputNumber
value={bucketCount || null}
type="number"
min={0}
rootClassName="bucket-input"
placeholder="Default: 30"
onChange={(val): void => {
setBucketCount(val || 0);
}}
/>
<Typography.Text className="label bucket-size-label">
Bucket width
</Typography.Text>
<InputNumber
value={bucketWidth || null}
type="number"
precision={2}
placeholder="Default: Auto"
step={0.1}
min={0.0}
rootClassName="bucket-input"
onChange={(val): void => {
setBucketWidth(val || 0);
}}
/>
<section className="combine-hist">
<Typography.Text className="label">
Merge all series into one
</Typography.Text>
<Switch
checked={combineHistogram}
size="small"
onChange={(checked): void => setCombineHistogram(checked)}
<SettingsSection title="Histogram / Buckets">
<section className="bucket-config control-container">
<Typography.Text className="label">Number of buckets</Typography.Text>
<InputNumber
value={bucketCount || null}
type="number"
min={0}
rootClassName="bucket-input"
placeholder="Default: 30"
onChange={(val): void => {
setBucketCount(val || 0);
}}
/>
<Typography.Text className="label bucket-size-label">
Bucket width
</Typography.Text>
<InputNumber
value={bucketWidth || null}
type="number"
precision={2}
placeholder="Default: Auto"
step={0.1}
min={0.0}
rootClassName="bucket-input"
onChange={(val): void => {
setBucketWidth(val || 0);
}}
/>
<section className="combine-hist">
<Typography.Text className="label">
Merge all series into one
</Typography.Text>
<Switch
checked={combineHistogram}
size="small"
onChange={(checked): void => setCombineHistogram(checked)}
/>
</section>
</section>
</section>
)}
{allowLogScale && (
<section className="log-scale">
<Typography.Text className="typography">Y Axis Scale</Typography.Text>
<Select
onChange={(value): void => setIsLogScale(value === LogScale.LOGARITHMIC)}
value={isLogScale ? LogScale.LOGARITHMIC : LogScale.LINEAR}
style={{ width: '100%' }}
className="panel-type-select"
defaultValue={LogScale.LINEAR}
>
<Option value={LogScale.LINEAR}>
<div className="select-option">
<div className="icon">
<LineChart size={16} />
</div>
<Typography.Text className="display">Linear</Typography.Text>
</div>
</Option>
<Option value={LogScale.LOGARITHMIC}>
<div className="select-option">
<div className="icon">
<Spline size={16} />
</div>
<Typography.Text className="display">Logarithmic</Typography.Text>
</div>
</Option>
</Select>
</section>
)}
{allowLegendPosition && (
<section className="legend-position">
<Typography.Text className="typography">Legend Position</Typography.Text>
<Select
onChange={(value: LegendPosition): void => setLegendPosition(value)}
value={legendPosition}
style={{ width: '100%' }}
className="panel-type-select"
defaultValue={LegendPosition.BOTTOM}
>
<Option value={LegendPosition.BOTTOM}>
<div className="select-option">
<Typography.Text className="display">Bottom</Typography.Text>
</div>
</Option>
<Option value={LegendPosition.RIGHT}>
<div className="select-option">
<Typography.Text className="display">Right</Typography.Text>
</div>
</Option>
</Select>
</section>
)}
{allowLegendColors && (
<section className="legend-colors">
<LegendColors
customLegendColors={customLegendColors}
setCustomLegendColors={setCustomLegendColors}
queryResponse={queryResponse}
/>
</section>
</SettingsSection>
)}
</section>
@@ -541,17 +601,25 @@ function RightContainer({
)}
{allowContextLinks && (
<section className="context-links">
<SettingsSection
title="Context Links"
icon={<Link size={14} />}
defaultOpen={!!contextLinks.linksData.length}
>
<ContextLinks
contextLinks={contextLinks}
setContextLinks={setContextLinks}
selectedWidget={selectedWidget}
/>
</section>
</SettingsSection>
)}
{allowThreshold && (
<section>
<SettingsSection
title="Thresholds"
icon={<Antenna size={14} />}
defaultOpen={!!thresholds.length}
>
<ThresholdSelector
thresholds={thresholds}
setThresholds={setThresholds}
@@ -559,7 +627,7 @@ function RightContainer({
selectedGraph={selectedGraph}
columnUnits={columnUnits}
/>
</section>
</SettingsSection>
)}
</div>
);

View File

@@ -36,7 +36,7 @@ const checkStackSeriesState = (
expect(getByTextUtil(container, 'Stack series')).toBeInTheDocument();
const stackSeriesSection = container.querySelector(
'section > .stack-chart',
'.stack-chart',
) as HTMLElement;
expect(stackSeriesSection).toBeInTheDocument();
@@ -326,7 +326,7 @@ describe('Stacking bar in new panel', () => {
expect(getByText('Stack series')).toBeInTheDocument();
// Verify section exists
const section = container.querySelector('section > .stack-chart');
const section = container.querySelector('.stack-chart');
expect(section).toBeInTheDocument();
// Verify switch is present and enabled (ant-switch-checked)

View File

@@ -835,56 +835,54 @@ function NewWidget({
</LeftContainerWrapper>
<RightContainerWrapper>
<OverlayScrollbar>
<RightContainer
setGraphHandler={setGraphHandler}
title={title}
setTitle={setTitle}
description={description}
setDescription={setDescription}
stackedBarChart={stackedBarChart}
setStackedBarChart={setStackedBarChart}
opacity={opacity}
yAxisUnit={yAxisUnit}
columnUnits={columnUnits}
setColumnUnits={setColumnUnits}
bucketCount={bucketCount}
bucketWidth={bucketWidth}
combineHistogram={combineHistogram}
setCombineHistogram={setCombineHistogram}
setBucketWidth={setBucketWidth}
setBucketCount={setBucketCount}
setOpacity={setOpacity}
selectedNullZeroValue={selectedNullZeroValue}
setSelectedNullZeroValue={setSelectedNullZeroValue}
selectedGraph={graphType}
setSelectedTime={setSelectedTime}
selectedTime={selectedTime}
setYAxisUnit={setYAxisUnit}
decimalPrecision={decimalPrecision}
setDecimalPrecision={setDecimalPrecision}
thresholds={thresholds}
setThresholds={setThresholds}
selectedWidget={selectedWidget}
isFillSpans={isFillSpans}
setIsFillSpans={setIsFillSpans}
isLogScale={isLogScale}
setIsLogScale={setIsLogScale}
legendPosition={legendPosition}
setLegendPosition={setLegendPosition}
customLegendColors={customLegendColors}
setCustomLegendColors={setCustomLegendColors}
queryResponse={queryResponse}
softMin={softMin}
setSoftMin={setSoftMin}
softMax={softMax}
setSoftMax={setSoftMax}
contextLinks={contextLinks}
setContextLinks={setContextLinks}
enableDrillDown={enableDrillDown}
isNewDashboard={isNewDashboard}
/>
</OverlayScrollbar>
<RightContainer
setGraphHandler={setGraphHandler}
title={title}
setTitle={setTitle}
description={description}
setDescription={setDescription}
stackedBarChart={stackedBarChart}
setStackedBarChart={setStackedBarChart}
opacity={opacity}
yAxisUnit={yAxisUnit}
columnUnits={columnUnits}
setColumnUnits={setColumnUnits}
bucketCount={bucketCount}
bucketWidth={bucketWidth}
combineHistogram={combineHistogram}
setCombineHistogram={setCombineHistogram}
setBucketWidth={setBucketWidth}
setBucketCount={setBucketCount}
setOpacity={setOpacity}
selectedNullZeroValue={selectedNullZeroValue}
setSelectedNullZeroValue={setSelectedNullZeroValue}
selectedGraph={graphType}
setSelectedTime={setSelectedTime}
selectedTime={selectedTime}
setYAxisUnit={setYAxisUnit}
decimalPrecision={decimalPrecision}
setDecimalPrecision={setDecimalPrecision}
thresholds={thresholds}
setThresholds={setThresholds}
selectedWidget={selectedWidget}
isFillSpans={isFillSpans}
setIsFillSpans={setIsFillSpans}
isLogScale={isLogScale}
setIsLogScale={setIsLogScale}
legendPosition={legendPosition}
setLegendPosition={setLegendPosition}
customLegendColors={customLegendColors}
setCustomLegendColors={setCustomLegendColors}
queryResponse={queryResponse}
softMin={softMin}
setSoftMin={setSoftMin}
softMax={softMax}
setSoftMax={setSoftMax}
contextLinks={contextLinks}
setContextLinks={setContextLinks}
enableDrillDown={enableDrillDown}
isNewDashboard={isNewDashboard}
/>
</RightContainerWrapper>
</PanelContainer>
<Modal

View File

@@ -15,7 +15,14 @@ export const RightContainerWrapper = styled(Col)`
overflow-y: auto;
}
&::-webkit-scrollbar {
width: 0rem;
width: 0.3rem;
}
&::-webkit-scrollbar-thumb {
background: rgb(136, 136, 136);
border-radius: 0.625rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
`;

View File

@@ -11,11 +11,8 @@ import { getYAxisCategories } from 'components/YAxisUnitSelector/utils';
import {
initialQueryBuilderFormValuesMap,
PANEL_TYPES,
} from 'constants/queryBuilder';
import {
listViewInitialLogQuery,
PANEL_TYPES_INITIAL_QUERY,
} from 'container/DashboardContainer/ComponentsSlider/constants';
} from 'constants/queryBuilder';
import {
defaultLogsSelectedColumns,
defaultTraceSelectedColumns,
@@ -549,10 +546,7 @@ export const getDefaultWidgetData = (
nullZeroValues: '',
opacity: '',
panelTypes: name,
query:
name === PANEL_TYPES.LIST
? listViewInitialLogQuery
: PANEL_TYPES_INITIAL_QUERY[name],
query: PANEL_TYPES_INITIAL_QUERY[name],
timePreferance: 'GLOBAL_TIME',
softMax: null,
softMin: null,

View File

@@ -12,6 +12,8 @@ import {
ATTRIBUTE_TYPES,
initialAutocompleteData,
initialQueryBuilderFormValuesMap,
listViewInitialLogQuery,
listViewInitialTraceQuery,
mapOfFormulaToFilters,
mapOfQueryFilters,
PANEL_TYPES,
@@ -23,10 +25,6 @@ import {
metricsUnknownSpaceAggregateOperatorOptions,
metricsUnknownTimeAggregateOperatorOptions,
} from 'constants/queryBuilderOperators';
import {
listViewInitialLogQuery,
listViewInitialTraceQuery,
} from 'container/DashboardContainer/ComponentsSlider/constants';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { getMetricsOperatorsByAttributeType } from 'lib/newQueryBuilder/getMetricsOperatorsByAttributeType';
import { getOperatorsBySourceAndPanelType } from 'lib/newQueryBuilder/getOperatorsBySourceAndPanelType';

View File

@@ -53,9 +53,7 @@ import { IDashboardContext, WidgetColumnWidths } from './types';
import { sortLayout } from './util';
export const DashboardContext = createContext<IDashboardContext>({
isDashboardSliderOpen: false,
isDashboardLocked: false,
handleToggleDashboardSlider: () => {},
handleDashboardLockToggle: () => {},
dashboardResponse: {} as UseQueryResult<
SuccessResponseV2<Dashboard>,
@@ -82,8 +80,6 @@ export function DashboardProvider({
children,
dashboardId,
}: PropsWithChildren<{ dashboardId: string }>): JSX.Element {
const [isDashboardSliderOpen, setIsDashboardSlider] = useState<boolean>(false);
const [isDashboardLocked, setIsDashboardLocked] = useState<boolean>(false);
const [
@@ -288,13 +284,8 @@ export function DashboardProvider({
}
}, [isVisible]);
const handleToggleDashboardSlider = (value: boolean): void => {
setIsDashboardSlider(value);
};
const { mutate: lockDashboard } = useMutation(locked, {
onSuccess: (_, props) => {
setIsDashboardSlider(false);
setIsDashboardLocked(props.lock);
},
onError: (error) => {
@@ -319,9 +310,7 @@ export function DashboardProvider({
const value: IDashboardContext = useMemo(
() => ({
isDashboardSliderOpen,
isDashboardLocked,
handleToggleDashboardSlider,
handleDashboardLockToggle,
dashboardResponse,
selectedDashboard,
@@ -341,7 +330,6 @@ export function DashboardProvider({
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[
isDashboardSliderOpen,
isDashboardLocked,
dashboardResponse,
selectedDashboard,

View File

@@ -0,0 +1,18 @@
import { create } from 'zustand';
interface IPanelTypeSelectionModalState {
isPanelTypeSelectionModalOpen: boolean;
setIsPanelTypeSelectionModalOpen: (isOpen: boolean) => void;
}
/**
* This helper is used for selecting the panel type when creating a new panel in the dashboard.
* It uses Zustand for state management to keep track of whether the panel type selection modal is open or closed.
*/
export const usePanelTypeSelectionModalStore = create<IPanelTypeSelectionModalState>(
(set) => ({
isPanelTypeSelectionModalOpen: false,
setIsPanelTypeSelectionModalOpen: (isOpen): void =>
set({ isPanelTypeSelectionModalOpen: isOpen }),
}),
);

View File

@@ -9,9 +9,7 @@ export type WidgetColumnWidths = {
};
export interface IDashboardContext {
isDashboardSliderOpen: boolean;
isDashboardLocked: boolean;
handleToggleDashboardSlider: (value: boolean) => void;
handleDashboardLockToggle: (value: boolean) => void;
dashboardResponse: UseQueryResult<SuccessResponseV2<Dashboard>, unknown>;
selectedDashboard: Dashboard | undefined;

2
go.mod
View File

@@ -11,7 +11,6 @@ require (
github.com/SigNoz/signoz-otel-collector v0.144.2
github.com/antlr4-go/antlr/v4 v4.13.1
github.com/antonmedv/expr v1.15.3
github.com/bytedance/sonic v1.14.1
github.com/cespare/xxhash/v2 v2.3.0
github.com/coreos/go-oidc/v3 v3.17.0
github.com/dgraph-io/ristretto/v2 v2.3.0
@@ -106,6 +105,7 @@ require (
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 // indirect
github.com/aws/smithy-go v1.24.0 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.14.1 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect

View File

@@ -78,7 +78,7 @@ func (m *module) ListPromotedAndIndexedPaths(ctx context.Context) ([]promotetype
// add the paths that are not promoted but have indexes
for path, indexes := range aggr {
path := strings.TrimPrefix(path, telemetrylogs.BodyJSONColumnPrefix)
path := strings.TrimPrefix(path, telemetrylogs.BodyV2ColumnPrefix)
path = telemetrytypes.BodyJSONStringSearchPrefix + path
response = append(response, promotetypes.PromotePath{
Path: path,
@@ -163,7 +163,7 @@ func (m *module) PromoteAndIndexPaths(
}
}
if len(it.Indexes) > 0 {
parentColumn := telemetrylogs.LogsV2BodyJSONColumn
parentColumn := telemetrylogs.LogsV2BodyV2Column
// if the path is already promoted or is being promoted, add it to the promoted column
if _, promoted := existingPromotedPaths[it.Path]; promoted || it.Promote {
parentColumn = telemetrylogs.LogsV2BodyPromotedColumn

View File

@@ -10,13 +10,11 @@ import (
"github.com/ClickHouse/clickhouse-go/v2"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/telemetrylogs"
"github.com/SigNoz/signoz/pkg/telemetrystore"
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
"github.com/SigNoz/signoz/pkg/types/instrumentationtypes"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/bytedance/sonic"
)
type builderQuery[T any] struct {
@@ -262,40 +260,6 @@ func (q *builderQuery[T]) executeWithContext(ctx context.Context, query string,
return nil, err
}
// merge body_json and promoted into body
if q.spec.Signal == telemetrytypes.SignalLogs {
switch typedPayload := payload.(type) {
case *qbtypes.RawData:
for _, rr := range typedPayload.Rows {
seeder := func() error {
body, ok := rr.Data[telemetrylogs.LogsV2BodyJSONColumn].(map[string]any)
if !ok {
return nil
}
promoted, ok := rr.Data[telemetrylogs.LogsV2BodyPromotedColumn].(map[string]any)
if !ok {
return nil
}
seed(promoted, body)
str, err := sonic.MarshalString(body)
if err != nil {
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to marshal body")
}
rr.Data["body"] = str
return nil
}
err := seeder()
if err != nil {
return nil, err
}
delete(rr.Data, telemetrylogs.LogsV2BodyJSONColumn)
delete(rr.Data, telemetrylogs.LogsV2BodyPromotedColumn)
}
payload = typedPayload
}
}
return &qbtypes.Result{
Type: q.kind,
Value: payload,
@@ -423,18 +387,3 @@ func decodeCursor(cur string) (int64, error) {
}
return strconv.ParseInt(string(b), 10, 64)
}
func seed(promoted map[string]any, body map[string]any) {
for key, fromValue := range promoted {
if toValue, ok := body[key]; !ok {
body[key] = fromValue
} else {
if fromValue, ok := fromValue.(map[string]any); ok {
if toValue, ok := toValue.(map[string]any); ok {
seed(fromValue, toValue)
body[key] = toValue
}
}
}
}
}

View File

@@ -14,7 +14,6 @@ import (
"github.com/ClickHouse/clickhouse-go/v2/lib/driver"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/bytedance/sonic"
)
var (
@@ -394,17 +393,11 @@ func readAsRaw(rows driver.Rows, queryName string) (*qbtypes.RawData, error) {
// de-reference the typed pointer to any
val := reflect.ValueOf(cellPtr).Elem().Interface()
// Post-process JSON columns: normalize into structured values
// Post-process JSON columns: normalize into String value
if strings.HasPrefix(strings.ToUpper(colTypes[i].DatabaseTypeName()), "JSON") {
switch x := val.(type) {
case []byte:
if len(x) > 0 {
var v any
if err := sonic.Unmarshal(x, &v); err == nil {
val = v
}
}
val = string(x)
default:
// already a structured type (map[string]any, []any, etc.)
}

View File

@@ -219,7 +219,6 @@ func DataTypeCollisionHandledFieldName(key *telemetrytypes.TelemetryFieldKey, va
// we don't have a toBoolOrNull in ClickHouse, so we need to convert the bool to a string
value = fmt.Sprintf("%t", v)
}
case telemetrytypes.FieldDataTypeInt64,
telemetrytypes.FieldDataTypeArrayInt64,
telemetrytypes.FieldDataTypeNumber,

View File

@@ -313,37 +313,30 @@ func (v *filterExpressionVisitor) VisitPrimary(ctx *grammar.PrimaryContext) any
return ""
}
child := ctx.GetChild(0)
var searchText string
if keyCtx, ok := child.(*grammar.KeyContext); ok {
// create a full text search condition on the body field
keyText := keyCtx.GetText()
cond, err := v.conditionBuilder.ConditionFor(context.Background(), v.fullTextColumn, qbtypes.FilterOperatorRegexp, FormatFullTextSearch(keyText), v.builder, v.startNs, v.endNs)
if err != nil {
v.errors = append(v.errors, fmt.Sprintf("failed to build full text search condition: %s", err.Error()))
return ""
}
return cond
searchText = keyCtx.GetText()
} else if valCtx, ok := child.(*grammar.ValueContext); ok {
var text string
if valCtx.QUOTED_TEXT() != nil {
text = trimQuotes(valCtx.QUOTED_TEXT().GetText())
searchText = trimQuotes(valCtx.QUOTED_TEXT().GetText())
} else if valCtx.NUMBER() != nil {
text = valCtx.NUMBER().GetText()
searchText = valCtx.NUMBER().GetText()
} else if valCtx.BOOL() != nil {
text = valCtx.BOOL().GetText()
searchText = valCtx.BOOL().GetText()
} else if valCtx.KEY() != nil {
text = valCtx.KEY().GetText()
searchText = valCtx.KEY().GetText()
} else {
v.errors = append(v.errors, fmt.Sprintf("unsupported value type: %s", valCtx.GetText()))
return ""
}
cond, err := v.conditionBuilder.ConditionFor(context.Background(), v.fullTextColumn, qbtypes.FilterOperatorRegexp, FormatFullTextSearch(text), v.builder, v.startNs, v.endNs)
if err != nil {
v.errors = append(v.errors, fmt.Sprintf("failed to build full text search condition: %s", err.Error()))
return ""
}
return cond
}
cond, err := v.conditionBuilder.ConditionFor(context.Background(), v.fullTextColumn, qbtypes.FilterOperatorRegexp, FormatFullTextSearch(searchText), v.builder, v.startNs, v.endNs)
if err != nil {
v.errors = append(v.errors, fmt.Sprintf("failed to build full text search condition: %s", err.Error()))
return ""
}
return cond
}
return "" // Should not happen with valid input
@@ -383,6 +376,7 @@ func (v *filterExpressionVisitor) VisitComparison(ctx *grammar.ComparisonContext
for _, key := range keys {
condition, err := v.conditionBuilder.ConditionFor(context.Background(), key, op, nil, v.builder, v.startNs, v.endNs)
if err != nil {
v.errors = append(v.errors, fmt.Sprintf("failed to build condition: %s", err.Error()))
return ""
}
conds = append(conds, condition)
@@ -648,7 +642,6 @@ func (v *filterExpressionVisitor) VisitValueList(ctx *grammar.ValueListContext)
// VisitFullText handles standalone quoted strings for full-text search
func (v *filterExpressionVisitor) VisitFullText(ctx *grammar.FullTextContext) any {
if v.skipFullTextFilter {
return ""
}
@@ -670,6 +663,7 @@ func (v *filterExpressionVisitor) VisitFullText(ctx *grammar.FullTextContext) an
v.errors = append(v.errors, fmt.Sprintf("failed to build full text search condition: %s", err.Error()))
return ""
}
return cond
}

View File

@@ -3,14 +3,12 @@ package telemetrylogs
import (
"context"
"fmt"
"slices"
schema "github.com/SigNoz/signoz-otel-collector/cmd/signozschemamigrator/schema_migrator"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/querybuilder"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"golang.org/x/exp/maps"
"github.com/huandu/go-sqlbuilder"
)
@@ -35,7 +33,7 @@ func (c *conditionBuilder) conditionFor(
return "", err
}
if column.Type.GetType() == schema.ColumnTypeEnumJSON && querybuilder.BodyJSONQueryEnabled {
if column.Type.GetType() == schema.ColumnTypeEnumJSON && querybuilder.BodyJSONQueryEnabled && key.Name != messageSubField {
valueType, value := InferDataType(value, operator, key)
cond, err := NewJSONConditionBuilder(key, valueType).buildJSONCondition(operator, value, sb)
if err != nil {
@@ -54,14 +52,14 @@ func (c *conditionBuilder) conditionFor(
}
// Check if this is a body JSON search - either by FieldContext
if key.FieldContext == telemetrytypes.FieldContextBody {
if key.FieldContext == telemetrytypes.FieldContextBody && !querybuilder.BodyJSONQueryEnabled {
tblFieldName, value = GetBodyJSONKey(ctx, key, operator, value)
}
tblFieldName, value = querybuilder.DataTypeCollisionHandledFieldName(key, value, tblFieldName, operator)
// make use of case insensitive index for body
if tblFieldName == "body" {
if tblFieldName == "body" || tblFieldName == messageSubColumn {
switch operator {
case qbtypes.FilterOperatorLike:
return sb.ILike(tblFieldName, value), nil
@@ -108,7 +106,6 @@ func (c *conditionBuilder) conditionFor(
return sb.ILike(tblFieldName, fmt.Sprintf("%%%s%%", value)), nil
case qbtypes.FilterOperatorNotContains:
return sb.NotILike(tblFieldName, fmt.Sprintf("%%%s%%", value)), nil
case qbtypes.FilterOperatorRegexp:
// Note: Escape $$ to $$$$ to avoid sqlbuilder interpreting materialized $ signs
// Only needed because we are using sprintf instead of sb.Match (not implemented in sqlbuilder)
@@ -178,9 +175,8 @@ func (c *conditionBuilder) conditionFor(
case schema.ColumnTypeEnumJSON:
if operator == qbtypes.FilterOperatorExists {
return sb.IsNotNull(tblFieldName), nil
} else {
return sb.IsNull(tblFieldName), nil
}
return sb.IsNull(tblFieldName), nil
case schema.ColumnTypeEnumLowCardinality:
switch elementType := column.Type.(schema.LowCardinalityColumnType).ElementType; elementType.GetType() {
case schema.ColumnTypeEnumString:
@@ -247,19 +243,30 @@ func (c *conditionBuilder) ConditionFor(
return "", err
}
if !(key.FieldContext == telemetrytypes.FieldContextBody && querybuilder.BodyJSONQueryEnabled) && operator.AddDefaultExistsFilter() {
// skip adding exists filter for intrinsic fields
// with an exception for body json search
field, _ := c.fm.FieldFor(ctx, key)
if slices.Contains(maps.Keys(IntrinsicFields), field) && key.FieldContext != telemetrytypes.FieldContextBody {
// Skip adding exists filter for intrinsic fields i.e. Table level log context fields
buildExistCondition := operator.AddDefaultExistsFilter()
switch key.FieldContext {
case telemetrytypes.FieldContextLog, telemetrytypes.FieldContextScope:
// pass; No need to build exist condition for top level columns
// immidiately return
return condition, nil
case telemetrytypes.FieldContextResource, telemetrytypes.FieldContextAttribute:
// build exist condition for resource and attribute fields based on filter operator
case telemetrytypes.FieldContextBody:
// Querying JSON fields already account for Nullability of fields
// so additional exists checks are not needed
if querybuilder.BodyJSONQueryEnabled {
return condition, nil
}
}
if buildExistCondition {
existsCondition, err := c.conditionFor(ctx, key, qbtypes.FilterOperatorExists, nil, sb)
if err != nil {
return "", err
}
return sb.And(condition, existsCondition), nil
}
return condition, nil
}

View File

@@ -127,7 +127,8 @@ func TestConditionFor(t *testing.T) {
{
name: "Contains operator - body",
key: telemetrytypes.TelemetryFieldKey{
Name: "body",
Name: "body",
FieldContext: telemetrytypes.FieldContextLog,
},
operator: qbtypes.FilterOperatorContains,
value: 521509198310,

View File

@@ -1,7 +1,10 @@
package telemetrylogs
import (
"fmt"
"github.com/SigNoz/signoz-otel-collector/constants"
"github.com/SigNoz/signoz/pkg/querybuilder"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
)
@@ -17,7 +20,7 @@ const (
LogsV2TimestampColumn = "timestamp"
LogsV2ObservedTimestampColumn = "observed_timestamp"
LogsV2BodyColumn = "body"
LogsV2BodyJSONColumn = constants.BodyV2Column
LogsV2BodyV2Column = constants.BodyV2Column
LogsV2BodyPromotedColumn = constants.BodyPromotedColumn
LogsV2TraceIDColumn = "trace_id"
LogsV2SpanIDColumn = "span_id"
@@ -34,8 +37,14 @@ const (
LogsV2ResourcesStringColumn = "resources_string"
LogsV2ScopeStringColumn = "scope_string"
BodyJSONColumnPrefix = constants.BodyV2ColumnPrefix
BodyV2ColumnPrefix = constants.BodyV2ColumnPrefix
BodyPromotedColumnPrefix = constants.BodyPromotedColumnPrefix
// messageSubColumn is the ClickHouse sub-column that body searches map to
// when BodyJSONQueryEnabled is true.
messageSubField = "message"
messageSubColumn = "body_v2.message"
bodySearchDefaultWarning = "body searches default to `body.message:string`. Use `body.<key>` to search a different field inside body"
)
var (
@@ -118,3 +127,11 @@ var (
},
}
)
func bodyAliasExpression() string {
if !querybuilder.BodyJSONQueryEnabled {
return LogsV2BodyColumn
}
return fmt.Sprintf("%s as body", LogsV2BodyV2Column)
}

View File

@@ -30,7 +30,8 @@ var (
"severity_text": {Name: "severity_text", Type: schema.LowCardinalityColumnType{ElementType: schema.ColumnTypeString}},
"severity_number": {Name: "severity_number", Type: schema.ColumnTypeUInt8},
"body": {Name: "body", Type: schema.ColumnTypeString},
LogsV2BodyJSONColumn: {Name: LogsV2BodyJSONColumn, Type: schema.JSONColumnType{
messageSubColumn: {Name: messageSubColumn, Type: schema.ColumnTypeString},
LogsV2BodyV2Column: {Name: LogsV2BodyV2Column, Type: schema.JSONColumnType{
MaxDynamicTypes: utils.ToPointer(uint(32)),
MaxDynamicPaths: utils.ToPointer(uint(0)),
}},
@@ -88,21 +89,26 @@ func (m *fieldMapper) getColumn(_ context.Context, key *telemetrytypes.Telemetry
return logsV2Columns["attributes_bool"], nil
}
case telemetrytypes.FieldContextBody:
// Body context is for JSON body fields
// Use body_json if feature flag is enabled
// Body context is for JSON body fields. Use body_v2 if feature flag is enabled.
if querybuilder.BodyJSONQueryEnabled {
return logsV2Columns[LogsV2BodyJSONColumn], nil
if key.Name == messageSubField {
return logsV2Columns[messageSubColumn], nil
}
return logsV2Columns[LogsV2BodyV2Column], nil
}
// Fall back to legacy body column
return logsV2Columns["body"], nil
case telemetrytypes.FieldContextLog, telemetrytypes.FieldContextUnspecified:
if key.Name == LogsV2BodyColumn && querybuilder.BodyJSONQueryEnabled {
return logsV2Columns[messageSubColumn], nil
}
col, ok := logsV2Columns[key.Name]
if !ok {
// check if the key has body JSON search
if strings.HasPrefix(key.Name, telemetrytypes.BodyJSONStringSearchPrefix) {
// Use body_json if feature flag is enabled and we have a body condition builder
// Use body_v2 if feature flag is enabled and we have a body condition builder
if querybuilder.BodyJSONQueryEnabled {
return logsV2Columns[LogsV2BodyJSONColumn], nil
return logsV2Columns[LogsV2BodyV2Column], nil
}
// Fall back to legacy body column
return logsV2Columns["body"], nil
@@ -138,6 +144,10 @@ func (m *fieldMapper) FieldFor(ctx context.Context, key *telemetrytypes.Telemetr
}
return fmt.Sprintf("multiIf(%s.`%s` IS NOT NULL, %s.`%s`::String, mapContains(%s, '%s'), %s, NULL)", column.Name, key.Name, column.Name, key.Name, oldColumn.Name, key.Name, oldKeyName), nil
case telemetrytypes.FieldContextBody:
if key.Name == messageSubField {
return messageSubColumn, nil
}
if key.JSONDataType == nil {
return "", qbtypes.ErrColumnNotFound
}
@@ -246,34 +256,37 @@ func (m *fieldMapper) buildFieldForJSON(key *telemetrytypes.TelemetryFieldKey) (
node := plan[0]
expr := fmt.Sprintf("dynamicElement(%s, '%s')", node.FieldPath(), node.TerminalConfig.ElemType.StringValue())
if key.Materialized {
if len(plan) < 2 {
return "", errors.Newf(errors.TypeUnexpected, CodePromotedPlanMissing,
"plan length is less than 2 for promoted path: %s", key.Name)
}
// TODO(Piyush): Promoted path logic commented out. Materialized now means type hint
// promotion will be extracted from key field evolution
// (direct sub-column access), not a promoted body_promoted.* column.
// if key.Materialized {
// if len(plan) < 2 {
// return "", errors.Newf(errors.TypeUnexpected, CodePromotedPlanMissing,
// "plan length is less than 2 for promoted path: %s", key.Name)
// }
node := plan[1]
promotedExpr := fmt.Sprintf(
"dynamicElement(%s, '%s')",
node.FieldPath(),
node.TerminalConfig.ElemType.StringValue(),
)
// node := plan[1]
// promotedExpr := fmt.Sprintf(
// "dynamicElement(%s, '%s')",
// node.FieldPath(),
// node.TerminalConfig.ElemType.StringValue(),
// )
// dynamicElement returns NULL for scalar types or an empty array for array types.
if node.TerminalConfig.ElemType.IsArray {
expr = fmt.Sprintf(
"if(length(%s) > 0, %s, %s)",
promotedExpr,
promotedExpr,
expr,
)
} else {
// promoted column first then body_json column
// TODO(Piyush): Change this in future for better performance
expr = fmt.Sprintf("coalesce(%s, %s)", promotedExpr, expr)
}
// // dynamicElement returns NULL for scalar types or an empty array for array types.
// if node.TerminalConfig.ElemType.IsArray {
// expr = fmt.Sprintf(
// "if(length(%s) > 0, %s, %s)",
// promotedExpr,
// promotedExpr,
// expr,
// )
// } else {
// // promoted column first then body_json column
// // TODO(Piyush): Change this in future for better performance
// expr = fmt.Sprintf("coalesce(%s, %s)", promotedExpr, expr)
// }
}
// }
return expr, nil
}

View File

@@ -30,7 +30,7 @@ func NewJSONConditionBuilder(key *telemetrytypes.TelemetryFieldKey, valueType te
return &jsonConditionBuilder{key: key, valueType: telemetrytypes.MappingFieldDataTypeToJSONDataType[valueType]}
}
// BuildCondition builds the full WHERE condition for body_json JSON paths
// BuildCondition builds the full WHERE condition for body_v2 JSON paths
func (c *jsonConditionBuilder) buildJSONCondition(operator qbtypes.FilterOperator, value any, sb *sqlbuilder.SelectBuilder) (string, error) {
conditions := []string{}
for _, node := range c.key.JSONPlan {
@@ -40,6 +40,7 @@ func (c *jsonConditionBuilder) buildJSONCondition(operator qbtypes.FilterOperato
}
conditions = append(conditions, condition)
}
return sb.Or(conditions...), nil
}
@@ -288,9 +289,9 @@ func (c *jsonConditionBuilder) applyOperator(sb *sqlbuilder.SelectBuilder, field
}
return sb.NotIn(fieldExpr, values...), nil
case qbtypes.FilterOperatorExists:
return fmt.Sprintf("%s IS NOT NULL", fieldExpr), nil
return sb.IsNotNull(fieldExpr), nil
case qbtypes.FilterOperatorNotExists:
return fmt.Sprintf("%s IS NULL", fieldExpr), nil
return sb.IsNull(fieldExpr), nil
// between and not between
case qbtypes.FilterOperatorBetween, qbtypes.FilterOperatorNotBetween:
values, ok := value.([]any)

File diff suppressed because one or more lines are too long

View File

@@ -65,7 +65,7 @@ func (b *logQueryStatementBuilder) Build(
start = querybuilder.ToNanoSecs(start)
end = querybuilder.ToNanoSecs(end)
keySelectors := getKeySelectors(query)
keySelectors, warnings := getKeySelectors(query)
keys, _, err := b.metadataStore.GetKeysMulti(ctx, keySelectors)
if err != nil {
return nil, err
@@ -76,20 +76,29 @@ func (b *logQueryStatementBuilder) Build(
// Create SQL builder
q := sqlbuilder.NewSelectBuilder()
var stmt *qbtypes.Statement
switch requestType {
case qbtypes.RequestTypeRaw, qbtypes.RequestTypeRawStream:
return b.buildListQuery(ctx, q, query, start, end, keys, variables)
stmt, err = b.buildListQuery(ctx, q, query, start, end, keys, variables)
case qbtypes.RequestTypeTimeSeries:
return b.buildTimeSeriesQuery(ctx, q, query, start, end, keys, variables)
stmt, err = b.buildTimeSeriesQuery(ctx, q, query, start, end, keys, variables)
case qbtypes.RequestTypeScalar:
return b.buildScalarQuery(ctx, q, query, start, end, keys, false, variables)
stmt, err = b.buildScalarQuery(ctx, q, query, start, end, keys, false, variables)
default:
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "unsupported request type: %s", requestType)
}
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "unsupported request type: %s", requestType)
if err != nil {
return nil, err
}
stmt.Warnings = append(stmt.Warnings, warnings...)
return stmt, nil
}
func getKeySelectors(query qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]) []*telemetrytypes.FieldKeySelector {
func getKeySelectors(query qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]) ([]*telemetrytypes.FieldKeySelector, []string) {
var keySelectors []*telemetrytypes.FieldKeySelector
var warnings []string
for idx := range query.Aggregations {
aggExpr := query.Aggregations[idx]
@@ -136,7 +145,19 @@ func getKeySelectors(query qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]) []
keySelectors[idx].SelectorMatchType = telemetrytypes.FieldSelectorMatchTypeExact
}
return keySelectors
// When the new JSON body experience is enabled, warn the user if they use the bare
// "body" key in the filter — queries on plain "body" default to body.message:string.
// TODO(Piyush): Setup better for coming FTS support.
if querybuilder.BodyJSONQueryEnabled {
for _, sel := range keySelectors {
if sel.Name == LogsV2BodyColumn {
warnings = append(warnings, bodySearchDefaultWarning)
break
}
}
}
return keySelectors, warnings
}
func (b *logQueryStatementBuilder) adjustKeys(ctx context.Context, keys map[string][]*telemetrytypes.TelemetryFieldKey, query qbtypes.QueryBuilderQuery[qbtypes.LogAggregation], requestType qbtypes.RequestType) qbtypes.QueryBuilderQuery[qbtypes.LogAggregation] {
@@ -203,7 +224,6 @@ func (b *logQueryStatementBuilder) adjustKeys(ctx context.Context, keys map[stri
}
func (b *logQueryStatementBuilder) adjustKey(key *telemetrytypes.TelemetryFieldKey, keys map[string][]*telemetrytypes.TelemetryFieldKey) []string {
// First check if it matches with any intrinsic fields
var intrinsicOrCalculatedField telemetrytypes.TelemetryFieldKey
if _, ok := IntrinsicFields[key.Name]; ok {
@@ -212,7 +232,6 @@ func (b *logQueryStatementBuilder) adjustKey(key *telemetrytypes.TelemetryFieldK
}
return querybuilder.AdjustKey(key, keys, nil)
}
// buildListQuery builds a query for list panel type
@@ -249,11 +268,7 @@ func (b *logQueryStatementBuilder) buildListQuery(
sb.SelectMore(LogsV2SeverityNumberColumn)
sb.SelectMore(LogsV2ScopeNameColumn)
sb.SelectMore(LogsV2ScopeVersionColumn)
sb.SelectMore(LogsV2BodyColumn)
if querybuilder.BodyJSONQueryEnabled {
sb.SelectMore(LogsV2BodyJSONColumn)
sb.SelectMore(LogsV2BodyPromotedColumn)
}
sb.SelectMore(bodyAliasExpression())
sb.SelectMore(LogsV2AttributesStringColumn)
sb.SelectMore(LogsV2AttributesNumberColumn)
sb.SelectMore(LogsV2AttributesBoolColumn)

View File

@@ -5,6 +5,7 @@ import (
"testing"
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
"github.com/SigNoz/signoz/pkg/querybuilder"
"github.com/SigNoz/signoz/pkg/querybuilder/resourcefilter"
@@ -886,3 +887,246 @@ func TestAdjustKey(t *testing.T) {
})
}
}
func TestStmtBuilderBodyField(t *testing.T) {
cases := []struct {
name string
requestType qbtypes.RequestType
query qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]
enableBodyJSONQuery bool
expected qbtypes.Statement
expectedErr error
}{
{
name: "body_exists",
requestType: qbtypes.RequestTypeRaw,
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
Signal: telemetrytypes.SignalLogs,
Filter: &qbtypes.Filter{Expression: "body Exists"},
Limit: 10,
},
enableBodyJSONQuery: true,
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND body_v2.message <> ? AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
Args: []any{uint64(1747945619), uint64(1747983448), "", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
Warnings: []string{bodySearchDefaultWarning},
},
expectedErr: nil,
},
{
name: "body_exists_disabled",
requestType: qbtypes.RequestTypeRaw,
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
Signal: telemetrytypes.SignalLogs,
Filter: &qbtypes.Filter{Expression: "body Exists"},
Limit: 10,
},
enableBodyJSONQuery: false,
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND body <> ? AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
Args: []any{uint64(1747945619), uint64(1747983448), "", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
},
expectedErr: nil,
},
{
name: "body_empty",
requestType: qbtypes.RequestTypeRaw,
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
Signal: telemetrytypes.SignalLogs,
Filter: &qbtypes.Filter{Expression: "body == ''"},
Limit: 10,
},
enableBodyJSONQuery: true,
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND body_v2.message = ? AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
Args: []any{uint64(1747945619), uint64(1747983448), "", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
Warnings: []string{bodySearchDefaultWarning},
},
expectedErr: nil,
},
{
name: "body_empty_disabled",
requestType: qbtypes.RequestTypeRaw,
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
Signal: telemetrytypes.SignalLogs,
Filter: &qbtypes.Filter{Expression: "body == ''"},
Limit: 10,
},
enableBodyJSONQuery: false,
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND body = ? AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
Args: []any{uint64(1747945619), uint64(1747983448), "", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
},
expectedErr: nil,
},
{
name: "body_contains",
requestType: qbtypes.RequestTypeRaw,
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
Signal: telemetrytypes.SignalLogs,
Filter: &qbtypes.Filter{Expression: "body CONTAINS 'error'"},
Limit: 10,
},
enableBodyJSONQuery: true,
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND LOWER(body_v2.message) LIKE LOWER(?) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
Args: []any{uint64(1747945619), uint64(1747983448), "%error%", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
Warnings: []string{bodySearchDefaultWarning},
},
expectedErr: nil,
},
{
name: "body_contains_disabled",
requestType: qbtypes.RequestTypeRaw,
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
Signal: telemetrytypes.SignalLogs,
Filter: &qbtypes.Filter{Expression: "body CONTAINS 'error'"},
Limit: 10,
},
enableBodyJSONQuery: false,
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND LOWER(body) LIKE LOWER(?) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
Args: []any{uint64(1747945619), uint64(1747983448), "%error%", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
},
expectedErr: nil,
},
}
fm := NewFieldMapper()
cb := NewConditionBuilder(fm)
enable, disable := jsonQueryTestUtil(t)
defer disable()
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
if c.enableBodyJSONQuery {
enable()
} else {
disable()
}
// build the key map after enabling/disabling body JSON query
mockMetadataStore := telemetrytypestest.NewMockMetadataStore()
for _, field := range IntrinsicFields {
f := field
mockMetadataStore.KeysMap[field.Name] = append(mockMetadataStore.KeysMap[field.Name], &f)
}
aggExprRewriter := querybuilder.NewAggExprRewriter(instrumentationtest.New().ToProviderSettings(), nil, fm, cb, nil)
resourceFilterStmtBuilder := resourceFilterStmtBuilder()
statementBuilder := NewLogQueryStatementBuilder(
instrumentationtest.New().ToProviderSettings(),
mockMetadataStore,
fm,
cb,
resourceFilterStmtBuilder,
aggExprRewriter,
DefaultFullTextColumn,
GetBodyJSONKey,
)
q, err := statementBuilder.Build(context.Background(), 1747947419000, 1747983448000, c.requestType, c.query, nil)
if c.expectedErr != nil {
require.Error(t, err)
require.Contains(t, err.Error(), c.expectedErr.Error())
} else {
if err != nil {
_, _, _, _, _, add := errors.Unwrapb(err)
t.Logf("error additionals: %v", add)
}
require.NoError(t, err)
require.Equal(t, c.expected.Query, q.Query)
require.Equal(t, c.expected.Args, q.Args)
require.Equal(t, c.expected.Warnings, q.Warnings)
}
})
}
}
func TestStmtBuilderBodyFullTextSearch(t *testing.T) {
cases := []struct {
name string
requestType qbtypes.RequestType
query qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]
enableBodyJSONQuery bool
expected qbtypes.Statement
expectedErr error
}{
{
name: "body_contains",
requestType: qbtypes.RequestTypeRaw,
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
Signal: telemetrytypes.SignalLogs,
Filter: &qbtypes.Filter{Expression: "'error'"},
Limit: 10,
},
enableBodyJSONQuery: true,
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND match(LOWER(body_v2.message), LOWER(?)) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
Args: []any{uint64(1747945619), uint64(1747983448), "error", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
},
expectedErr: nil,
},
{
name: "body_contains_disabled",
requestType: qbtypes.RequestTypeRaw,
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
Signal: telemetrytypes.SignalLogs,
Filter: &qbtypes.Filter{Expression: "'error'"},
Limit: 10,
},
enableBodyJSONQuery: false,
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND match(LOWER(body), LOWER(?)) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
Args: []any{uint64(1747945619), uint64(1747983448), "error", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
},
expectedErr: nil,
},
}
fm := NewFieldMapper()
cb := NewConditionBuilder(fm)
enable, disable := jsonQueryTestUtil(t)
defer disable()
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
if c.enableBodyJSONQuery {
enable()
} else {
disable()
}
// build the key map after enabling/disabling body JSON query
mockMetadataStore := telemetrytypestest.NewMockMetadataStore()
for _, field := range IntrinsicFields {
f := field
mockMetadataStore.KeysMap[field.Name] = append(mockMetadataStore.KeysMap[field.Name], &f)
}
aggExprRewriter := querybuilder.NewAggExprRewriter(instrumentationtest.New().ToProviderSettings(), nil, fm, cb, nil)
resourceFilterStmtBuilder := resourceFilterStmtBuilder()
statementBuilder := NewLogQueryStatementBuilder(
instrumentationtest.New().ToProviderSettings(),
mockMetadataStore,
fm,
cb,
resourceFilterStmtBuilder,
aggExprRewriter,
DefaultFullTextColumn,
GetBodyJSONKey,
)
q, err := statementBuilder.Build(context.Background(), 1747947419000, 1747983448000, c.requestType, c.query, nil)
if c.expectedErr != nil {
require.Error(t, err)
require.Contains(t, err.Error(), c.expectedErr.Error())
} else {
if err != nil {
_, _, _, _, _, add := errors.Unwrapb(err)
t.Logf("error additionals: %v", add)
}
require.NoError(t, err)
require.Equal(t, c.expected.Query, q.Query)
require.Equal(t, c.expected.Args, q.Args)
require.Equal(t, c.expected.Warnings, q.Warnings)
}
})
}
}

View File

@@ -27,13 +27,6 @@ func buildCompleteFieldKeyMap() map[string][]*telemetrytypes.TelemetryFieldKey {
FieldDataType: telemetrytypes.FieldDataTypeString,
},
},
"body": {
{
Name: "body",
FieldContext: telemetrytypes.FieldContextLog,
FieldDataType: telemetrytypes.FieldDataTypeString,
},
},
"http.status_code": {
{
Name: "http.status_code",
@@ -938,6 +931,13 @@ func buildCompleteFieldKeyMap() map[string][]*telemetrytypes.TelemetryFieldKey {
Materialized: true,
},
},
"body": {
{
Name: "body",
FieldContext: telemetrytypes.FieldContextLog,
FieldDataType: telemetrytypes.FieldDataTypeString,
},
},
}
for _, keys := range keysMap {
@@ -945,6 +945,7 @@ func buildCompleteFieldKeyMap() map[string][]*telemetrytypes.TelemetryFieldKey {
key.Signal = telemetrytypes.SignalLogs
}
}
return keysMap
}

View File

@@ -54,6 +54,7 @@ func (t *telemetryMetaStore) fetchBodyJSONPaths(ctx context.Context,
instrumentationtypes.CodeNamespace: "metadata",
instrumentationtypes.CodeFunctionName: "fetchBodyJSONPaths",
})
query, args, limit := buildGetBodyJSONPathsQuery(fieldKeySelectors)
rows, err := t.telemetrystore.ClickhouseDB().Query(ctx, query, args...)
if err != nil {
@@ -184,7 +185,6 @@ func buildGetBodyJSONPathsQuery(fieldKeySelectors []*telemetrytypes.FieldKeySele
limit += fieldKeySelector.Limit
}
sb.Where(sb.Or(orClauses...))
// Group by path to get unique paths with aggregated types
sb.GroupBy("path")
@@ -319,7 +319,7 @@ func (t *telemetryMetaStore) ListJSONValues(ctx context.Context, path string, li
if promoted {
path = telemetrylogs.BodyPromotedColumnPrefix + path
} else {
path = telemetrylogs.BodyJSONColumnPrefix + path
path = telemetrylogs.BodyV2ColumnPrefix + path
}
from := fmt.Sprintf("%s.%s", telemetrylogs.DBName, telemetrylogs.LogsV2TableName)
@@ -522,7 +522,7 @@ func (t *telemetryMetaStore) GetPromotedPaths(ctx context.Context, paths ...stri
// TODO(Piyush): Remove this function
func CleanPathPrefixes(path string) string {
path = strings.TrimPrefix(path, telemetrytypes.BodyJSONStringSearchPrefix)
path = strings.TrimPrefix(path, telemetrylogs.BodyJSONColumnPrefix)
path = strings.TrimPrefix(path, telemetrylogs.BodyV2ColumnPrefix)
path = strings.TrimPrefix(path, telemetrylogs.BodyPromotedColumnPrefix)
return path
}

View File

@@ -102,7 +102,7 @@ func NewTelemetryMetaStore(
jsonColumnMetadata: map[telemetrytypes.Signal]map[telemetrytypes.FieldContext]telemetrytypes.JSONColumnMetadata{
telemetrytypes.SignalLogs: {
telemetrytypes.FieldContextBody: telemetrytypes.JSONColumnMetadata{
BaseColumn: telemetrylogs.LogsV2BodyJSONColumn,
BaseColumn: telemetrylogs.LogsV2BodyV2Column,
PromotedColumn: telemetrylogs.LogsV2BodyPromotedColumn,
},
},

View File

@@ -40,7 +40,7 @@ type JSONAccessNode struct {
// Node information
Name string
IsTerminal bool
isRoot bool // marked true for only body_json and body_json_promoted
isRoot bool // marked true for only body_v2 and body_promoted
// Precomputed type information (single source of truth)
AvailableTypes []JSONDataType

View File

@@ -1,12 +1,19 @@
package telemetrytypes
import (
"fmt"
"testing"
otelconstants "github.com/SigNoz/signoz-otel-collector/constants"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
)
const (
bodyV2Column = otelconstants.BodyV2Column
bodyPromotedColumn = otelconstants.BodyPromotedColumn
)
// ============================================================================
// Helper Functions for Test Data Creation
// ============================================================================
@@ -109,8 +116,8 @@ func TestNode_Alias(t *testing.T) {
}{
{
name: "Root node returns name as-is",
node: NewRootJSONAccessNode("body_json", 32, 0),
expected: "body_json",
node: NewRootJSONAccessNode(bodyV2Column, 32, 0),
expected: bodyV2Column,
},
{
name: "Node without parent returns backticked name",
@@ -124,9 +131,9 @@ func TestNode_Alias(t *testing.T) {
name: "Node with root parent uses dot separator",
node: &JSONAccessNode{
Name: "age",
Parent: NewRootJSONAccessNode("body_json", 32, 0),
Parent: NewRootJSONAccessNode(bodyV2Column, 32, 0),
},
expected: "`" + "body_json" + ".age`",
expected: "`" + bodyV2Column + ".age`",
},
{
name: "Node with non-root parent uses array separator",
@@ -134,10 +141,10 @@ func TestNode_Alias(t *testing.T) {
Name: "name",
Parent: &JSONAccessNode{
Name: "education",
Parent: NewRootJSONAccessNode("body_json", 32, 0),
Parent: NewRootJSONAccessNode(bodyV2Column, 32, 0),
},
},
expected: "`" + "body_json" + ".education[].name`",
expected: "`" + bodyV2Column + ".education[].name`",
},
{
name: "Nested array path with multiple levels",
@@ -147,11 +154,11 @@ func TestNode_Alias(t *testing.T) {
Name: "awards",
Parent: &JSONAccessNode{
Name: "education",
Parent: NewRootJSONAccessNode("body_json", 32, 0),
Parent: NewRootJSONAccessNode(bodyV2Column, 32, 0),
},
},
},
expected: "`" + "body_json" + ".education[].awards[].type`",
expected: "`" + bodyV2Column + ".education[].awards[].type`",
},
}
@@ -173,18 +180,18 @@ func TestNode_FieldPath(t *testing.T) {
name: "Simple field path from root",
node: &JSONAccessNode{
Name: "user",
Parent: NewRootJSONAccessNode("body_json", 32, 0),
Parent: NewRootJSONAccessNode(bodyV2Column, 32, 0),
},
// FieldPath() always wraps the field name in backticks
expected: "body_json" + ".`user`",
expected: bodyV2Column + ".`user`",
},
{
name: "Field path with backtick-required key",
node: &JSONAccessNode{
Name: "user-name", // requires backtick
Parent: NewRootJSONAccessNode("body_json", 32, 0),
Parent: NewRootJSONAccessNode(bodyV2Column, 32, 0),
},
expected: "body_json" + ".`user-name`",
expected: bodyV2Column + ".`user-name`",
},
{
name: "Nested field path",
@@ -192,11 +199,11 @@ func TestNode_FieldPath(t *testing.T) {
Name: "age",
Parent: &JSONAccessNode{
Name: "user",
Parent: NewRootJSONAccessNode("body_json", 32, 0),
Parent: NewRootJSONAccessNode(bodyV2Column, 32, 0),
},
},
// FieldPath() always wraps the field name in backticks
expected: "`" + "body_json" + ".user`.`age`",
expected: "`" + bodyV2Column + ".user`.`age`",
},
{
name: "Array element field path",
@@ -204,11 +211,11 @@ func TestNode_FieldPath(t *testing.T) {
Name: "name",
Parent: &JSONAccessNode{
Name: "education",
Parent: NewRootJSONAccessNode("body_json", 32, 0),
Parent: NewRootJSONAccessNode(bodyV2Column, 32, 0),
},
},
// FieldPath() always wraps the field name in backticks
expected: "`" + "body_json" + ".education`.`name`",
expected: "`" + bodyV2Column + ".education`.`name`",
},
}
@@ -236,36 +243,36 @@ func TestPlanJSON_BasicStructure(t *testing.T) {
{
name: "Simple path not promoted",
key: makeKey("user.name", String, false),
expectedYAML: `
expectedYAML: fmt.Sprintf(`
- name: user.name
column: body_json
column: %s
availableTypes:
- String
maxDynamicTypes: 16
isTerminal: true
elemType: String
`,
`, bodyV2Column),
},
{
name: "Simple path promoted",
key: makeKey("user.name", String, true),
expectedYAML: `
expectedYAML: fmt.Sprintf(`
- name: user.name
column: body_json
column: %s
availableTypes:
- String
maxDynamicTypes: 16
isTerminal: true
elemType: String
- name: user.name
column: body_json_promoted
column: %s
availableTypes:
- String
maxDynamicTypes: 16
maxDynamicPaths: 256
isTerminal: true
elemType: String
`,
`, bodyV2Column, bodyPromotedColumn),
},
{
name: "Empty path returns error",
@@ -278,8 +285,8 @@ func TestPlanJSON_BasicStructure(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.key.SetJSONAccessPlan(JSONColumnMetadata{
BaseColumn: "body_json",
PromotedColumn: "body_json_promoted",
BaseColumn: bodyV2Column,
PromotedColumn: bodyPromotedColumn,
}, types)
if tt.expectErr {
require.Error(t, err)
@@ -304,9 +311,9 @@ func TestPlanJSON_ArrayPaths(t *testing.T) {
{
name: "Single array level - JSON branch only",
path: "education[].name",
expectedYAML: `
expectedYAML: fmt.Sprintf(`
- name: education
column: body_json
column: %s
availableTypes:
- Array(JSON)
maxDynamicTypes: 16
@@ -318,14 +325,14 @@ func TestPlanJSON_ArrayPaths(t *testing.T) {
maxDynamicTypes: 8
isTerminal: true
elemType: String
`,
`, bodyV2Column),
},
{
name: "Single array level - both JSON and Dynamic branches",
path: "education[].awards[].type",
expectedYAML: `
expectedYAML: fmt.Sprintf(`
- name: education
column: body_json
column: %s
availableTypes:
- Array(JSON)
maxDynamicTypes: 16
@@ -352,14 +359,14 @@ func TestPlanJSON_ArrayPaths(t *testing.T) {
maxDynamicPaths: 256
isTerminal: true
elemType: String
`,
`, bodyV2Column),
},
{
name: "Deeply nested array path",
path: "interests[].entities[].reviews[].entries[].metadata[].positions[].name",
expectedYAML: `
expectedYAML: fmt.Sprintf(`
- name: interests
column: body_json
column: %s
availableTypes:
- Array(JSON)
maxDynamicTypes: 16
@@ -399,14 +406,14 @@ func TestPlanJSON_ArrayPaths(t *testing.T) {
- String
isTerminal: true
elemType: String
`,
`, bodyV2Column),
},
{
name: "ArrayAnyIndex replacement [*] to []",
path: "education[*].name",
expectedYAML: `
expectedYAML: fmt.Sprintf(`
- name: education
column: body_json
column: %s
availableTypes:
- Array(JSON)
maxDynamicTypes: 16
@@ -418,7 +425,7 @@ func TestPlanJSON_ArrayPaths(t *testing.T) {
maxDynamicTypes: 8
isTerminal: true
elemType: String
`,
`, bodyV2Column),
},
}
@@ -426,8 +433,8 @@ func TestPlanJSON_ArrayPaths(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
key := makeKey(tt.path, String, false)
err := key.SetJSONAccessPlan(JSONColumnMetadata{
BaseColumn: "body_json",
PromotedColumn: "body_json_promoted",
BaseColumn: bodyV2Column,
PromotedColumn: bodyPromotedColumn,
}, types)
require.NoError(t, err)
require.NotNil(t, key.JSONPlan)
@@ -445,15 +452,15 @@ func TestPlanJSON_PromotedVsNonPromoted(t *testing.T) {
t.Run("Non-promoted plan", func(t *testing.T) {
key := makeKey(path, String, false)
err := key.SetJSONAccessPlan(JSONColumnMetadata{
BaseColumn: "body_json",
PromotedColumn: "body_json_promoted",
BaseColumn: bodyV2Column,
PromotedColumn: bodyPromotedColumn,
}, types)
require.NoError(t, err)
require.Len(t, key.JSONPlan, 1)
expectedYAML := `
expectedYAML := fmt.Sprintf(`
- name: education
column: body_json
column: %s
availableTypes:
- Array(JSON)
maxDynamicTypes: 16
@@ -480,7 +487,7 @@ func TestPlanJSON_PromotedVsNonPromoted(t *testing.T) {
maxDynamicPaths: 256
isTerminal: true
elemType: String
`
`, bodyV2Column)
got := plansToYAML(t, key.JSONPlan)
require.YAMLEq(t, expectedYAML, got)
})
@@ -488,15 +495,15 @@ func TestPlanJSON_PromotedVsNonPromoted(t *testing.T) {
t.Run("Promoted plan", func(t *testing.T) {
key := makeKey(path, String, true)
err := key.SetJSONAccessPlan(JSONColumnMetadata{
BaseColumn: "body_json",
PromotedColumn: "body_json_promoted",
BaseColumn: bodyV2Column,
PromotedColumn: bodyPromotedColumn,
}, types)
require.NoError(t, err)
require.Len(t, key.JSONPlan, 2)
expectedYAML := `
expectedYAML := fmt.Sprintf(`
- name: education
column: body_json
column: %s
availableTypes:
- Array(JSON)
maxDynamicTypes: 16
@@ -524,7 +531,7 @@ func TestPlanJSON_PromotedVsNonPromoted(t *testing.T) {
isTerminal: true
elemType: String
- name: education
column: body_json_promoted
column: %s
availableTypes:
- Array(JSON)
maxDynamicTypes: 16
@@ -554,7 +561,7 @@ func TestPlanJSON_PromotedVsNonPromoted(t *testing.T) {
maxDynamicPaths: 256
isTerminal: true
elemType: String
`
`, bodyV2Column, bodyPromotedColumn)
got := plansToYAML(t, key.JSONPlan)
require.YAMLEq(t, expectedYAML, got)
})
@@ -575,11 +582,11 @@ func TestPlanJSON_EdgeCases(t *testing.T) {
expectErr: true,
},
{
name: "Very deep nesting - validates progression doesn't go negative",
path: "interests[].entities[].reviews[].entries[].metadata[].positions[].name",
expectedYAML: `
name: "Very deep nesting - validates progression doesn't go negative",
path: "interests[].entities[].reviews[].entries[].metadata[].positions[].name",
expectedYAML: fmt.Sprintf(`
- name: interests
column: body_json
column: %s
availableTypes:
- Array(JSON)
maxDynamicTypes: 16
@@ -619,14 +626,14 @@ func TestPlanJSON_EdgeCases(t *testing.T) {
- String
isTerminal: true
elemType: String
`,
`, bodyV2Column),
},
{
name: "Path with mixed scalar and array types",
path: "education[].type",
expectedYAML: `
name: "Path with mixed scalar and array types",
path: "education[].type",
expectedYAML: fmt.Sprintf(`
- name: education
column: body_json
column: %s
availableTypes:
- Array(JSON)
maxDynamicTypes: 16
@@ -639,20 +646,20 @@ func TestPlanJSON_EdgeCases(t *testing.T) {
maxDynamicTypes: 8
isTerminal: true
elemType: String
`,
`, bodyV2Column),
},
{
name: "Exists with only array types available",
path: "education",
expectedYAML: `
name: "Exists with only array types available",
path: "education",
expectedYAML: fmt.Sprintf(`
- name: education
column: body_json
column: %s
availableTypes:
- Array(JSON)
maxDynamicTypes: 16
isTerminal: true
elemType: Array(JSON)
`,
`, bodyV2Column),
},
}
@@ -668,8 +675,8 @@ func TestPlanJSON_EdgeCases(t *testing.T) {
}
key := makeKey(tt.path, keyType, false)
err := key.SetJSONAccessPlan(JSONColumnMetadata{
BaseColumn: "body_json",
PromotedColumn: "body_json_promoted",
BaseColumn: bodyV2Column,
PromotedColumn: bodyPromotedColumn,
}, types)
if tt.expectErr {
require.Error(t, err)
@@ -687,15 +694,15 @@ func TestPlanJSON_TreeStructure(t *testing.T) {
path := "education[].awards[].participated[].team[].branch"
key := makeKey(path, String, false)
err := key.SetJSONAccessPlan(JSONColumnMetadata{
BaseColumn: "body_json",
PromotedColumn: "body_json_promoted",
BaseColumn: bodyV2Column,
PromotedColumn: bodyPromotedColumn,
}, types)
require.NoError(t, err)
require.Len(t, key.JSONPlan, 1)
expectedYAML := `
expectedYAML := fmt.Sprintf(`
- name: education
column: body_json
column: %s
availableTypes:
- Array(JSON)
maxDynamicTypes: 16
@@ -780,7 +787,7 @@ func TestPlanJSON_TreeStructure(t *testing.T) {
maxDynamicPaths: 64
isTerminal: true
elemType: String
`
`, bodyV2Column)
got := plansToYAML(t, key.JSONPlan)
require.YAMLEq(t, expectedYAML, got)