mirror of
https://github.com/SigNoz/signoz.git
synced 2026-03-20 11:40:27 +00:00
Compare commits
81 Commits
debug_time
...
keep-messa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19f4cde52e | ||
|
|
fcce8b4008 | ||
|
|
c2c2678125 | ||
|
|
73dc095b79 | ||
|
|
b2adbb510b | ||
|
|
4f7ec4c057 | ||
|
|
9c5a5488e2 | ||
|
|
09b6382820 | ||
|
|
9689b847f0 | ||
|
|
15e5938e95 | ||
|
|
c5ef455283 | ||
|
|
9ee23eae06 | ||
|
|
2316b5be83 | ||
|
|
0f36479787 | ||
|
|
95a9a24875 | ||
|
|
937ebc1582 | ||
|
|
dcc8173c79 | ||
|
|
4b4ef5ce58 | ||
|
|
7d1e39037c | ||
|
|
54f104db5f | ||
|
|
5b8d5fbfd3 | ||
|
|
ab0852bbfb | ||
|
|
4c7aba680e | ||
|
|
23c247a1ba | ||
|
|
4777b13ddf | ||
|
|
2d3060bac4 | ||
|
|
9101d51920 | ||
|
|
82b82b0208 | ||
|
|
51bd760d9a | ||
|
|
2a492cc783 | ||
|
|
24afdad36c | ||
|
|
5d20019207 | ||
|
|
1963d5811d | ||
|
|
15cfccad74 | ||
|
|
a0399560e3 | ||
|
|
265e337d5c | ||
|
|
bb8c874755 | ||
|
|
13cbe03d64 | ||
|
|
93621c29b7 | ||
|
|
2c691b5a75 | ||
|
|
cd7e1bb114 | ||
|
|
a1d2ec8b8a | ||
|
|
8bbafb52d5 | ||
|
|
075cfab463 | ||
|
|
86bccaac0c | ||
|
|
de1aac63c0 | ||
|
|
14fe8745b5 | ||
|
|
4013c7ee03 | ||
|
|
0d34360e0b | ||
|
|
d204c89dec | ||
|
|
8dd33c1ab7 | ||
|
|
8e5c3d5ae1 | ||
|
|
d45bb52f33 | ||
|
|
e71818292d | ||
|
|
37557f7f24 | ||
|
|
27ff102660 | ||
|
|
cb2aa4cffd | ||
|
|
58d1d84ec7 | ||
|
|
d8e116a7bc | ||
|
|
6a48bdc37e | ||
|
|
ffb62432f8 | ||
|
|
57c51f070c | ||
|
|
36becfc7a2 | ||
|
|
8e71de09f3 | ||
|
|
56de92de73 | ||
|
|
62b10f8e77 | ||
|
|
20b53d7856 | ||
|
|
8f2c506304 | ||
|
|
7b5b9027dd | ||
|
|
b77f97fcb7 | ||
|
|
62942a4162 | ||
|
|
349bbbbf1d | ||
|
|
1966a7a5f6 | ||
|
|
a4eed9ff13 | ||
|
|
24d1ee33b5 | ||
|
|
3402203021 | ||
|
|
e8e4897cc8 | ||
|
|
96fb88aaee | ||
|
|
5a00e6c2cd | ||
|
|
e2500cff7d | ||
|
|
4864c3bc37 |
5
.github/CODEOWNERS
vendored
5
.github/CODEOWNERS
vendored
@@ -127,12 +127,15 @@
|
|||||||
/frontend/src/pages/DashboardsListPage/ @SigNoz/pulse-frontend
|
/frontend/src/pages/DashboardsListPage/ @SigNoz/pulse-frontend
|
||||||
/frontend/src/container/ListOfDashboard/ @SigNoz/pulse-frontend
|
/frontend/src/container/ListOfDashboard/ @SigNoz/pulse-frontend
|
||||||
|
|
||||||
|
# Dashboard Widget Page
|
||||||
|
/frontend/src/pages/DashboardWidget/ @SigNoz/pulse-frontend
|
||||||
|
/frontend/src/container/NewWidget/ @SigNoz/pulse-frontend
|
||||||
|
|
||||||
## Dashboard Page
|
## Dashboard Page
|
||||||
|
|
||||||
/frontend/src/pages/DashboardPage/ @SigNoz/pulse-frontend
|
/frontend/src/pages/DashboardPage/ @SigNoz/pulse-frontend
|
||||||
/frontend/src/container/DashboardContainer/ @SigNoz/pulse-frontend
|
/frontend/src/container/DashboardContainer/ @SigNoz/pulse-frontend
|
||||||
/frontend/src/container/GridCardLayout/ @SigNoz/pulse-frontend
|
/frontend/src/container/GridCardLayout/ @SigNoz/pulse-frontend
|
||||||
/frontend/src/container/NewWidget/ @SigNoz/pulse-frontend
|
|
||||||
|
|
||||||
## Public Dashboard Page
|
## Public Dashboard Page
|
||||||
|
|
||||||
|
|||||||
59
.github/workflows/mergequeueci.yaml
vendored
Normal file
59
.github/workflows/mergequeueci.yaml
vendored
Normal 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."
|
||||||
2021
frontend/public/Images/allInOneLightMode.svg
Normal file
2021
frontend/public/Images/allInOneLightMode.svg
Normal file
File diff suppressed because it is too large
Load Diff
|
After Width: | Height: | Size: 214 KiB |
@@ -302,7 +302,6 @@ function CustomTimePicker({
|
|||||||
): void => {
|
): void => {
|
||||||
event?.preventDefault();
|
event?.preventDefault();
|
||||||
event?.stopPropagation();
|
event?.stopPropagation();
|
||||||
|
|
||||||
// check if the entered time is in the format of 1m, 2h, 3d, 4w
|
// check if the entered time is in the format of 1m, 2h, 3d, 4w
|
||||||
const isTimeDurationShortHandFormat = /^(\d+)([mhdw])$/.test(inputValue);
|
const isTimeDurationShortHandFormat = /^(\d+)([mhdw])$/.test(inputValue);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// ** Helpers
|
// ** Helpers
|
||||||
import { MetrictypesTypeDTO } from 'api/generated/services/sigNoz.schemas';
|
import { MetrictypesTypeDTO } from 'api/generated/services/sigNoz.schemas';
|
||||||
|
import { defaultTraceSelectedColumns } from 'container/OptionsMenu/constants';
|
||||||
import { createIdFromObjectFields } from 'lib/createIdFromObjectFields';
|
import { createIdFromObjectFields } from 'lib/createIdFromObjectFields';
|
||||||
import { createNewBuilderItemName } from 'lib/newQueryBuilder/createNewBuilderItemName';
|
import { createNewBuilderItemName } from 'lib/newQueryBuilder/createNewBuilderItemName';
|
||||||
import { IAttributeValuesResponse } from 'types/api/queryBuilder/getAttributesValues';
|
import { IAttributeValuesResponse } from 'types/api/queryBuilder/getAttributesValues';
|
||||||
@@ -548,3 +549,49 @@ export const DATA_TYPE_VS_ATTRIBUTE_VALUES_KEY: Record<
|
|||||||
[DataTypes.ArrayBool]: 'boolAttributeValues',
|
[DataTypes.ArrayBool]: 'boolAttributeValues',
|
||||||
[DataTypes.EMPTY]: 'stringAttributeValues',
|
[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,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|||||||
@@ -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,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
`;
|
|
||||||
@@ -182,9 +182,7 @@ describe('Dashboard landing page actions header tests', () => {
|
|||||||
(useLocation as jest.Mock).mockReturnValue(mockLocation);
|
(useLocation as jest.Mock).mockReturnValue(mockLocation);
|
||||||
|
|
||||||
const mockContextValue: IDashboardContext = {
|
const mockContextValue: IDashboardContext = {
|
||||||
isDashboardSliderOpen: false,
|
|
||||||
isDashboardLocked: false,
|
isDashboardLocked: false,
|
||||||
handleToggleDashboardSlider: jest.fn(),
|
|
||||||
handleDashboardLockToggle: jest.fn(),
|
handleDashboardLockToggle: jest.fn(),
|
||||||
dashboardResponse: {} as IDashboardContext['dashboardResponse'],
|
dashboardResponse: {} as IDashboardContext['dashboardResponse'],
|
||||||
selectedDashboard: (getDashboardById.data as unknown) as Dashboard,
|
selectedDashboard: (getDashboardById.data as unknown) as Dashboard,
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useAppContext } from 'providers/App/App';
|
import { useAppContext } from 'providers/App/App';
|
||||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||||
|
import { usePanelTypeSelectionModalStore } from 'providers/Dashboard/helpers/panelTypeSelectionModalHelper';
|
||||||
import { sortLayout } from 'providers/Dashboard/util';
|
import { sortLayout } from 'providers/Dashboard/util';
|
||||||
import { DashboardData } from 'types/api/dashboard/getAll';
|
import { DashboardData } from 'types/api/dashboard/getAll';
|
||||||
import { Props } from 'types/api/dashboard/update';
|
import { Props } from 'types/api/dashboard/update';
|
||||||
@@ -48,10 +49,10 @@ import { ComponentTypes } from 'utils/permission';
|
|||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
import DashboardHeader from '../components/DashboardHeader/DashboardHeader';
|
import DashboardHeader from '../components/DashboardHeader/DashboardHeader';
|
||||||
import DashboardGraphSlider from '../ComponentsSlider';
|
|
||||||
import DashboardSettings from '../DashboardSettings';
|
import DashboardSettings from '../DashboardSettings';
|
||||||
import { Base64Icons } from '../DashboardSettings/General/utils';
|
import { Base64Icons } from '../DashboardSettings/General/utils';
|
||||||
import DashboardVariableSelection from '../DashboardVariablesSelection';
|
import DashboardVariableSelection from '../DashboardVariablesSelection';
|
||||||
|
import PanelTypeSelectionModal from '../PanelTypeSelectionModal';
|
||||||
import SettingsDrawer from './SettingsDrawer';
|
import SettingsDrawer from './SettingsDrawer';
|
||||||
import { VariablesSettingsTab } from './types';
|
import { VariablesSettingsTab } from './types';
|
||||||
import {
|
import {
|
||||||
@@ -69,6 +70,9 @@ interface DashboardDescriptionProps {
|
|||||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
||||||
const { handle } = props;
|
const { handle } = props;
|
||||||
|
const setIsPanelTypeSelectionModalOpen = usePanelTypeSelectionModalStore(
|
||||||
|
(s) => s.setIsPanelTypeSelectionModalOpen,
|
||||||
|
);
|
||||||
const {
|
const {
|
||||||
selectedDashboard,
|
selectedDashboard,
|
||||||
panelMap,
|
panelMap,
|
||||||
@@ -77,7 +81,6 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
|||||||
setLayouts,
|
setLayouts,
|
||||||
isDashboardLocked,
|
isDashboardLocked,
|
||||||
setSelectedDashboard,
|
setSelectedDashboard,
|
||||||
handleToggleDashboardSlider,
|
|
||||||
handleDashboardLockToggle,
|
handleDashboardLockToggle,
|
||||||
} = useDashboard();
|
} = useDashboard();
|
||||||
|
|
||||||
@@ -145,14 +148,14 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
|||||||
const [addPanelPermission] = useComponentPermission(permissions, userRole);
|
const [addPanelPermission] = useComponentPermission(permissions, userRole);
|
||||||
|
|
||||||
const onEmptyWidgetHandler = useCallback(() => {
|
const onEmptyWidgetHandler = useCallback(() => {
|
||||||
handleToggleDashboardSlider(true);
|
setIsPanelTypeSelectionModalOpen(true);
|
||||||
logEvent('Dashboard Detail: Add new panel clicked', {
|
logEvent('Dashboard Detail: Add new panel clicked', {
|
||||||
dashboardId: selectedDashboard?.id,
|
dashboardId: selectedDashboard?.id,
|
||||||
dashboardName: selectedDashboard?.data.title,
|
dashboardName: selectedDashboard?.data.title,
|
||||||
numberOfPanels: selectedDashboard?.data.widgets?.length,
|
numberOfPanels: selectedDashboard?.data.widgets?.length,
|
||||||
});
|
});
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [handleToggleDashboardSlider]);
|
}, [setIsPanelTypeSelectionModalOpen]);
|
||||||
|
|
||||||
const handleLockDashboardToggle = (): void => {
|
const handleLockDashboardToggle = (): void => {
|
||||||
setIsDashbordSettingsOpen(false);
|
setIsDashbordSettingsOpen(false);
|
||||||
@@ -521,7 +524,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
|||||||
<DashboardVariableSelection />
|
<DashboardVariableSelection />
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
<DashboardGraphSlider />
|
<PanelTypeSelectionModal />
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
open={isRenameDashboardOpen}
|
open={isRenameDashboardOpen}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
} from 'hooks/dashboard/useDashboardVariables';
|
} from 'hooks/dashboard/useDashboardVariables';
|
||||||
import useVariablesFromUrl from 'hooks/dashboard/useVariablesFromUrl';
|
import useVariablesFromUrl from 'hooks/dashboard/useVariablesFromUrl';
|
||||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||||
import { initializeDefaultVariables } from 'providers/Dashboard/initializeDefaultVariables';
|
|
||||||
import { updateDashboardVariablesStore } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStore';
|
import { updateDashboardVariablesStore } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStore';
|
||||||
import {
|
import {
|
||||||
enqueueDescendantsOfVariable,
|
enqueueDescendantsOfVariable,
|
||||||
@@ -30,7 +29,7 @@ function DashboardVariableSelection(): JSX.Element | null {
|
|||||||
updateLocalStorageDashboardVariables,
|
updateLocalStorageDashboardVariables,
|
||||||
} = useDashboard();
|
} = useDashboard();
|
||||||
|
|
||||||
const { updateUrlVariable, getUrlVariables } = useVariablesFromUrl();
|
const { updateUrlVariable } = useVariablesFromUrl();
|
||||||
|
|
||||||
const { dashboardVariables } = useDashboardVariables();
|
const { dashboardVariables } = useDashboardVariables();
|
||||||
const dashboardId = useDashboardVariablesSelector(
|
const dashboardId = useDashboardVariablesSelector(
|
||||||
@@ -50,15 +49,6 @@ function DashboardVariableSelection(): JSX.Element | null {
|
|||||||
(state) => state.globalTime,
|
(state) => state.globalTime,
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Initialize variables with default values if not in URL
|
|
||||||
initializeDefaultVariables(
|
|
||||||
dashboardVariables,
|
|
||||||
getUrlVariables,
|
|
||||||
updateUrlVariable,
|
|
||||||
);
|
|
||||||
}, [getUrlVariables, updateUrlVariable, dashboardVariables]);
|
|
||||||
|
|
||||||
// Memoize the order key to avoid unnecessary triggers
|
// Memoize the order key to avoid unnecessary triggers
|
||||||
const variableOrderKey = useMemo(() => {
|
const variableOrderKey = useMemo(() => {
|
||||||
const queryVariableOrderKey = dependencyData?.order?.join(',') ?? '';
|
const queryVariableOrderKey = dependencyData?.order?.join(',') ?? '';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
.graph-selection {
|
.panel-type-selection-modal {
|
||||||
.ant-modal-content {
|
.ant-modal-content {
|
||||||
width: 515px;
|
width: 515px;
|
||||||
max-height: 646px;
|
max-height: 646px;
|
||||||
@@ -76,6 +76,11 @@
|
|||||||
content: none;
|
content: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.panel-type-text {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,7 +119,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.lightMode {
|
.lightMode {
|
||||||
.graph-selection {
|
.panel-type-selection-modal {
|
||||||
.ant-modal-content {
|
.ant-modal-content {
|
||||||
border: 1px solid var(--bg-vanilla-300);
|
border: 1px solid var(--bg-vanilla-300);
|
||||||
background: var(--bg-vanilla-100);
|
background: var(--bg-vanilla-100);
|
||||||
@@ -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);
|
||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
Table,
|
Table,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
const Items: ItemsProps[] = [
|
export const PanelTypesWithData: ItemsProps[] = [
|
||||||
{
|
{
|
||||||
name: PANEL_TYPES.TIME_SERIES,
|
name: PANEL_TYPES.TIME_SERIES,
|
||||||
icon: <LineChart size={16} color={Color.BG_ROBIN_400} />,
|
icon: <LineChart size={16} color={Color.BG_ROBIN_400} />,
|
||||||
@@ -52,5 +52,3 @@ export interface ItemsProps {
|
|||||||
icon: JSX.Element;
|
icon: JSX.Element;
|
||||||
display: string;
|
display: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Items;
|
|
||||||
@@ -9,17 +9,18 @@ import DashboardSettings from 'container/DashboardContainer/DashboardSettings';
|
|||||||
import useComponentPermission from 'hooks/useComponentPermission';
|
import useComponentPermission from 'hooks/useComponentPermission';
|
||||||
import { useAppContext } from 'providers/App/App';
|
import { useAppContext } from 'providers/App/App';
|
||||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||||
|
import { usePanelTypeSelectionModalStore } from 'providers/Dashboard/helpers/panelTypeSelectionModalHelper';
|
||||||
import { ROLES, USER_ROLES } from 'types/roles';
|
import { ROLES, USER_ROLES } from 'types/roles';
|
||||||
import { ComponentTypes } from 'utils/permission';
|
import { ComponentTypes } from 'utils/permission';
|
||||||
|
|
||||||
import './DashboardEmptyState.styles.scss';
|
import './DashboardEmptyState.styles.scss';
|
||||||
|
|
||||||
export default function DashboardEmptyState(): JSX.Element {
|
export default function DashboardEmptyState(): JSX.Element {
|
||||||
const {
|
const setIsPanelTypeSelectionModalOpen = usePanelTypeSelectionModalStore(
|
||||||
selectedDashboard,
|
(s) => s.setIsPanelTypeSelectionModalOpen,
|
||||||
isDashboardLocked,
|
);
|
||||||
handleToggleDashboardSlider,
|
|
||||||
} = useDashboard();
|
const { selectedDashboard, isDashboardLocked } = useDashboard();
|
||||||
|
|
||||||
const variablesSettingsTabHandle = useRef<VariablesSettingsTab>(null);
|
const variablesSettingsTabHandle = useRef<VariablesSettingsTab>(null);
|
||||||
const [isSettingsDrawerOpen, setIsSettingsDrawerOpen] = useState<boolean>(
|
const [isSettingsDrawerOpen, setIsSettingsDrawerOpen] = useState<boolean>(
|
||||||
@@ -41,14 +42,14 @@ export default function DashboardEmptyState(): JSX.Element {
|
|||||||
const [addPanelPermission] = useComponentPermission(permissions, userRole);
|
const [addPanelPermission] = useComponentPermission(permissions, userRole);
|
||||||
|
|
||||||
const onEmptyWidgetHandler = useCallback(() => {
|
const onEmptyWidgetHandler = useCallback(() => {
|
||||||
handleToggleDashboardSlider(true);
|
setIsPanelTypeSelectionModalOpen(true);
|
||||||
logEvent('Dashboard Detail: Add new panel clicked', {
|
logEvent('Dashboard Detail: Add new panel clicked', {
|
||||||
dashboardId: selectedDashboard?.id,
|
dashboardId: selectedDashboard?.id,
|
||||||
dashboardName: selectedDashboard?.data.title,
|
dashboardName: selectedDashboard?.data.title,
|
||||||
numberOfPanels: selectedDashboard?.data.widgets?.length,
|
numberOfPanels: selectedDashboard?.data.widgets?.length,
|
||||||
});
|
});
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [handleToggleDashboardSlider]);
|
}, [setIsPanelTypeSelectionModalOpen]);
|
||||||
|
|
||||||
const onConfigureClick = useCallback((): void => {
|
const onConfigureClick = useCallback((): void => {
|
||||||
setIsSettingsDrawerOpen(true);
|
setIsSettingsDrawerOpen(true);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useCallback } from 'react';
|
|||||||
import { Select, Typography } from 'antd';
|
import { Select, Typography } from 'antd';
|
||||||
import { QueryParams } from 'constants/query';
|
import { QueryParams } from 'constants/query';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
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 { handleQueryChange } from 'container/NewWidget/utils';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
@@ -59,7 +59,7 @@ function PanelTypeSelector({
|
|||||||
data-testid="panel-change-select"
|
data-testid="panel-change-select"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
{GraphTypes.map((item) => (
|
{PanelTypesWithData.map((item) => (
|
||||||
<Option key={item.name} value={item.name}>
|
<Option key={item.name} value={item.name}>
|
||||||
<div className="view-panel-select-option">
|
<div className="view-panel-select-option">
|
||||||
<div className="icon">{item.icon}</div>
|
<div className="icon">{item.icon}</div>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import useComponentPermission from 'hooks/useComponentPermission';
|
|||||||
import { EllipsisIcon, PenLine, Plus, X } from 'lucide-react';
|
import { EllipsisIcon, PenLine, Plus, X } from 'lucide-react';
|
||||||
import { useAppContext } from 'providers/App/App';
|
import { useAppContext } from 'providers/App/App';
|
||||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||||
|
import { usePanelTypeSelectionModalStore } from 'providers/Dashboard/helpers/panelTypeSelectionModalHelper';
|
||||||
import { setSelectedRowWidgetId } from 'providers/Dashboard/helpers/selectedRowWidgetIdHelper';
|
import { setSelectedRowWidgetId } from 'providers/Dashboard/helpers/selectedRowWidgetIdHelper';
|
||||||
import { ROLES, USER_ROLES } from 'types/roles';
|
import { ROLES, USER_ROLES } from 'types/roles';
|
||||||
import { ComponentTypes } from 'utils/permission';
|
import { ComponentTypes } from 'utils/permission';
|
||||||
@@ -34,11 +35,11 @@ export function WidgetRowHeader(props: WidgetRowHeaderProps): JSX.Element {
|
|||||||
} = props;
|
} = props;
|
||||||
const [isRowSettingsOpen, setIsRowSettingsOpen] = useState<boolean>(false);
|
const [isRowSettingsOpen, setIsRowSettingsOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
const {
|
const setIsPanelTypeSelectionModalOpen = usePanelTypeSelectionModalStore(
|
||||||
handleToggleDashboardSlider,
|
(s) => s.setIsPanelTypeSelectionModalOpen,
|
||||||
selectedDashboard,
|
);
|
||||||
isDashboardLocked,
|
|
||||||
} = useDashboard();
|
const { selectedDashboard, isDashboardLocked } = useDashboard();
|
||||||
|
|
||||||
const permissions: ComponentTypes[] = ['add_panel'];
|
const permissions: ComponentTypes[] = ['add_panel'];
|
||||||
const { user } = useAppContext();
|
const { user } = useAppContext();
|
||||||
@@ -87,7 +88,7 @@ export function WidgetRowHeader(props: WidgetRowHeaderProps): JSX.Element {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setSelectedRowWidgetId(selectedDashboard.id, id);
|
setSelectedRowWidgetId(selectedDashboard.id, id);
|
||||||
handleToggleDashboardSlider(true);
|
setIsPanelTypeSelectionModalOpen(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
New Panel
|
New Panel
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import ROUTES from 'constants/routes';
|
|||||||
import { getMetricsListQuery } from 'container/MetricsExplorer/Summary/utils';
|
import { getMetricsListQuery } from 'container/MetricsExplorer/Summary/utils';
|
||||||
import { useGetMetricsList } from 'hooks/metricsExplorer/useGetMetricsList';
|
import { useGetMetricsList } from 'hooks/metricsExplorer/useGetMetricsList';
|
||||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||||
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import cloneDeep from 'lodash-es/cloneDeep';
|
import cloneDeep from 'lodash-es/cloneDeep';
|
||||||
import { AnimatePresence } from 'motion/react';
|
import { AnimatePresence } from 'motion/react';
|
||||||
@@ -43,6 +44,7 @@ const homeInterval = 30 * 60 * 1000;
|
|||||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
export default function Home(): JSX.Element {
|
export default function Home(): JSX.Element {
|
||||||
const { user } = useAppContext();
|
const { user } = useAppContext();
|
||||||
|
const isDarkMode = useIsDarkMode();
|
||||||
|
|
||||||
const [startTime, setStartTime] = useState<number | null>(null);
|
const [startTime, setStartTime] = useState<number | null>(null);
|
||||||
const [endTime, setEndTime] = 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">
|
<div className="checklist-img-container">
|
||||||
<img
|
<img
|
||||||
src="/Images/allInOne.svg"
|
src={
|
||||||
|
isDarkMode
|
||||||
|
? '/Images/allInOne.svg'
|
||||||
|
: '/Images/allInOneLightMode.svg'
|
||||||
|
}
|
||||||
alt="checklist-img"
|
alt="checklist-img"
|
||||||
className="checklist-img"
|
className="checklist-img"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
.column-unit-selector {
|
.column-unit-selector {
|
||||||
margin-top: 16px;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
.heading {
|
.heading {
|
||||||
color: var(--bg-vanilla-400);
|
color: var(--bg-vanilla-400);
|
||||||
@@ -30,6 +32,11 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
&-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.lightMode {
|
.lightMode {
|
||||||
|
|||||||
@@ -72,22 +72,24 @@ export function ColumnUnitSelector(
|
|||||||
return (
|
return (
|
||||||
<section className="column-unit-selector">
|
<section className="column-unit-selector">
|
||||||
<Typography.Text className="heading">Column Units</Typography.Text>
|
<Typography.Text className="heading">Column Units</Typography.Text>
|
||||||
{aggregationQueries.map(({ value, label }) => {
|
<div className="column-unit-selector-content">
|
||||||
const baseQueryName = value.split('.')[0];
|
{aggregationQueries.map(({ value, label }) => {
|
||||||
return (
|
const baseQueryName = value.split('.')[0];
|
||||||
<YAxisUnitSelectorV2
|
return (
|
||||||
value={columnUnits[value] || ''}
|
<YAxisUnitSelectorV2
|
||||||
onSelect={(unitValue: string): void =>
|
value={columnUnits[value] || ''}
|
||||||
handleColumnUnitSelect(value, unitValue)
|
onSelect={(unitValue: string): void =>
|
||||||
}
|
handleColumnUnitSelect(value, unitValue)
|
||||||
fieldLabel={label}
|
}
|
||||||
key={value}
|
fieldLabel={label}
|
||||||
selectedQueryName={baseQueryName}
|
key={value}
|
||||||
// Update the column unit value automatically only in create mode
|
selectedQueryName={baseQueryName}
|
||||||
shouldUpdateYAxisUnit={isNewDashboard}
|
// Update the column unit value automatically only in create mode
|
||||||
/>
|
shouldUpdateYAxisUnit={isNewDashboard}
|
||||||
);
|
/>
|
||||||
})}
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
// Check that the add button is present
|
||||||
expect(
|
expect(
|
||||||
screen.getByRole('button', { name: /context link/i }),
|
screen.getByRole('button', { name: /context link/i }),
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
verticalListSortingStrategy,
|
verticalListSortingStrategy,
|
||||||
} from '@dnd-kit/sortable';
|
} from '@dnd-kit/sortable';
|
||||||
import { CSS } from '@dnd-kit/utilities';
|
import { CSS } from '@dnd-kit/utilities';
|
||||||
import { Button, Modal, Typography } from 'antd';
|
import { Button, Modal } from 'antd';
|
||||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||||
import { GripVertical, Pencil, Plus, Trash2 } from 'lucide-react';
|
import { GripVertical, Pencil, Plus, Trash2 } from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
@@ -134,11 +134,16 @@ function ContextLinks({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="context-links-container">
|
<div className="context-links-container">
|
||||||
<Typography.Text className="context-links-text">
|
|
||||||
Context Links
|
|
||||||
</Typography.Text>
|
|
||||||
|
|
||||||
<div className="context-links-list">
|
<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>
|
<OverlayScrollbar>
|
||||||
<DndContext
|
<DndContext
|
||||||
sensors={sensors}
|
sensors={sensors}
|
||||||
@@ -160,16 +165,6 @@ function ContextLinks({
|
|||||||
</SortableContext>
|
</SortableContext>
|
||||||
</DndContext>
|
</DndContext>
|
||||||
</OverlayScrollbar>
|
</OverlayScrollbar>
|
||||||
|
|
||||||
{/* button to add context link */}
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
className="add-context-link-button"
|
|
||||||
icon={<Plus size={12} />}
|
|
||||||
onClick={handleAddContextLink}
|
|
||||||
>
|
|
||||||
Context Link
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
margin: 12px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.context-links-text {
|
.context-links-text {
|
||||||
@@ -110,10 +109,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.add-context-link-button {
|
.add-context-link-button {
|
||||||
display: flex;
|
width: 100%;
|
||||||
align-items: center;
|
|
||||||
margin: auto;
|
|
||||||
width: fit-content;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.lightMode {
|
.lightMode {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
.right-container {
|
.right-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
padding-bottom: 48px;
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -24,14 +25,14 @@
|
|||||||
letter-spacing: -0.07px;
|
letter-spacing: -0.07px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.control-container {
|
||||||
.name-description {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
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;
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name-description {
|
||||||
|
padding: 0 0 4px 0;
|
||||||
|
|
||||||
.typography {
|
.typography {
|
||||||
color: var(--bg-vanilla-400);
|
color: var(--bg-vanilla-400);
|
||||||
@@ -88,9 +89,6 @@
|
|||||||
.panel-config {
|
.panel-config {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 12px 12px 16px 12px;
|
|
||||||
gap: 8px;
|
|
||||||
border-bottom: 1px solid var(--bg-slate-500);
|
|
||||||
|
|
||||||
.typography {
|
.typography {
|
||||||
color: var(--bg-vanilla-400);
|
color: var(--bg-vanilla-400);
|
||||||
@@ -104,6 +102,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.panel-type-select {
|
.panel-type-select {
|
||||||
|
width: 100%;
|
||||||
.ant-select-selector {
|
.ant-select-selector {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
@@ -137,7 +136,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.fill-gaps {
|
.fill-gaps {
|
||||||
margin-top: 16px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -156,31 +154,24 @@
|
|||||||
letter-spacing: 0.52px;
|
letter-spacing: 0.52px;
|
||||||
text-transform: uppercase;
|
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,
|
.log-scale,
|
||||||
.decimal-precision-selector {
|
.decimal-precision-selector,
|
||||||
margin-top: 16px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.legend-position {
|
.legend-position {
|
||||||
margin-top: 16px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.legend-colors {
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-time-text {
|
.panel-time-text {
|
||||||
margin-top: 16px;
|
|
||||||
color: var(--bg-vanilla-400);
|
color: var(--bg-vanilla-400);
|
||||||
font-family: 'Space Mono';
|
font-family: 'Space Mono';
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
@@ -193,7 +184,6 @@
|
|||||||
|
|
||||||
.y-axis-unit-selector,
|
.y-axis-unit-selector,
|
||||||
.y-axis-unit-selector-v2 {
|
.y-axis-unit-selector-v2 {
|
||||||
margin-top: 16px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
@@ -278,11 +268,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.stack-chart {
|
.stack-chart {
|
||||||
margin-top: 16px;
|
flex-direction: row;
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 8px;
|
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
color: var(--bg-vanilla-400);
|
color: var(--bg-vanilla-400);
|
||||||
font-family: 'Space Mono';
|
font-family: 'Space Mono';
|
||||||
@@ -296,11 +283,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.bucket-config {
|
.bucket-config {
|
||||||
margin-top: 16px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
color: var(--bg-vanilla-400);
|
color: var(--bg-vanilla-400);
|
||||||
font-family: 'Space Mono';
|
font-family: 'Space Mono';
|
||||||
@@ -352,16 +334,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.context-links {
|
|
||||||
border-bottom: 1px solid var(--bg-slate-500);
|
|
||||||
}
|
|
||||||
|
|
||||||
.alerts {
|
.alerts {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 12px;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
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;
|
cursor: pointer;
|
||||||
|
|
||||||
.left-section {
|
.left-section {
|
||||||
@@ -387,6 +366,16 @@
|
|||||||
color: var(--bg-vanilla-400);
|
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 {
|
.select-option {
|
||||||
@@ -418,9 +407,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.name-description {
|
.name-description {
|
||||||
border-top: 1px solid var(--bg-vanilla-300);
|
|
||||||
border-bottom: 1px solid var(--bg-vanilla-300);
|
|
||||||
|
|
||||||
.typography {
|
.typography {
|
||||||
color: var(--bg-ink-400);
|
color: var(--bg-ink-400);
|
||||||
}
|
}
|
||||||
@@ -441,8 +427,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.panel-config {
|
.panel-config {
|
||||||
border-bottom: 1px solid var(--bg-vanilla-300);
|
|
||||||
|
|
||||||
.typography {
|
.typography {
|
||||||
color: var(--bg-ink-400);
|
color: var(--bg-ink-400);
|
||||||
}
|
}
|
||||||
@@ -478,6 +462,9 @@
|
|||||||
.fill-gaps-text {
|
.fill-gaps-text {
|
||||||
color: var(--bg-ink-400);
|
color: var(--bg-ink-400);
|
||||||
}
|
}
|
||||||
|
.fill-gaps-text-description {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.bucket-config {
|
.bucket-config {
|
||||||
@@ -530,7 +517,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.alerts {
|
.alerts {
|
||||||
border-bottom: 1px solid var(--bg-vanilla-300);
|
border-top: 1px solid var(--bg-vanilla-300);
|
||||||
|
|
||||||
.left-section {
|
.left-section {
|
||||||
.bell-icon {
|
.bell-icon {
|
||||||
@@ -549,6 +536,10 @@
|
|||||||
.context-links {
|
.context-links {
|
||||||
border-bottom: 1px solid var(--bg-vanilla-300);
|
border-bottom: 1px solid var(--bg-vanilla-300);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.thresholds-section {
|
||||||
|
border-top: 1px solid var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-option {
|
.select-option {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
.threshold-selector-container {
|
.threshold-selector-container {
|
||||||
padding: 12px;
|
|
||||||
padding-bottom: 80px;
|
padding-bottom: 80px;
|
||||||
|
|
||||||
.threshold-select {
|
.threshold-select {
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { DndProvider } from 'react-dnd';
|
import { DndProvider } from 'react-dnd';
|
||||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||||
import { Typography } from 'antd';
|
import { Button } from 'antd';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import { useGetQueryLabels } from 'hooks/useGetQueryLabels';
|
import { useGetQueryLabels } from 'hooks/useGetQueryLabels';
|
||||||
import { Antenna, Plus } from 'lucide-react';
|
import { Plus } from 'lucide-react';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
import Threshold from './Threshold';
|
import Threshold from './Threshold';
|
||||||
@@ -68,11 +68,14 @@ function ThresholdSelector({
|
|||||||
<DndProvider backend={HTML5Backend}>
|
<DndProvider backend={HTML5Backend}>
|
||||||
<div className="threshold-selector-container">
|
<div className="threshold-selector-container">
|
||||||
<div className="threshold-select" onClick={addThresholdHandler}>
|
<div className="threshold-select" onClick={addThresholdHandler}>
|
||||||
<div className="left-section">
|
<Button
|
||||||
<Antenna size={14} className="icon" />
|
type="default"
|
||||||
<Typography.Text className="text">Thresholds</Typography.Text>
|
icon={<Plus size={14} />}
|
||||||
</div>
|
style={{ width: '100%' }}
|
||||||
<Plus size={14} onClick={addThresholdHandler} className="icon" />
|
onClick={addThresholdHandler}
|
||||||
|
>
|
||||||
|
Add Threshold
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{thresholds.map((threshold, idx) => (
|
{thresholds.map((threshold, idx) => (
|
||||||
<Threshold
|
<Threshold
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -14,23 +14,30 @@ import {
|
|||||||
Input,
|
Input,
|
||||||
InputNumber,
|
InputNumber,
|
||||||
Select,
|
Select,
|
||||||
Space,
|
|
||||||
Switch,
|
Switch,
|
||||||
Typography,
|
Typography,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import { PrecisionOption, PrecisionOptionsEnum } from 'components/Graph/types';
|
import { PrecisionOption, PrecisionOptionsEnum } from 'components/Graph/types';
|
||||||
import TimePreference from 'components/TimePreferenceDropDown';
|
import TimePreference from 'components/TimePreferenceDropDown';
|
||||||
import { PANEL_TYPES, PanelDisplay } from 'constants/queryBuilder';
|
import { PANEL_TYPES, PanelDisplay } from 'constants/queryBuilder';
|
||||||
import GraphTypes, {
|
import {
|
||||||
ItemsProps,
|
ItemsProps,
|
||||||
} from 'container/DashboardContainer/ComponentsSlider/menuItems';
|
PanelTypesWithData,
|
||||||
|
} from 'container/DashboardContainer/PanelTypeSelectionModal/menuItems';
|
||||||
import { useDashboardVariables } from 'hooks/dashboard/useDashboardVariables';
|
import { useDashboardVariables } from 'hooks/dashboard/useDashboardVariables';
|
||||||
import useCreateAlerts from 'hooks/queryBuilder/useCreateAlerts';
|
import useCreateAlerts from 'hooks/queryBuilder/useCreateAlerts';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import {
|
import {
|
||||||
|
Antenna,
|
||||||
|
Axis3D,
|
||||||
ConciergeBell,
|
ConciergeBell,
|
||||||
|
Layers,
|
||||||
|
LayoutDashboard,
|
||||||
LineChart,
|
LineChart,
|
||||||
|
Link,
|
||||||
|
Pencil,
|
||||||
Plus,
|
Plus,
|
||||||
|
SlidersHorizontal,
|
||||||
Spline,
|
Spline,
|
||||||
SquareArrowOutUpRight,
|
SquareArrowOutUpRight,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
@@ -46,6 +53,7 @@ import { DataSource } from 'types/common/queryBuilder';
|
|||||||
import { popupContainer } from 'utils/selectPopupContainer';
|
import { popupContainer } from 'utils/selectPopupContainer';
|
||||||
|
|
||||||
import { ColumnUnitSelector } from './ColumnUnitSelector/ColumnUnitSelector';
|
import { ColumnUnitSelector } from './ColumnUnitSelector/ColumnUnitSelector';
|
||||||
|
import SettingsSection from './components/SettingsSection/SettingsSection';
|
||||||
import {
|
import {
|
||||||
panelTypeVsBucketConfig,
|
panelTypeVsBucketConfig,
|
||||||
panelTypeVsColumnUnitPreferences,
|
panelTypeVsColumnUnitPreferences,
|
||||||
@@ -143,7 +151,7 @@ function RightContainer({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const selectedGraphType =
|
const selectedGraphType =
|
||||||
GraphTypes.find((e) => e.name === selectedGraph)?.display || '';
|
PanelTypesWithData.find((e) => e.name === selectedGraph)?.display || '';
|
||||||
|
|
||||||
const onCreateAlertsHandler = useCreateAlerts(selectedWidget, 'panelView');
|
const onCreateAlertsHandler = useCreateAlerts(selectedWidget, 'panelView');
|
||||||
|
|
||||||
@@ -169,7 +177,7 @@ function RightContainer({
|
|||||||
|
|
||||||
const { currentQuery } = useQueryBuilder();
|
const { currentQuery } = useQueryBuilder();
|
||||||
|
|
||||||
const [graphTypes, setGraphTypes] = useState<ItemsProps[]>(GraphTypes);
|
const [graphTypes, setGraphTypes] = useState<ItemsProps[]>(PanelTypesWithData);
|
||||||
|
|
||||||
const dashboardVariableOptions = useMemo<VariableOption[]>(() => {
|
const dashboardVariableOptions = useMemo<VariableOption[]>(() => {
|
||||||
return Object.entries(dashboardVariables).map(([, value]) => ({
|
return Object.entries(dashboardVariables).map(([, value]) => ({
|
||||||
@@ -178,6 +186,21 @@ function RightContainer({
|
|||||||
}));
|
}));
|
||||||
}, [dashboardVariables]);
|
}, [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 => {
|
const updateCursorAndDropdown = (value: string, pos: number): void => {
|
||||||
setCursorPos(pos);
|
setCursorPos(pos);
|
||||||
const lastDollar = value.lastIndexOf('$', pos - 1);
|
const lastDollar = value.lastIndexOf('$', pos - 1);
|
||||||
@@ -193,6 +216,15 @@ function RightContainer({
|
|||||||
}, 0);
|
}, 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 handleInputCursor = (): void => {
|
||||||
const pos = inputRef.current?.input?.selectionStart ?? 0;
|
const pos = inputRef.current?.input?.selectionStart ?? 0;
|
||||||
updateCursorAndDropdown(inputValue, pos);
|
updateCursorAndDropdown(inputValue, pos);
|
||||||
@@ -241,7 +273,7 @@ function RightContainer({
|
|||||||
prev.filter((graph) => graph.name !== PANEL_TYPES.LIST),
|
prev.filter((graph) => graph.name !== PANEL_TYPES.LIST),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
setGraphTypes(GraphTypes);
|
setGraphTypes(PanelTypesWithData);
|
||||||
}
|
}
|
||||||
}, [currentQuery]);
|
}, [currentQuery]);
|
||||||
|
|
||||||
@@ -263,269 +295,297 @@ function RightContainer({
|
|||||||
<div className="right-container">
|
<div className="right-container">
|
||||||
<section className="header">
|
<section className="header">
|
||||||
<div className="purple-dot" />
|
<div className="purple-dot" />
|
||||||
<Typography.Text className="header-text">Panel details</Typography.Text>
|
<Typography.Text className="header-text">Panel Settings</Typography.Text>
|
||||||
</section>
|
</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 && (
|
<SettingsSection title="General" defaultOpen icon={<Pencil size={14} />}>
|
||||||
<Space className="fill-gaps">
|
<section className="name-description control-container">
|
||||||
<Typography className="fill-gaps-text">Fill gaps</Typography>
|
<Typography.Text className="typography">Name</Typography.Text>
|
||||||
<Switch
|
<AutoComplete
|
||||||
checked={isFillSpans}
|
options={dashboardVariableOptions}
|
||||||
size="small"
|
value={inputValue}
|
||||||
onChange={(checked): void => setIsFillSpans(checked)}
|
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>
|
</AutoComplete>
|
||||||
)}
|
<Typography.Text className="typography">Description</Typography.Text>
|
||||||
|
<TextArea
|
||||||
{allowPanelTimePreference && (
|
placeholder="Enter the panel description here..."
|
||||||
<>
|
bordered
|
||||||
<Typography.Text className="panel-time-text">
|
allowClear
|
||||||
Panel Time Preference
|
value={description}
|
||||||
</Typography.Text>
|
onChange={(event): void =>
|
||||||
<TimePreference
|
onChangeHandler(setDescription, event.target.value)
|
||||||
{...{
|
|
||||||
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'
|
|
||||||
}
|
}
|
||||||
// Only update the y-axis unit value automatically in create mode
|
rootClassName="description-input"
|
||||||
shouldUpdateYAxisUnit={isNewDashboard}
|
|
||||||
/>
|
/>
|
||||||
)}
|
</section>
|
||||||
|
</SettingsSection>
|
||||||
|
|
||||||
{allowDecimalPrecision && (
|
<section className="panel-config">
|
||||||
<section className="decimal-precision-selector">
|
<SettingsSection
|
||||||
<Typography.Text className="typography">
|
title="Visualization"
|
||||||
Decimal Precision
|
defaultOpen
|
||||||
</Typography.Text>
|
icon={<LayoutDashboard size={14} />}
|
||||||
|
>
|
||||||
|
<section className="panel-type control-container">
|
||||||
|
<Typography.Text className="typography">Panel Type</Typography.Text>
|
||||||
<Select
|
<Select
|
||||||
options={[
|
onChange={setGraphHandler}
|
||||||
{ label: '0 decimals', value: PrecisionOptionsEnum.ZERO },
|
value={selectedGraph}
|
||||||
{ 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%' }}
|
|
||||||
className="panel-type-select"
|
className="panel-type-select"
|
||||||
defaultValue={PrecisionOptionsEnum.TWO}
|
data-testid="panel-change-select"
|
||||||
onChange={(val: PrecisionOption): void => setDecimalPrecision(val)}
|
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>
|
</section>
|
||||||
)}
|
|
||||||
|
|
||||||
{allowSoftMinMax && (
|
{allowPanelTimePreference && (
|
||||||
<section className="soft-min-max">
|
<section className="panel-time-preference control-container">
|
||||||
<section className="container">
|
<Typography.Text className="panel-time-text">
|
||||||
<Typography.Text className="text">Soft Min</Typography.Text>
|
Panel Time Preference
|
||||||
<InputNumber
|
</Typography.Text>
|
||||||
type="number"
|
<TimePreference
|
||||||
value={softMin}
|
{...{
|
||||||
onChange={softMinHandler}
|
selectedTime,
|
||||||
rootClassName="input"
|
setSelectedTime,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
<section className="container">
|
)}
|
||||||
<Typography.Text className="text">Soft Max</Typography.Text>
|
|
||||||
<InputNumber
|
{allowStackingBarChart && (
|
||||||
value={softMax}
|
<section className="stack-chart control-container">
|
||||||
type="number"
|
<Typography.Text className="label">Stack series</Typography.Text>
|
||||||
rootClassName="input"
|
<Switch
|
||||||
onChange={softMaxHandler}
|
checked={stackedBarChart}
|
||||||
|
size="small"
|
||||||
|
onChange={(checked): void => setStackedBarChart(checked)}
|
||||||
/>
|
/>
|
||||||
</section>
|
</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 && (
|
{isAxisSectionVisible && (
|
||||||
<section className="stack-chart">
|
<SettingsSection title="Axes" icon={<Axis3D size={14} />}>
|
||||||
<Typography.Text className="label">Stack series</Typography.Text>
|
{allowSoftMinMax && (
|
||||||
<Switch
|
<section className="soft-min-max">
|
||||||
checked={stackedBarChart}
|
<section className="container">
|
||||||
size="small"
|
<Typography.Text className="text">Soft Min</Typography.Text>
|
||||||
onChange={(checked): void => setStackedBarChart(checked)}
|
<InputNumber
|
||||||
/>
|
type="number"
|
||||||
</section>
|
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 && (
|
{allowBucketConfig && (
|
||||||
<section className="bucket-config">
|
<SettingsSection title="Histogram / Buckets">
|
||||||
<Typography.Text className="label">Number of buckets</Typography.Text>
|
<section className="bucket-config control-container">
|
||||||
<InputNumber
|
<Typography.Text className="label">Number of buckets</Typography.Text>
|
||||||
value={bucketCount || null}
|
<InputNumber
|
||||||
type="number"
|
value={bucketCount || null}
|
||||||
min={0}
|
type="number"
|
||||||
rootClassName="bucket-input"
|
min={0}
|
||||||
placeholder="Default: 30"
|
rootClassName="bucket-input"
|
||||||
onChange={(val): void => {
|
placeholder="Default: 30"
|
||||||
setBucketCount(val || 0);
|
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)}
|
|
||||||
/>
|
/>
|
||||||
|
<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>
|
||||||
</section>
|
</SettingsSection>
|
||||||
)}
|
|
||||||
|
|
||||||
{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>
|
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -541,17 +601,25 @@ function RightContainer({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{allowContextLinks && (
|
{allowContextLinks && (
|
||||||
<section className="context-links">
|
<SettingsSection
|
||||||
|
title="Context Links"
|
||||||
|
icon={<Link size={14} />}
|
||||||
|
defaultOpen={!!contextLinks.linksData.length}
|
||||||
|
>
|
||||||
<ContextLinks
|
<ContextLinks
|
||||||
contextLinks={contextLinks}
|
contextLinks={contextLinks}
|
||||||
setContextLinks={setContextLinks}
|
setContextLinks={setContextLinks}
|
||||||
selectedWidget={selectedWidget}
|
selectedWidget={selectedWidget}
|
||||||
/>
|
/>
|
||||||
</section>
|
</SettingsSection>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{allowThreshold && (
|
{allowThreshold && (
|
||||||
<section>
|
<SettingsSection
|
||||||
|
title="Thresholds"
|
||||||
|
icon={<Antenna size={14} />}
|
||||||
|
defaultOpen={!!thresholds.length}
|
||||||
|
>
|
||||||
<ThresholdSelector
|
<ThresholdSelector
|
||||||
thresholds={thresholds}
|
thresholds={thresholds}
|
||||||
setThresholds={setThresholds}
|
setThresholds={setThresholds}
|
||||||
@@ -559,7 +627,7 @@ function RightContainer({
|
|||||||
selectedGraph={selectedGraph}
|
selectedGraph={selectedGraph}
|
||||||
columnUnits={columnUnits}
|
columnUnits={columnUnits}
|
||||||
/>
|
/>
|
||||||
</section>
|
</SettingsSection>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ const checkStackSeriesState = (
|
|||||||
expect(getByTextUtil(container, 'Stack series')).toBeInTheDocument();
|
expect(getByTextUtil(container, 'Stack series')).toBeInTheDocument();
|
||||||
|
|
||||||
const stackSeriesSection = container.querySelector(
|
const stackSeriesSection = container.querySelector(
|
||||||
'section > .stack-chart',
|
'.stack-chart',
|
||||||
) as HTMLElement;
|
) as HTMLElement;
|
||||||
expect(stackSeriesSection).toBeInTheDocument();
|
expect(stackSeriesSection).toBeInTheDocument();
|
||||||
|
|
||||||
@@ -326,7 +326,7 @@ describe('Stacking bar in new panel', () => {
|
|||||||
expect(getByText('Stack series')).toBeInTheDocument();
|
expect(getByText('Stack series')).toBeInTheDocument();
|
||||||
|
|
||||||
// Verify section exists
|
// Verify section exists
|
||||||
const section = container.querySelector('section > .stack-chart');
|
const section = container.querySelector('.stack-chart');
|
||||||
expect(section).toBeInTheDocument();
|
expect(section).toBeInTheDocument();
|
||||||
|
|
||||||
// Verify switch is present and enabled (ant-switch-checked)
|
// Verify switch is present and enabled (ant-switch-checked)
|
||||||
|
|||||||
@@ -439,6 +439,19 @@ function NewWidget({
|
|||||||
globalSelectedInterval,
|
globalSelectedInterval,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const navigateToDashboardPage = useCallback(() => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
|
const urlVariablesQueryString = query.get(QueryParams.variables);
|
||||||
|
if (urlVariablesQueryString) {
|
||||||
|
params.set(QueryParams.variables, urlVariablesQueryString);
|
||||||
|
}
|
||||||
|
|
||||||
|
const search = params.toString() ? `?${params.toString()}` : '';
|
||||||
|
|
||||||
|
safeNavigate(generatePath(ROUTES.DASHBOARD, { dashboardId }) + search);
|
||||||
|
}, [dashboardId, query, safeNavigate]);
|
||||||
|
|
||||||
const onClickSaveHandler = useCallback(() => {
|
const onClickSaveHandler = useCallback(() => {
|
||||||
if (!selectedDashboard) {
|
if (!selectedDashboard) {
|
||||||
return;
|
return;
|
||||||
@@ -554,9 +567,7 @@ function NewWidget({
|
|||||||
updateDashboardMutation.mutateAsync(dashboard, {
|
updateDashboardMutation.mutateAsync(dashboard, {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
setToScrollWidgetId(selectedWidget?.id || '');
|
setToScrollWidgetId(selectedWidget?.id || '');
|
||||||
safeNavigate({
|
navigateToDashboardPage();
|
||||||
pathname: generatePath(ROUTES.DASHBOARD, { dashboardId }),
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}, [
|
}, [
|
||||||
@@ -572,7 +583,7 @@ function NewWidget({
|
|||||||
updateDashboardMutation,
|
updateDashboardMutation,
|
||||||
widgets,
|
widgets,
|
||||||
setToScrollWidgetId,
|
setToScrollWidgetId,
|
||||||
safeNavigate,
|
navigateToDashboardPage,
|
||||||
dashboardId,
|
dashboardId,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -581,12 +592,12 @@ function NewWidget({
|
|||||||
setDiscardModal(true);
|
setDiscardModal(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
safeNavigate(generatePath(ROUTES.DASHBOARD, { dashboardId }));
|
navigateToDashboardPage();
|
||||||
}, [dashboardId, isQueryModified, safeNavigate]);
|
}, [isQueryModified, navigateToDashboardPage]);
|
||||||
|
|
||||||
const discardChanges = useCallback(() => {
|
const discardChanges = useCallback(() => {
|
||||||
safeNavigate(generatePath(ROUTES.DASHBOARD, { dashboardId }));
|
navigateToDashboardPage();
|
||||||
}, [dashboardId, safeNavigate]);
|
}, [navigateToDashboardPage]);
|
||||||
|
|
||||||
const setGraphHandler = (type: PANEL_TYPES): void => {
|
const setGraphHandler = (type: PANEL_TYPES): void => {
|
||||||
setIsLoadingPanelData(true);
|
setIsLoadingPanelData(true);
|
||||||
@@ -728,12 +739,14 @@ function NewWidget({
|
|||||||
}
|
}
|
||||||
const widgetId = query.get('widgetId') || '';
|
const widgetId = query.get('widgetId') || '';
|
||||||
const graphType = query.get('graphType') || '';
|
const graphType = query.get('graphType') || '';
|
||||||
|
const variables = query.get(QueryParams.variables) || '';
|
||||||
const queryParams = {
|
const queryParams = {
|
||||||
[QueryParams.expandedWidgetId]: widgetId,
|
[QueryParams.expandedWidgetId]: widgetId,
|
||||||
[QueryParams.graphType]: graphType,
|
[QueryParams.graphType]: graphType,
|
||||||
[QueryParams.compositeQuery]: encodeURIComponent(
|
[QueryParams.compositeQuery]: encodeURIComponent(
|
||||||
JSON.stringify(currentQuery),
|
JSON.stringify(currentQuery),
|
||||||
),
|
),
|
||||||
|
[QueryParams.variables]: variables,
|
||||||
};
|
};
|
||||||
|
|
||||||
const updatedSearch = createQueryParams(queryParams);
|
const updatedSearch = createQueryParams(queryParams);
|
||||||
@@ -822,56 +835,54 @@ function NewWidget({
|
|||||||
</LeftContainerWrapper>
|
</LeftContainerWrapper>
|
||||||
|
|
||||||
<RightContainerWrapper>
|
<RightContainerWrapper>
|
||||||
<OverlayScrollbar>
|
<RightContainer
|
||||||
<RightContainer
|
setGraphHandler={setGraphHandler}
|
||||||
setGraphHandler={setGraphHandler}
|
title={title}
|
||||||
title={title}
|
setTitle={setTitle}
|
||||||
setTitle={setTitle}
|
description={description}
|
||||||
description={description}
|
setDescription={setDescription}
|
||||||
setDescription={setDescription}
|
stackedBarChart={stackedBarChart}
|
||||||
stackedBarChart={stackedBarChart}
|
setStackedBarChart={setStackedBarChart}
|
||||||
setStackedBarChart={setStackedBarChart}
|
opacity={opacity}
|
||||||
opacity={opacity}
|
yAxisUnit={yAxisUnit}
|
||||||
yAxisUnit={yAxisUnit}
|
columnUnits={columnUnits}
|
||||||
columnUnits={columnUnits}
|
setColumnUnits={setColumnUnits}
|
||||||
setColumnUnits={setColumnUnits}
|
bucketCount={bucketCount}
|
||||||
bucketCount={bucketCount}
|
bucketWidth={bucketWidth}
|
||||||
bucketWidth={bucketWidth}
|
combineHistogram={combineHistogram}
|
||||||
combineHistogram={combineHistogram}
|
setCombineHistogram={setCombineHistogram}
|
||||||
setCombineHistogram={setCombineHistogram}
|
setBucketWidth={setBucketWidth}
|
||||||
setBucketWidth={setBucketWidth}
|
setBucketCount={setBucketCount}
|
||||||
setBucketCount={setBucketCount}
|
setOpacity={setOpacity}
|
||||||
setOpacity={setOpacity}
|
selectedNullZeroValue={selectedNullZeroValue}
|
||||||
selectedNullZeroValue={selectedNullZeroValue}
|
setSelectedNullZeroValue={setSelectedNullZeroValue}
|
||||||
setSelectedNullZeroValue={setSelectedNullZeroValue}
|
selectedGraph={graphType}
|
||||||
selectedGraph={graphType}
|
setSelectedTime={setSelectedTime}
|
||||||
setSelectedTime={setSelectedTime}
|
selectedTime={selectedTime}
|
||||||
selectedTime={selectedTime}
|
setYAxisUnit={setYAxisUnit}
|
||||||
setYAxisUnit={setYAxisUnit}
|
decimalPrecision={decimalPrecision}
|
||||||
decimalPrecision={decimalPrecision}
|
setDecimalPrecision={setDecimalPrecision}
|
||||||
setDecimalPrecision={setDecimalPrecision}
|
thresholds={thresholds}
|
||||||
thresholds={thresholds}
|
setThresholds={setThresholds}
|
||||||
setThresholds={setThresholds}
|
selectedWidget={selectedWidget}
|
||||||
selectedWidget={selectedWidget}
|
isFillSpans={isFillSpans}
|
||||||
isFillSpans={isFillSpans}
|
setIsFillSpans={setIsFillSpans}
|
||||||
setIsFillSpans={setIsFillSpans}
|
isLogScale={isLogScale}
|
||||||
isLogScale={isLogScale}
|
setIsLogScale={setIsLogScale}
|
||||||
setIsLogScale={setIsLogScale}
|
legendPosition={legendPosition}
|
||||||
legendPosition={legendPosition}
|
setLegendPosition={setLegendPosition}
|
||||||
setLegendPosition={setLegendPosition}
|
customLegendColors={customLegendColors}
|
||||||
customLegendColors={customLegendColors}
|
setCustomLegendColors={setCustomLegendColors}
|
||||||
setCustomLegendColors={setCustomLegendColors}
|
queryResponse={queryResponse}
|
||||||
queryResponse={queryResponse}
|
softMin={softMin}
|
||||||
softMin={softMin}
|
setSoftMin={setSoftMin}
|
||||||
setSoftMin={setSoftMin}
|
softMax={softMax}
|
||||||
softMax={softMax}
|
setSoftMax={setSoftMax}
|
||||||
setSoftMax={setSoftMax}
|
contextLinks={contextLinks}
|
||||||
contextLinks={contextLinks}
|
setContextLinks={setContextLinks}
|
||||||
setContextLinks={setContextLinks}
|
enableDrillDown={enableDrillDown}
|
||||||
enableDrillDown={enableDrillDown}
|
isNewDashboard={isNewDashboard}
|
||||||
isNewDashboard={isNewDashboard}
|
/>
|
||||||
/>
|
|
||||||
</OverlayScrollbar>
|
|
||||||
</RightContainerWrapper>
|
</RightContainerWrapper>
|
||||||
</PanelContainer>
|
</PanelContainer>
|
||||||
<Modal
|
<Modal
|
||||||
|
|||||||
@@ -15,7 +15,14 @@ export const RightContainerWrapper = styled(Col)`
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
width: 0rem;
|
width: 0.3rem;
|
||||||
|
}
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background: rgb(136, 136, 136);
|
||||||
|
border-radius: 0.625rem;
|
||||||
|
}
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -11,11 +11,8 @@ import { getYAxisCategories } from 'components/YAxisUnitSelector/utils';
|
|||||||
import {
|
import {
|
||||||
initialQueryBuilderFormValuesMap,
|
initialQueryBuilderFormValuesMap,
|
||||||
PANEL_TYPES,
|
PANEL_TYPES,
|
||||||
} from 'constants/queryBuilder';
|
|
||||||
import {
|
|
||||||
listViewInitialLogQuery,
|
|
||||||
PANEL_TYPES_INITIAL_QUERY,
|
PANEL_TYPES_INITIAL_QUERY,
|
||||||
} from 'container/DashboardContainer/ComponentsSlider/constants';
|
} from 'constants/queryBuilder';
|
||||||
import {
|
import {
|
||||||
defaultLogsSelectedColumns,
|
defaultLogsSelectedColumns,
|
||||||
defaultTraceSelectedColumns,
|
defaultTraceSelectedColumns,
|
||||||
@@ -549,10 +546,7 @@ export const getDefaultWidgetData = (
|
|||||||
nullZeroValues: '',
|
nullZeroValues: '',
|
||||||
opacity: '',
|
opacity: '',
|
||||||
panelTypes: name,
|
panelTypes: name,
|
||||||
query:
|
query: PANEL_TYPES_INITIAL_QUERY[name],
|
||||||
name === PANEL_TYPES.LIST
|
|
||||||
? listViewInitialLogQuery
|
|
||||||
: PANEL_TYPES_INITIAL_QUERY[name],
|
|
||||||
timePreferance: 'GLOBAL_TIME',
|
timePreferance: 'GLOBAL_TIME',
|
||||||
softMax: null,
|
softMax: null,
|
||||||
softMin: null,
|
softMin: null,
|
||||||
|
|||||||
@@ -0,0 +1,339 @@
|
|||||||
|
import { renderHook } from '@testing-library/react';
|
||||||
|
import { useDashboardVariablesFromLocalStorage } from 'hooks/dashboard/useDashboardFromLocalStorage';
|
||||||
|
import { useTransformDashboardVariables } from 'hooks/dashboard/useTransformDashboardVariables';
|
||||||
|
import useVariablesFromUrl from 'hooks/dashboard/useVariablesFromUrl';
|
||||||
|
import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||||
|
|
||||||
|
jest.mock('hooks/dashboard/useDashboardFromLocalStorage');
|
||||||
|
jest.mock('hooks/dashboard/useVariablesFromUrl');
|
||||||
|
|
||||||
|
const mockUseDashboardVariablesFromLocalStorage = useDashboardVariablesFromLocalStorage as jest.MockedFunction<
|
||||||
|
typeof useDashboardVariablesFromLocalStorage
|
||||||
|
>;
|
||||||
|
const mockUseVariablesFromUrl = useVariablesFromUrl as jest.MockedFunction<
|
||||||
|
typeof useVariablesFromUrl
|
||||||
|
>;
|
||||||
|
|
||||||
|
const makeVariable = (
|
||||||
|
overrides: Partial<IDashboardVariable> = {},
|
||||||
|
): IDashboardVariable => ({
|
||||||
|
id: 'existing-id',
|
||||||
|
name: 'env',
|
||||||
|
description: '',
|
||||||
|
type: 'QUERY',
|
||||||
|
sort: 'DISABLED',
|
||||||
|
multiSelect: false,
|
||||||
|
showALLOption: false,
|
||||||
|
selectedValue: 'prod',
|
||||||
|
...overrides,
|
||||||
|
});
|
||||||
|
|
||||||
|
const makeDashboard = (
|
||||||
|
variables: Record<string, IDashboardVariable>,
|
||||||
|
): Dashboard => ({
|
||||||
|
id: 'dash-1',
|
||||||
|
createdAt: '',
|
||||||
|
updatedAt: '',
|
||||||
|
createdBy: '',
|
||||||
|
updatedBy: '',
|
||||||
|
data: {
|
||||||
|
title: 'Test',
|
||||||
|
variables,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const setupHook = (
|
||||||
|
currentDashboard: Record<string, any> = {},
|
||||||
|
urlVariables: Record<string, any> = {},
|
||||||
|
): ReturnType<typeof useTransformDashboardVariables> => {
|
||||||
|
mockUseDashboardVariablesFromLocalStorage.mockReturnValue({
|
||||||
|
currentDashboard,
|
||||||
|
updateLocalStorageDashboardVariables: jest.fn(),
|
||||||
|
});
|
||||||
|
mockUseVariablesFromUrl.mockReturnValue({
|
||||||
|
getUrlVariables: () => urlVariables,
|
||||||
|
setUrlVariables: jest.fn(),
|
||||||
|
updateUrlVariable: jest.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useTransformDashboardVariables('dash-1'));
|
||||||
|
return result.current;
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('useTransformDashboardVariables', () => {
|
||||||
|
beforeEach(() => jest.clearAllMocks());
|
||||||
|
|
||||||
|
describe('order assignment', () => {
|
||||||
|
it('assigns order starting from 0 to variables that have none', () => {
|
||||||
|
const { transformDashboardVariables } = setupHook();
|
||||||
|
const dashboard = makeDashboard({
|
||||||
|
v1: makeVariable({ id: 'id1', name: 'v1', order: undefined }),
|
||||||
|
v2: makeVariable({ id: 'id2', name: 'v2', order: undefined }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = transformDashboardVariables(dashboard);
|
||||||
|
|
||||||
|
const orders = Object.values(result.data.variables).map((v) => v.order);
|
||||||
|
expect(orders).toContain(0);
|
||||||
|
expect(orders).toContain(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('preserves existing order values', () => {
|
||||||
|
const { transformDashboardVariables } = setupHook();
|
||||||
|
const dashboard = makeDashboard({
|
||||||
|
v1: makeVariable({ id: 'id1', name: 'v1', order: 5 }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = transformDashboardVariables(dashboard);
|
||||||
|
|
||||||
|
expect(result.data.variables.v1.order).toBe(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('assigns unique orders across multiple variables that all lack an order', () => {
|
||||||
|
const { transformDashboardVariables } = setupHook();
|
||||||
|
const dashboard = makeDashboard({
|
||||||
|
v1: makeVariable({ id: 'id1', name: 'v1', order: undefined }),
|
||||||
|
v2: makeVariable({ id: 'id2', name: 'v2', order: undefined }),
|
||||||
|
v3: makeVariable({ id: 'id3', name: 'v3', order: undefined }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = transformDashboardVariables(dashboard);
|
||||||
|
|
||||||
|
const orders = Object.values(result.data.variables).map((v) => v.order);
|
||||||
|
// All three newly assigned orders must be distinct
|
||||||
|
expect(new Set(orders).size).toBe(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ID assignment', () => {
|
||||||
|
it('assigns a UUID to variables that have no id', () => {
|
||||||
|
const { transformDashboardVariables } = setupHook();
|
||||||
|
const variable = makeVariable({ name: 'v1' });
|
||||||
|
(variable as any).id = undefined;
|
||||||
|
const dashboard = makeDashboard({ v1: variable });
|
||||||
|
|
||||||
|
const result = transformDashboardVariables(dashboard);
|
||||||
|
|
||||||
|
expect(result.data.variables.v1.id).toMatch(
|
||||||
|
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('preserves existing IDs', () => {
|
||||||
|
const { transformDashboardVariables } = setupHook();
|
||||||
|
const dashboard = makeDashboard({
|
||||||
|
v1: makeVariable({ id: 'keep-me', name: 'v1' }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = transformDashboardVariables(dashboard);
|
||||||
|
|
||||||
|
expect(result.data.variables.v1.id).toBe('keep-me');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('TEXTBOX backward compatibility', () => {
|
||||||
|
it('copies textboxValue to defaultValue when defaultValue is missing', () => {
|
||||||
|
const { transformDashboardVariables } = setupHook();
|
||||||
|
const dashboard = makeDashboard({
|
||||||
|
v1: makeVariable({
|
||||||
|
id: 'id1',
|
||||||
|
name: 'v1',
|
||||||
|
type: 'TEXTBOX',
|
||||||
|
textboxValue: 'hello',
|
||||||
|
defaultValue: undefined,
|
||||||
|
order: undefined,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = transformDashboardVariables(dashboard);
|
||||||
|
|
||||||
|
expect(result.data.variables.v1.defaultValue).toBe('hello');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not overwrite an existing defaultValue', () => {
|
||||||
|
const { transformDashboardVariables } = setupHook();
|
||||||
|
const dashboard = makeDashboard({
|
||||||
|
v1: makeVariable({
|
||||||
|
id: 'id1',
|
||||||
|
name: 'v1',
|
||||||
|
type: 'TEXTBOX',
|
||||||
|
textboxValue: 'old',
|
||||||
|
defaultValue: 'keep',
|
||||||
|
order: undefined,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = transformDashboardVariables(dashboard);
|
||||||
|
|
||||||
|
expect(result.data.variables.v1.defaultValue).toBe('keep');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('localStorage merge', () => {
|
||||||
|
it('applies localStorage selectedValue over DB value', () => {
|
||||||
|
const { transformDashboardVariables } = setupHook({
|
||||||
|
env: { selectedValue: 'staging', allSelected: false },
|
||||||
|
});
|
||||||
|
const dashboard = makeDashboard({
|
||||||
|
v1: makeVariable({ id: 'id1', name: 'env', selectedValue: 'prod' }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = transformDashboardVariables(dashboard);
|
||||||
|
|
||||||
|
expect(result.data.variables.v1.selectedValue).toBe('staging');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('applies localStorage allSelected over DB value', () => {
|
||||||
|
const { transformDashboardVariables } = setupHook({
|
||||||
|
env: { selectedValue: undefined, allSelected: true },
|
||||||
|
});
|
||||||
|
const dashboard = makeDashboard({
|
||||||
|
v1: makeVariable({
|
||||||
|
id: 'id1',
|
||||||
|
name: 'env',
|
||||||
|
allSelected: false,
|
||||||
|
showALLOption: true,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = transformDashboardVariables(dashboard);
|
||||||
|
|
||||||
|
expect(result.data.variables.v1.allSelected).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('URL variable override', () => {
|
||||||
|
it('sets allSelected=true when URL value is __ALL__', () => {
|
||||||
|
const { transformDashboardVariables } = setupHook(
|
||||||
|
{ env: { selectedValue: 'prod', allSelected: false } },
|
||||||
|
{ env: '__ALL__' },
|
||||||
|
);
|
||||||
|
const dashboard = makeDashboard({
|
||||||
|
v1: makeVariable({
|
||||||
|
id: 'id1',
|
||||||
|
name: 'env',
|
||||||
|
showALLOption: true,
|
||||||
|
allSelected: false,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = transformDashboardVariables(dashboard);
|
||||||
|
|
||||||
|
expect(result.data.variables.v1.allSelected).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets selectedValue from URL and clears allSelected when showALLOption is true', () => {
|
||||||
|
const { transformDashboardVariables } = setupHook(
|
||||||
|
{ env: { selectedValue: undefined, allSelected: true } },
|
||||||
|
{ env: 'dev' },
|
||||||
|
);
|
||||||
|
const dashboard = makeDashboard({
|
||||||
|
v1: makeVariable({
|
||||||
|
id: 'id1',
|
||||||
|
name: 'env',
|
||||||
|
showALLOption: true,
|
||||||
|
allSelected: true,
|
||||||
|
multiSelect: false,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = transformDashboardVariables(dashboard);
|
||||||
|
|
||||||
|
expect(result.data.variables.v1.selectedValue).toBe('dev');
|
||||||
|
expect(result.data.variables.v1.allSelected).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not set allSelected=false when showALLOption is false', () => {
|
||||||
|
const { transformDashboardVariables } = setupHook(
|
||||||
|
{ env: { selectedValue: undefined, allSelected: true } },
|
||||||
|
{ env: 'dev' },
|
||||||
|
);
|
||||||
|
const dashboard = makeDashboard({
|
||||||
|
v1: makeVariable({
|
||||||
|
id: 'id1',
|
||||||
|
name: 'env',
|
||||||
|
showALLOption: false,
|
||||||
|
allSelected: true,
|
||||||
|
multiSelect: false,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = transformDashboardVariables(dashboard);
|
||||||
|
|
||||||
|
expect(result.data.variables.v1.selectedValue).toBe('dev');
|
||||||
|
expect(result.data.variables.v1.allSelected).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('normalizes array URL value to single value for single-select variable', () => {
|
||||||
|
const { transformDashboardVariables } = setupHook(
|
||||||
|
{},
|
||||||
|
{ env: ['prod', 'dev'] },
|
||||||
|
);
|
||||||
|
const dashboard = makeDashboard({
|
||||||
|
v1: makeVariable({
|
||||||
|
id: 'id1',
|
||||||
|
name: 'env',
|
||||||
|
multiSelect: false,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = transformDashboardVariables(dashboard);
|
||||||
|
|
||||||
|
expect(result.data.variables.v1.selectedValue).toBe('prod');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('wraps single URL value in array for multi-select variable', () => {
|
||||||
|
const { transformDashboardVariables } = setupHook({}, { env: 'prod' });
|
||||||
|
const dashboard = makeDashboard({
|
||||||
|
v1: makeVariable({
|
||||||
|
id: 'id1',
|
||||||
|
name: 'env',
|
||||||
|
multiSelect: true,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = transformDashboardVariables(dashboard);
|
||||||
|
|
||||||
|
expect(result.data.variables.v1.selectedValue).toEqual(['prod']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('looks up URL variable by variable id when name is absent', () => {
|
||||||
|
const { transformDashboardVariables } = setupHook(
|
||||||
|
{},
|
||||||
|
{ 'var-uuid': 'fallback' },
|
||||||
|
);
|
||||||
|
const variable = makeVariable({ id: 'var-uuid', multiSelect: false });
|
||||||
|
delete variable.name;
|
||||||
|
const dashboard = makeDashboard({ v1: variable });
|
||||||
|
|
||||||
|
const result = transformDashboardVariables(dashboard);
|
||||||
|
|
||||||
|
expect(result.data.variables.v1.selectedValue).toBe('fallback');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('edge cases', () => {
|
||||||
|
it('returns data unchanged when there are no variables', () => {
|
||||||
|
const { transformDashboardVariables } = setupHook();
|
||||||
|
const dashboard = makeDashboard({});
|
||||||
|
|
||||||
|
const result = transformDashboardVariables(dashboard);
|
||||||
|
|
||||||
|
expect(result.data.variables).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not mutate the original dashboard', () => {
|
||||||
|
const { transformDashboardVariables } = setupHook({
|
||||||
|
env: { selectedValue: 'staging', allSelected: false },
|
||||||
|
});
|
||||||
|
const dashboard = makeDashboard({
|
||||||
|
v1: makeVariable({ id: 'id1', name: 'env', selectedValue: 'prod' }),
|
||||||
|
});
|
||||||
|
const originalValue = dashboard.data.variables.v1.selectedValue;
|
||||||
|
|
||||||
|
transformDashboardVariables(dashboard);
|
||||||
|
|
||||||
|
expect(dashboard.data.variables.v1.selectedValue).toBe(originalValue);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -15,7 +15,7 @@ interface DashboardLocalStorageVariables {
|
|||||||
[id: string]: LocalStoreDashboardVariables;
|
[id: string]: LocalStoreDashboardVariables;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UseDashboardVariablesFromLocalStorageReturn {
|
export interface UseDashboardVariablesFromLocalStorageReturn {
|
||||||
currentDashboard: LocalStoreDashboardVariables;
|
currentDashboard: LocalStoreDashboardVariables;
|
||||||
updateLocalStorageDashboardVariables: (
|
updateLocalStorageDashboardVariables: (
|
||||||
id: string,
|
id: string,
|
||||||
|
|||||||
128
frontend/src/hooks/dashboard/useTransformDashboardVariables.ts
Normal file
128
frontend/src/hooks/dashboard/useTransformDashboardVariables.ts
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
import { ALL_SELECTED_VALUE } from 'components/NewSelect/utils';
|
||||||
|
import {
|
||||||
|
useDashboardVariablesFromLocalStorage,
|
||||||
|
UseDashboardVariablesFromLocalStorageReturn,
|
||||||
|
} from 'hooks/dashboard/useDashboardFromLocalStorage';
|
||||||
|
import useVariablesFromUrl, {
|
||||||
|
UseVariablesFromUrlReturn,
|
||||||
|
} from 'hooks/dashboard/useVariablesFromUrl';
|
||||||
|
import { isEmpty } from 'lodash-es';
|
||||||
|
import { normalizeUrlValueForVariable } from 'providers/Dashboard/normalizeUrlValue';
|
||||||
|
import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||||
|
import { v4 as generateUUID } from 'uuid';
|
||||||
|
|
||||||
|
export function useTransformDashboardVariables(
|
||||||
|
dashboardId: string,
|
||||||
|
): Pick<UseVariablesFromUrlReturn, 'getUrlVariables' | 'updateUrlVariable'> &
|
||||||
|
UseDashboardVariablesFromLocalStorageReturn & {
|
||||||
|
transformDashboardVariables: (data: Dashboard) => Dashboard;
|
||||||
|
} {
|
||||||
|
const {
|
||||||
|
currentDashboard,
|
||||||
|
updateLocalStorageDashboardVariables,
|
||||||
|
} = useDashboardVariablesFromLocalStorage(dashboardId);
|
||||||
|
const { getUrlVariables, updateUrlVariable } = useVariablesFromUrl();
|
||||||
|
|
||||||
|
const mergeDBWithLocalStorage = (
|
||||||
|
data: Dashboard,
|
||||||
|
localStorageVariables: any,
|
||||||
|
): Dashboard => {
|
||||||
|
const updatedData = data;
|
||||||
|
if (data && localStorageVariables) {
|
||||||
|
const updatedVariables = data.data.variables;
|
||||||
|
const variablesFromUrl = getUrlVariables();
|
||||||
|
Object.keys(data.data.variables).forEach((variable) => {
|
||||||
|
const variableData = data.data.variables[variable];
|
||||||
|
|
||||||
|
// values from url
|
||||||
|
const urlVariable = variableData?.name
|
||||||
|
? variablesFromUrl[variableData?.name] || variablesFromUrl[variableData.id]
|
||||||
|
: variablesFromUrl[variableData.id];
|
||||||
|
|
||||||
|
let updatedVariable = {
|
||||||
|
...data.data.variables[variable],
|
||||||
|
...localStorageVariables[variableData.name as any],
|
||||||
|
};
|
||||||
|
|
||||||
|
// respect the url variable if it is set, override the others
|
||||||
|
if (!isEmpty(urlVariable)) {
|
||||||
|
if (urlVariable === ALL_SELECTED_VALUE) {
|
||||||
|
updatedVariable = {
|
||||||
|
...updatedVariable,
|
||||||
|
allSelected: true,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Normalize URL value to match variable's multiSelect configuration
|
||||||
|
const normalizedValue = normalizeUrlValueForVariable(
|
||||||
|
urlVariable,
|
||||||
|
variableData,
|
||||||
|
);
|
||||||
|
|
||||||
|
updatedVariable = {
|
||||||
|
...updatedVariable,
|
||||||
|
selectedValue: normalizedValue,
|
||||||
|
// Only set allSelected to false if showALLOption is available
|
||||||
|
...(updatedVariable?.showALLOption && { allSelected: false }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedVariables[variable] = updatedVariable;
|
||||||
|
});
|
||||||
|
updatedData.data.variables = updatedVariables;
|
||||||
|
}
|
||||||
|
return updatedData;
|
||||||
|
};
|
||||||
|
|
||||||
|
// As we do not have order and ID's in the variables object, we have to process variables to add order and ID if they do not exist in the variables object
|
||||||
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
|
const transformDashboardVariables = (data: Dashboard): Dashboard => {
|
||||||
|
if (data && data.data && data.data.variables) {
|
||||||
|
const clonedDashboardData = mergeDBWithLocalStorage(
|
||||||
|
JSON.parse(JSON.stringify(data)),
|
||||||
|
currentDashboard,
|
||||||
|
);
|
||||||
|
const { variables } = clonedDashboardData.data;
|
||||||
|
const existingOrders: Set<number> = new Set();
|
||||||
|
|
||||||
|
for (const key in variables) {
|
||||||
|
// eslint-disable-next-line no-prototype-builtins
|
||||||
|
if (variables.hasOwnProperty(key)) {
|
||||||
|
const variable: IDashboardVariable = variables[key];
|
||||||
|
|
||||||
|
// Check if 'order' property doesn't exist or is undefined
|
||||||
|
if (variable.order === undefined) {
|
||||||
|
// Find a unique order starting from 0
|
||||||
|
let order = 0;
|
||||||
|
while (existingOrders.has(order)) {
|
||||||
|
order += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
variable.order = order;
|
||||||
|
existingOrders.add(order);
|
||||||
|
// ! BWC - Specific case for backward compatibility where textboxValue was used instead of defaultValue
|
||||||
|
if (variable.type === 'TEXTBOX' && !variable.defaultValue) {
|
||||||
|
variable.defaultValue = variable.textboxValue || '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (variable.id === undefined) {
|
||||||
|
variable.id = generateUUID();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return clonedDashboardData;
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
transformDashboardVariables,
|
||||||
|
getUrlVariables,
|
||||||
|
updateUrlVariable,
|
||||||
|
currentDashboard,
|
||||||
|
updateLocalStorageDashboardVariables,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ export interface LocalStoreDashboardVariables {
|
|||||||
| IDashboardVariable['selectedValue'];
|
| IDashboardVariable['selectedValue'];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UseVariablesFromUrlReturn {
|
export interface UseVariablesFromUrlReturn {
|
||||||
getUrlVariables: () => LocalStoreDashboardVariables;
|
getUrlVariables: () => LocalStoreDashboardVariables;
|
||||||
setUrlVariables: (variables: LocalStoreDashboardVariables) => void;
|
setUrlVariables: (variables: LocalStoreDashboardVariables) => void;
|
||||||
updateUrlVariable: (
|
updateUrlVariable: (
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import {
|
|||||||
ATTRIBUTE_TYPES,
|
ATTRIBUTE_TYPES,
|
||||||
initialAutocompleteData,
|
initialAutocompleteData,
|
||||||
initialQueryBuilderFormValuesMap,
|
initialQueryBuilderFormValuesMap,
|
||||||
|
listViewInitialLogQuery,
|
||||||
|
listViewInitialTraceQuery,
|
||||||
mapOfFormulaToFilters,
|
mapOfFormulaToFilters,
|
||||||
mapOfQueryFilters,
|
mapOfQueryFilters,
|
||||||
PANEL_TYPES,
|
PANEL_TYPES,
|
||||||
@@ -23,10 +25,6 @@ import {
|
|||||||
metricsUnknownSpaceAggregateOperatorOptions,
|
metricsUnknownSpaceAggregateOperatorOptions,
|
||||||
metricsUnknownTimeAggregateOperatorOptions,
|
metricsUnknownTimeAggregateOperatorOptions,
|
||||||
} from 'constants/queryBuilderOperators';
|
} from 'constants/queryBuilderOperators';
|
||||||
import {
|
|
||||||
listViewInitialLogQuery,
|
|
||||||
listViewInitialTraceQuery,
|
|
||||||
} from 'container/DashboardContainer/ComponentsSlider/constants';
|
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import { getMetricsOperatorsByAttributeType } from 'lib/newQueryBuilder/getMetricsOperatorsByAttributeType';
|
import { getMetricsOperatorsByAttributeType } from 'lib/newQueryBuilder/getMetricsOperatorsByAttributeType';
|
||||||
import { getOperatorsBySourceAndPanelType } from 'lib/newQueryBuilder/getOperatorsBySourceAndPanelType';
|
import { getOperatorsBySourceAndPanelType } from 'lib/newQueryBuilder/getOperatorsBySourceAndPanelType';
|
||||||
|
|||||||
@@ -0,0 +1,143 @@
|
|||||||
|
import { Route } from 'react-router-dom';
|
||||||
|
import * as getDashboardModule from 'api/v1/dashboards/id/get';
|
||||||
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
|
import { rest, server } from 'mocks-server/server';
|
||||||
|
import { render, screen, waitFor } from 'tests/test-utils';
|
||||||
|
|
||||||
|
import DashboardWidget from '../index';
|
||||||
|
|
||||||
|
const DASHBOARD_ID = 'dash-1';
|
||||||
|
const WIDGET_ID = 'widget-abc';
|
||||||
|
|
||||||
|
const mockDashboardResponse = {
|
||||||
|
status: 'success',
|
||||||
|
data: {
|
||||||
|
id: DASHBOARD_ID,
|
||||||
|
createdAt: '2024-01-01T00:00:00Z',
|
||||||
|
createdBy: 'test',
|
||||||
|
updatedAt: '2024-01-01T00:00:00Z',
|
||||||
|
updatedBy: 'test',
|
||||||
|
isLocked: false,
|
||||||
|
data: {
|
||||||
|
collapsableRowsMigrated: true,
|
||||||
|
description: '',
|
||||||
|
name: '',
|
||||||
|
panelMap: {},
|
||||||
|
tags: [],
|
||||||
|
title: 'Test Dashboard',
|
||||||
|
uploadedGrafana: false,
|
||||||
|
uuid: '',
|
||||||
|
version: '',
|
||||||
|
variables: {},
|
||||||
|
widgets: [],
|
||||||
|
layout: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockSafeNavigate = jest.fn();
|
||||||
|
|
||||||
|
jest.mock('hooks/useSafeNavigate', () => ({
|
||||||
|
useSafeNavigate: (): { safeNavigate: jest.Mock } => ({
|
||||||
|
safeNavigate: mockSafeNavigate,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('container/NewWidget', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: (): JSX.Element => <div data-testid="new-widget">NewWidget</div>,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Wrap component in a Route so useParams can resolve dashboardId.
|
||||||
|
// Query params are passed via the URL so useUrlQuery (react-router) can read them.
|
||||||
|
function renderAtRoute(
|
||||||
|
queryState: Record<string, string | null> = {},
|
||||||
|
): ReturnType<typeof render> {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
Object.entries(queryState).forEach(([k, v]) => {
|
||||||
|
if (v !== null) {
|
||||||
|
params.set(k, v);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const search = params.toString() ? `?${params.toString()}` : '';
|
||||||
|
return render(
|
||||||
|
<Route path="/dashboard/:dashboardId/new">
|
||||||
|
<DashboardWidget />
|
||||||
|
</Route>,
|
||||||
|
undefined,
|
||||||
|
{ initialRoute: `/dashboard/${DASHBOARD_ID}/new${search}` },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockSafeNavigate.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('DashboardWidget', () => {
|
||||||
|
it('redirects to dashboard when widgetId is missing', async () => {
|
||||||
|
renderAtRoute({ graphType: PANEL_TYPES.TIME_SERIES });
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockSafeNavigate).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
const [navigatedTo] = mockSafeNavigate.mock.calls[0];
|
||||||
|
expect(navigatedTo).toContain(`/dashboard/${DASHBOARD_ID}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('redirects to dashboard when graphType is missing', async () => {
|
||||||
|
renderAtRoute({ widgetId: WIDGET_ID });
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockSafeNavigate).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
const [navigatedTo] = mockSafeNavigate.mock.calls[0];
|
||||||
|
expect(navigatedTo).toContain(`/dashboard/${DASHBOARD_ID}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows spinner while dashboard is loading', () => {
|
||||||
|
// Spy instead of MSW delay('infinite') to avoid leaving an open network handle.
|
||||||
|
jest
|
||||||
|
.spyOn(getDashboardModule, 'default')
|
||||||
|
.mockReturnValue(new Promise(() => {}));
|
||||||
|
|
||||||
|
renderAtRoute({ widgetId: WIDGET_ID, graphType: PANEL_TYPES.TIME_SERIES });
|
||||||
|
|
||||||
|
expect(screen.getByRole('img', { name: 'loading' })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows error message when dashboard fetch fails', async () => {
|
||||||
|
server.use(
|
||||||
|
rest.get(
|
||||||
|
`http://localhost/api/v1/dashboards/${DASHBOARD_ID}`,
|
||||||
|
(_req, res, ctx) => res(ctx.status(500), ctx.json({ status: 'error' })),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
renderAtRoute({ widgetId: WIDGET_ID, graphType: PANEL_TYPES.TIME_SERIES });
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Something went wrong')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders NewWidget when dashboard loads successfully', async () => {
|
||||||
|
server.use(
|
||||||
|
rest.get(
|
||||||
|
`http://localhost/api/v1/dashboards/${DASHBOARD_ID}`,
|
||||||
|
(_req, res, ctx) => res(ctx.status(200), ctx.json(mockDashboardResponse)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
renderAtRoute({ widgetId: WIDGET_ID, graphType: PANEL_TYPES.TIME_SERIES });
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId('new-widget')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,29 +1,34 @@
|
|||||||
import { useEffect, useMemo } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { useQuery } from 'react-query';
|
import { useQuery } from 'react-query';
|
||||||
import { generatePath, useParams } from 'react-router-dom';
|
import { generatePath, useParams } from 'react-router-dom';
|
||||||
import { Card, Typography } from 'antd';
|
import { Card, Typography } from 'antd';
|
||||||
import getDashboard from 'api/v1/dashboards/id/get';
|
import getDashboard from 'api/v1/dashboards/id/get';
|
||||||
import Spinner from 'components/Spinner';
|
import Spinner from 'components/Spinner';
|
||||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||||
|
import { QueryParams } from 'constants/query';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import { DASHBOARD_CACHE_TIME } from 'constants/queryCacheTime';
|
import { DASHBOARD_CACHE_TIME } from 'constants/queryCacheTime';
|
||||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import NewWidget from 'container/NewWidget';
|
import NewWidget from 'container/NewWidget';
|
||||||
import { isDrilldownEnabled } from 'container/QueryTable/Drilldown/drilldownUtils';
|
import { isDrilldownEnabled } from 'container/QueryTable/Drilldown/drilldownUtils';
|
||||||
|
import { useTransformDashboardVariables } from 'hooks/dashboard/useTransformDashboardVariables';
|
||||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||||
import { parseAsStringEnum, useQueryState } from 'nuqs';
|
import useUrlQuery from 'hooks/useUrlQuery';
|
||||||
import { setDashboardVariablesStore } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStore';
|
import { setDashboardVariablesStore } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStore';
|
||||||
|
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||||
|
|
||||||
function DashboardWidget(): JSX.Element | null {
|
function DashboardWidget(): JSX.Element | null {
|
||||||
const { dashboardId } = useParams<{
|
const { dashboardId } = useParams<{
|
||||||
dashboardId: string;
|
dashboardId: string;
|
||||||
}>();
|
}>();
|
||||||
const [widgetId] = useQueryState('widgetId');
|
const query = useUrlQuery();
|
||||||
const [graphType] = useQueryState(
|
const { graphType, widgetId } = useMemo(() => {
|
||||||
'graphType',
|
return {
|
||||||
parseAsStringEnum<PANEL_TYPES>(Object.values(PANEL_TYPES)),
|
graphType: query.get(QueryParams.graphType) as PANEL_TYPES,
|
||||||
);
|
widgetId: query.get(QueryParams.widgetId),
|
||||||
|
};
|
||||||
|
}, [query]);
|
||||||
|
|
||||||
const { safeNavigate } = useSafeNavigate();
|
const { safeNavigate } = useSafeNavigate();
|
||||||
|
|
||||||
@@ -57,8 +62,15 @@ function DashboardWidgetInternal({
|
|||||||
widgetId: string;
|
widgetId: string;
|
||||||
graphType: PANEL_TYPES;
|
graphType: PANEL_TYPES;
|
||||||
}): JSX.Element | null {
|
}): JSX.Element | null {
|
||||||
|
const [selectedDashboard, setSelectedDashboard] = useState<
|
||||||
|
Dashboard | undefined
|
||||||
|
>(undefined);
|
||||||
|
|
||||||
|
const { transformDashboardVariables } = useTransformDashboardVariables(
|
||||||
|
dashboardId,
|
||||||
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: dashboardResponse,
|
|
||||||
isFetching: isFetchingDashboardResponse,
|
isFetching: isFetchingDashboardResponse,
|
||||||
isError: isErrorDashboardResponse,
|
isError: isErrorDashboardResponse,
|
||||||
} = useQuery([REACT_QUERY_KEY.DASHBOARD_BY_ID, dashboardId, widgetId], {
|
} = useQuery([REACT_QUERY_KEY.DASHBOARD_BY_ID, dashboardId, widgetId], {
|
||||||
@@ -70,17 +82,15 @@ function DashboardWidgetInternal({
|
|||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
cacheTime: DASHBOARD_CACHE_TIME,
|
cacheTime: DASHBOARD_CACHE_TIME,
|
||||||
onSuccess: (response) => {
|
onSuccess: (response) => {
|
||||||
|
const updatedDashboardData = transformDashboardVariables(response.data);
|
||||||
|
setSelectedDashboard(updatedDashboardData);
|
||||||
setDashboardVariablesStore({
|
setDashboardVariablesStore({
|
||||||
dashboardId,
|
dashboardId,
|
||||||
variables: response.data.data.variables,
|
variables: updatedDashboardData.data.variables,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const selectedDashboard = useMemo(() => dashboardResponse?.data, [
|
|
||||||
dashboardResponse?.data,
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (isFetchingDashboardResponse) {
|
if (isFetchingDashboardResponse) {
|
||||||
return <Spinner tip="Loading.." />;
|
return <Spinner tip="Loading.." />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,21 +17,18 @@ import { useDispatch, useSelector } from 'react-redux';
|
|||||||
import { Modal } from 'antd';
|
import { Modal } from 'antd';
|
||||||
import getDashboard from 'api/v1/dashboards/id/get';
|
import getDashboard from 'api/v1/dashboards/id/get';
|
||||||
import locked from 'api/v1/dashboards/id/lock';
|
import locked from 'api/v1/dashboards/id/lock';
|
||||||
import { ALL_SELECTED_VALUE } from 'components/NewSelect/utils';
|
|
||||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||||
import dayjs, { Dayjs } from 'dayjs';
|
import dayjs, { Dayjs } from 'dayjs';
|
||||||
import { useDashboardVariablesFromLocalStorage } from 'hooks/dashboard/useDashboardFromLocalStorage';
|
import { useTransformDashboardVariables } from 'hooks/dashboard/useTransformDashboardVariables';
|
||||||
import useVariablesFromUrl from 'hooks/dashboard/useVariablesFromUrl';
|
|
||||||
import useTabVisibility from 'hooks/useTabFocus';
|
import useTabVisibility from 'hooks/useTabFocus';
|
||||||
import { getUpdatedLayout } from 'lib/dashboard/getUpdatedLayout';
|
import { getUpdatedLayout } from 'lib/dashboard/getUpdatedLayout';
|
||||||
import { getMinMaxForSelectedTime } from 'lib/getMinMax';
|
import { getMinMaxForSelectedTime } from 'lib/getMinMax';
|
||||||
import { defaultTo, isEmpty } from 'lodash-es';
|
import { defaultTo } from 'lodash-es';
|
||||||
import isEqual from 'lodash-es/isEqual';
|
import isEqual from 'lodash-es/isEqual';
|
||||||
import isUndefined from 'lodash-es/isUndefined';
|
import isUndefined from 'lodash-es/isUndefined';
|
||||||
import omitBy from 'lodash-es/omitBy';
|
import omitBy from 'lodash-es/omitBy';
|
||||||
import { useAppContext } from 'providers/App/App';
|
import { useAppContext } from 'providers/App/App';
|
||||||
import { initializeDefaultVariables } from 'providers/Dashboard/initializeDefaultVariables';
|
import { initializeDefaultVariables } from 'providers/Dashboard/initializeDefaultVariables';
|
||||||
import { normalizeUrlValueForVariable } from 'providers/Dashboard/normalizeUrlValue';
|
|
||||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||||
// eslint-disable-next-line no-restricted-imports
|
// eslint-disable-next-line no-restricted-imports
|
||||||
import { Dispatch } from 'redux';
|
import { Dispatch } from 'redux';
|
||||||
@@ -39,10 +36,9 @@ import { AppState } from 'store/reducers';
|
|||||||
import AppActions from 'types/actions';
|
import AppActions from 'types/actions';
|
||||||
import { UPDATE_TIME_INTERVAL } from 'types/actions/globalTime';
|
import { UPDATE_TIME_INTERVAL } from 'types/actions/globalTime';
|
||||||
import { SuccessResponseV2 } from 'types/api';
|
import { SuccessResponseV2 } from 'types/api';
|
||||||
import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll';
|
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||||
import APIError from 'types/api/error';
|
import APIError from 'types/api/error';
|
||||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
import { v4 as generateUUID } from 'uuid';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DASHBOARD_CACHE_TIME,
|
DASHBOARD_CACHE_TIME,
|
||||||
@@ -57,9 +53,7 @@ import { IDashboardContext, WidgetColumnWidths } from './types';
|
|||||||
import { sortLayout } from './util';
|
import { sortLayout } from './util';
|
||||||
|
|
||||||
export const DashboardContext = createContext<IDashboardContext>({
|
export const DashboardContext = createContext<IDashboardContext>({
|
||||||
isDashboardSliderOpen: false,
|
|
||||||
isDashboardLocked: false,
|
isDashboardLocked: false,
|
||||||
handleToggleDashboardSlider: () => {},
|
|
||||||
handleDashboardLockToggle: () => {},
|
handleDashboardLockToggle: () => {},
|
||||||
dashboardResponse: {} as UseQueryResult<
|
dashboardResponse: {} as UseQueryResult<
|
||||||
SuccessResponseV2<Dashboard>,
|
SuccessResponseV2<Dashboard>,
|
||||||
@@ -86,8 +80,6 @@ export function DashboardProvider({
|
|||||||
children,
|
children,
|
||||||
dashboardId,
|
dashboardId,
|
||||||
}: PropsWithChildren<{ dashboardId: string }>): JSX.Element {
|
}: PropsWithChildren<{ dashboardId: string }>): JSX.Element {
|
||||||
const [isDashboardSliderOpen, setIsDashboardSlider] = useState<boolean>(false);
|
|
||||||
|
|
||||||
const [isDashboardLocked, setIsDashboardLocked] = useState<boolean>(false);
|
const [isDashboardLocked, setIsDashboardLocked] = useState<boolean>(false);
|
||||||
|
|
||||||
const [
|
const [
|
||||||
@@ -137,9 +129,10 @@ export function DashboardProvider({
|
|||||||
const {
|
const {
|
||||||
currentDashboard,
|
currentDashboard,
|
||||||
updateLocalStorageDashboardVariables,
|
updateLocalStorageDashboardVariables,
|
||||||
} = useDashboardVariablesFromLocalStorage(dashboardId);
|
getUrlVariables,
|
||||||
|
updateUrlVariable,
|
||||||
const { getUrlVariables, updateUrlVariable } = useVariablesFromUrl();
|
transformDashboardVariables,
|
||||||
|
} = useTransformDashboardVariables(dashboardId);
|
||||||
|
|
||||||
const updatedTimeRef = useRef<Dayjs | null>(null); // Using ref to store the updated time
|
const updatedTimeRef = useRef<Dayjs | null>(null); // Using ref to store the updated time
|
||||||
const modalRef = useRef<any>(null);
|
const modalRef = useRef<any>(null);
|
||||||
@@ -151,99 +144,6 @@ export function DashboardProvider({
|
|||||||
|
|
||||||
const [isDashboardFetching, setIsDashboardFetching] = useState<boolean>(false);
|
const [isDashboardFetching, setIsDashboardFetching] = useState<boolean>(false);
|
||||||
|
|
||||||
const mergeDBWithLocalStorage = (
|
|
||||||
data: Dashboard,
|
|
||||||
localStorageVariables: any,
|
|
||||||
): Dashboard => {
|
|
||||||
const updatedData = data;
|
|
||||||
if (data && localStorageVariables) {
|
|
||||||
const updatedVariables = data.data.variables;
|
|
||||||
const variablesFromUrl = getUrlVariables();
|
|
||||||
Object.keys(data.data.variables).forEach((variable) => {
|
|
||||||
const variableData = data.data.variables[variable];
|
|
||||||
|
|
||||||
// values from url
|
|
||||||
const urlVariable = variableData?.name
|
|
||||||
? variablesFromUrl[variableData?.name] || variablesFromUrl[variableData.id]
|
|
||||||
: variablesFromUrl[variableData.id];
|
|
||||||
|
|
||||||
let updatedVariable = {
|
|
||||||
...data.data.variables[variable],
|
|
||||||
...localStorageVariables[variableData.name as any],
|
|
||||||
};
|
|
||||||
|
|
||||||
// respect the url variable if it is set, override the others
|
|
||||||
if (!isEmpty(urlVariable)) {
|
|
||||||
if (urlVariable === ALL_SELECTED_VALUE) {
|
|
||||||
updatedVariable = {
|
|
||||||
...updatedVariable,
|
|
||||||
allSelected: true,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
// Normalize URL value to match variable's multiSelect configuration
|
|
||||||
const normalizedValue = normalizeUrlValueForVariable(
|
|
||||||
urlVariable,
|
|
||||||
variableData,
|
|
||||||
);
|
|
||||||
|
|
||||||
updatedVariable = {
|
|
||||||
...updatedVariable,
|
|
||||||
selectedValue: normalizedValue,
|
|
||||||
// Only set allSelected to false if showALLOption is available
|
|
||||||
...(updatedVariable?.showALLOption && { allSelected: false }),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updatedVariables[variable] = updatedVariable;
|
|
||||||
});
|
|
||||||
updatedData.data.variables = updatedVariables;
|
|
||||||
}
|
|
||||||
return updatedData;
|
|
||||||
};
|
|
||||||
// As we do not have order and ID's in the variables object, we have to process variables to add order and ID if they do not exist in the variables object
|
|
||||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
|
||||||
const transformDashboardVariables = (data: Dashboard): Dashboard => {
|
|
||||||
if (data && data.data && data.data.variables) {
|
|
||||||
const clonedDashboardData = mergeDBWithLocalStorage(
|
|
||||||
JSON.parse(JSON.stringify(data)),
|
|
||||||
currentDashboard,
|
|
||||||
);
|
|
||||||
const { variables } = clonedDashboardData.data;
|
|
||||||
const existingOrders: Set<number> = new Set();
|
|
||||||
|
|
||||||
for (const key in variables) {
|
|
||||||
// eslint-disable-next-line no-prototype-builtins
|
|
||||||
if (variables.hasOwnProperty(key)) {
|
|
||||||
const variable: IDashboardVariable = variables[key];
|
|
||||||
|
|
||||||
// Check if 'order' property doesn't exist or is undefined
|
|
||||||
if (variable.order === undefined) {
|
|
||||||
// Find a unique order starting from 0
|
|
||||||
let order = 0;
|
|
||||||
while (existingOrders.has(order)) {
|
|
||||||
order += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
variable.order = order;
|
|
||||||
existingOrders.add(order);
|
|
||||||
// ! BWC - Specific case for backward compatibility where textboxValue was used instead of defaultValue
|
|
||||||
if (variable.type === 'TEXTBOX' && !variable.defaultValue) {
|
|
||||||
variable.defaultValue = variable.textboxValue || '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (variable.id === undefined) {
|
|
||||||
variable.id = generateUUID();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return clonedDashboardData;
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
};
|
|
||||||
const dashboardResponse = useQuery(
|
const dashboardResponse = useQuery(
|
||||||
[
|
[
|
||||||
REACT_QUERY_KEY.DASHBOARD_BY_ID,
|
REACT_QUERY_KEY.DASHBOARD_BY_ID,
|
||||||
@@ -274,13 +174,14 @@ export function DashboardProvider({
|
|||||||
},
|
},
|
||||||
|
|
||||||
onSuccess: (data: SuccessResponseV2<Dashboard>) => {
|
onSuccess: (data: SuccessResponseV2<Dashboard>) => {
|
||||||
// if the url variable is not set for any variable, set it to the default value
|
const updatedDashboardData = transformDashboardVariables(data?.data);
|
||||||
const variables = data?.data?.data?.variables;
|
|
||||||
|
// initialize URL variables after dashboard state is set to avoid race conditions
|
||||||
|
const variables = updatedDashboardData?.data?.variables;
|
||||||
if (variables) {
|
if (variables) {
|
||||||
initializeDefaultVariables(variables, getUrlVariables, updateUrlVariable);
|
initializeDefaultVariables(variables, getUrlVariables, updateUrlVariable);
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedDashboardData = transformDashboardVariables(data?.data);
|
|
||||||
const updatedDate = dayjs(updatedDashboardData?.updatedAt);
|
const updatedDate = dayjs(updatedDashboardData?.updatedAt);
|
||||||
|
|
||||||
setIsDashboardLocked(updatedDashboardData?.locked || false);
|
setIsDashboardLocked(updatedDashboardData?.locked || false);
|
||||||
@@ -383,13 +284,8 @@ export function DashboardProvider({
|
|||||||
}
|
}
|
||||||
}, [isVisible]);
|
}, [isVisible]);
|
||||||
|
|
||||||
const handleToggleDashboardSlider = (value: boolean): void => {
|
|
||||||
setIsDashboardSlider(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const { mutate: lockDashboard } = useMutation(locked, {
|
const { mutate: lockDashboard } = useMutation(locked, {
|
||||||
onSuccess: (_, props) => {
|
onSuccess: (_, props) => {
|
||||||
setIsDashboardSlider(false);
|
|
||||||
setIsDashboardLocked(props.lock);
|
setIsDashboardLocked(props.lock);
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
@@ -414,9 +310,7 @@ export function DashboardProvider({
|
|||||||
|
|
||||||
const value: IDashboardContext = useMemo(
|
const value: IDashboardContext = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
isDashboardSliderOpen,
|
|
||||||
isDashboardLocked,
|
isDashboardLocked,
|
||||||
handleToggleDashboardSlider,
|
|
||||||
handleDashboardLockToggle,
|
handleDashboardLockToggle,
|
||||||
dashboardResponse,
|
dashboardResponse,
|
||||||
selectedDashboard,
|
selectedDashboard,
|
||||||
@@ -436,7 +330,6 @@ export function DashboardProvider({
|
|||||||
}),
|
}),
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
[
|
[
|
||||||
isDashboardSliderOpen,
|
|
||||||
isDashboardLocked,
|
isDashboardLocked,
|
||||||
dashboardResponse,
|
dashboardResponse,
|
||||||
selectedDashboard,
|
selectedDashboard,
|
||||||
|
|||||||
@@ -381,6 +381,7 @@ describe('Dashboard Provider - URL Variables Integration', () => {
|
|||||||
multiSelect: false,
|
multiSelect: false,
|
||||||
allSelected: false,
|
allSelected: false,
|
||||||
showALLOption: true,
|
showALLOption: true,
|
||||||
|
order: 0,
|
||||||
},
|
},
|
||||||
services: {
|
services: {
|
||||||
id: 'svc-id',
|
id: 'svc-id',
|
||||||
@@ -388,6 +389,7 @@ describe('Dashboard Provider - URL Variables Integration', () => {
|
|||||||
multiSelect: true,
|
multiSelect: true,
|
||||||
allSelected: false,
|
allSelected: false,
|
||||||
showALLOption: true,
|
showALLOption: true,
|
||||||
|
order: 1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mockGetUrlVariables,
|
mockGetUrlVariables,
|
||||||
|
|||||||
@@ -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 }),
|
||||||
|
}),
|
||||||
|
);
|
||||||
@@ -9,9 +9,7 @@ export type WidgetColumnWidths = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export interface IDashboardContext {
|
export interface IDashboardContext {
|
||||||
isDashboardSliderOpen: boolean;
|
|
||||||
isDashboardLocked: boolean;
|
isDashboardLocked: boolean;
|
||||||
handleToggleDashboardSlider: (value: boolean) => void;
|
|
||||||
handleDashboardLockToggle: (value: boolean) => void;
|
handleDashboardLockToggle: (value: boolean) => void;
|
||||||
dashboardResponse: UseQueryResult<SuccessResponseV2<Dashboard>, unknown>;
|
dashboardResponse: UseQueryResult<SuccessResponseV2<Dashboard>, unknown>;
|
||||||
selectedDashboard: Dashboard | undefined;
|
selectedDashboard: Dashboard | undefined;
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -11,7 +11,6 @@ require (
|
|||||||
github.com/SigNoz/signoz-otel-collector v0.144.2
|
github.com/SigNoz/signoz-otel-collector v0.144.2
|
||||||
github.com/antlr4-go/antlr/v4 v4.13.1
|
github.com/antlr4-go/antlr/v4 v4.13.1
|
||||||
github.com/antonmedv/expr v1.15.3
|
github.com/antonmedv/expr v1.15.3
|
||||||
github.com/bytedance/sonic v1.14.1
|
|
||||||
github.com/cespare/xxhash/v2 v2.3.0
|
github.com/cespare/xxhash/v2 v2.3.0
|
||||||
github.com/coreos/go-oidc/v3 v3.17.0
|
github.com/coreos/go-oidc/v3 v3.17.0
|
||||||
github.com/dgraph-io/ristretto/v2 v2.3.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/aws-sdk-go-v2/service/sts v1.41.6 // indirect
|
||||||
github.com/aws/smithy-go v1.24.0 // indirect
|
github.com/aws/smithy-go v1.24.0 // indirect
|
||||||
github.com/bytedance/gopkg v0.1.3 // 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/bytedance/sonic/loader v0.3.0 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ func (m *module) ListPromotedAndIndexedPaths(ctx context.Context) ([]promotetype
|
|||||||
|
|
||||||
// add the paths that are not promoted but have indexes
|
// add the paths that are not promoted but have indexes
|
||||||
for path, indexes := range aggr {
|
for path, indexes := range aggr {
|
||||||
path := strings.TrimPrefix(path, telemetrylogs.BodyJSONColumnPrefix)
|
path := strings.TrimPrefix(path, telemetrylogs.BodyV2ColumnPrefix)
|
||||||
path = telemetrytypes.BodyJSONStringSearchPrefix + path
|
path = telemetrytypes.BodyJSONStringSearchPrefix + path
|
||||||
response = append(response, promotetypes.PromotePath{
|
response = append(response, promotetypes.PromotePath{
|
||||||
Path: path,
|
Path: path,
|
||||||
@@ -163,7 +163,7 @@ func (m *module) PromoteAndIndexPaths(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(it.Indexes) > 0 {
|
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 the path is already promoted or is being promoted, add it to the promoted column
|
||||||
if _, promoted := existingPromotedPaths[it.Path]; promoted || it.Promote {
|
if _, promoted := existingPromotedPaths[it.Path]; promoted || it.Promote {
|
||||||
parentColumn = telemetrylogs.LogsV2BodyPromotedColumn
|
parentColumn = telemetrylogs.LogsV2BodyPromotedColumn
|
||||||
|
|||||||
@@ -10,13 +10,11 @@ import (
|
|||||||
|
|
||||||
"github.com/ClickHouse/clickhouse-go/v2"
|
"github.com/ClickHouse/clickhouse-go/v2"
|
||||||
"github.com/SigNoz/signoz/pkg/errors"
|
"github.com/SigNoz/signoz/pkg/errors"
|
||||||
"github.com/SigNoz/signoz/pkg/telemetrylogs"
|
|
||||||
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
||||||
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
|
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
|
||||||
"github.com/SigNoz/signoz/pkg/types/instrumentationtypes"
|
"github.com/SigNoz/signoz/pkg/types/instrumentationtypes"
|
||||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||||
"github.com/bytedance/sonic"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type builderQuery[T any] struct {
|
type builderQuery[T any] struct {
|
||||||
@@ -262,40 +260,6 @@ func (q *builderQuery[T]) executeWithContext(ctx context.Context, query string,
|
|||||||
return nil, err
|
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{
|
return &qbtypes.Result{
|
||||||
Type: q.kind,
|
Type: q.kind,
|
||||||
Value: payload,
|
Value: payload,
|
||||||
@@ -423,18 +387,3 @@ func decodeCursor(cur string) (int64, error) {
|
|||||||
}
|
}
|
||||||
return strconv.ParseInt(string(b), 10, 64)
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import (
|
|||||||
"github.com/ClickHouse/clickhouse-go/v2/lib/driver"
|
"github.com/ClickHouse/clickhouse-go/v2/lib/driver"
|
||||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||||
"github.com/bytedance/sonic"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -394,17 +393,11 @@ func readAsRaw(rows driver.Rows, queryName string) (*qbtypes.RawData, error) {
|
|||||||
|
|
||||||
// de-reference the typed pointer to any
|
// de-reference the typed pointer to any
|
||||||
val := reflect.ValueOf(cellPtr).Elem().Interface()
|
val := reflect.ValueOf(cellPtr).Elem().Interface()
|
||||||
|
// Post-process JSON columns: normalize into String value
|
||||||
// Post-process JSON columns: normalize into structured values
|
|
||||||
if strings.HasPrefix(strings.ToUpper(colTypes[i].DatabaseTypeName()), "JSON") {
|
if strings.HasPrefix(strings.ToUpper(colTypes[i].DatabaseTypeName()), "JSON") {
|
||||||
switch x := val.(type) {
|
switch x := val.(type) {
|
||||||
case []byte:
|
case []byte:
|
||||||
if len(x) > 0 {
|
val = string(x)
|
||||||
var v any
|
|
||||||
if err := sonic.Unmarshal(x, &v); err == nil {
|
|
||||||
val = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
// already a structured type (map[string]any, []any, etc.)
|
// already a structured type (map[string]any, []any, etc.)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
// we don't have a toBoolOrNull in ClickHouse, so we need to convert the bool to a string
|
||||||
value = fmt.Sprintf("%t", v)
|
value = fmt.Sprintf("%t", v)
|
||||||
}
|
}
|
||||||
|
|
||||||
case telemetrytypes.FieldDataTypeInt64,
|
case telemetrytypes.FieldDataTypeInt64,
|
||||||
telemetrytypes.FieldDataTypeArrayInt64,
|
telemetrytypes.FieldDataTypeArrayInt64,
|
||||||
telemetrytypes.FieldDataTypeNumber,
|
telemetrytypes.FieldDataTypeNumber,
|
||||||
|
|||||||
@@ -313,37 +313,30 @@ func (v *filterExpressionVisitor) VisitPrimary(ctx *grammar.PrimaryContext) any
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
child := ctx.GetChild(0)
|
child := ctx.GetChild(0)
|
||||||
|
var searchText string
|
||||||
if keyCtx, ok := child.(*grammar.KeyContext); ok {
|
if keyCtx, ok := child.(*grammar.KeyContext); ok {
|
||||||
// create a full text search condition on the body field
|
// create a full text search condition on the body field
|
||||||
|
searchText = keyCtx.GetText()
|
||||||
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
|
|
||||||
} else if valCtx, ok := child.(*grammar.ValueContext); ok {
|
} else if valCtx, ok := child.(*grammar.ValueContext); ok {
|
||||||
var text string
|
|
||||||
if valCtx.QUOTED_TEXT() != nil {
|
if valCtx.QUOTED_TEXT() != nil {
|
||||||
text = trimQuotes(valCtx.QUOTED_TEXT().GetText())
|
searchText = trimQuotes(valCtx.QUOTED_TEXT().GetText())
|
||||||
} else if valCtx.NUMBER() != nil {
|
} else if valCtx.NUMBER() != nil {
|
||||||
text = valCtx.NUMBER().GetText()
|
searchText = valCtx.NUMBER().GetText()
|
||||||
} else if valCtx.BOOL() != nil {
|
} else if valCtx.BOOL() != nil {
|
||||||
text = valCtx.BOOL().GetText()
|
searchText = valCtx.BOOL().GetText()
|
||||||
} else if valCtx.KEY() != nil {
|
} else if valCtx.KEY() != nil {
|
||||||
text = valCtx.KEY().GetText()
|
searchText = valCtx.KEY().GetText()
|
||||||
} else {
|
} else {
|
||||||
v.errors = append(v.errors, fmt.Sprintf("unsupported value type: %s", valCtx.GetText()))
|
v.errors = append(v.errors, fmt.Sprintf("unsupported value type: %s", valCtx.GetText()))
|
||||||
return ""
|
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
|
return "" // Should not happen with valid input
|
||||||
@@ -383,6 +376,7 @@ func (v *filterExpressionVisitor) VisitComparison(ctx *grammar.ComparisonContext
|
|||||||
for _, key := range keys {
|
for _, key := range keys {
|
||||||
condition, err := v.conditionBuilder.ConditionFor(context.Background(), key, op, nil, v.builder, v.startNs, v.endNs)
|
condition, err := v.conditionBuilder.ConditionFor(context.Background(), key, op, nil, v.builder, v.startNs, v.endNs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
v.errors = append(v.errors, fmt.Sprintf("failed to build condition: %s", err.Error()))
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
conds = append(conds, condition)
|
conds = append(conds, condition)
|
||||||
@@ -648,7 +642,6 @@ func (v *filterExpressionVisitor) VisitValueList(ctx *grammar.ValueListContext)
|
|||||||
|
|
||||||
// VisitFullText handles standalone quoted strings for full-text search
|
// VisitFullText handles standalone quoted strings for full-text search
|
||||||
func (v *filterExpressionVisitor) VisitFullText(ctx *grammar.FullTextContext) any {
|
func (v *filterExpressionVisitor) VisitFullText(ctx *grammar.FullTextContext) any {
|
||||||
|
|
||||||
if v.skipFullTextFilter {
|
if v.skipFullTextFilter {
|
||||||
return ""
|
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()))
|
v.errors = append(v.errors, fmt.Sprintf("failed to build full text search condition: %s", err.Error()))
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
return cond
|
return cond
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,14 +3,12 @@ package telemetrylogs
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"slices"
|
|
||||||
|
|
||||||
schema "github.com/SigNoz/signoz-otel-collector/cmd/signozschemamigrator/schema_migrator"
|
schema "github.com/SigNoz/signoz-otel-collector/cmd/signozschemamigrator/schema_migrator"
|
||||||
"github.com/SigNoz/signoz/pkg/errors"
|
"github.com/SigNoz/signoz/pkg/errors"
|
||||||
"github.com/SigNoz/signoz/pkg/querybuilder"
|
"github.com/SigNoz/signoz/pkg/querybuilder"
|
||||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||||
"golang.org/x/exp/maps"
|
|
||||||
|
|
||||||
"github.com/huandu/go-sqlbuilder"
|
"github.com/huandu/go-sqlbuilder"
|
||||||
)
|
)
|
||||||
@@ -35,7 +33,7 @@ func (c *conditionBuilder) conditionFor(
|
|||||||
return "", err
|
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)
|
valueType, value := InferDataType(value, operator, key)
|
||||||
cond, err := NewJSONConditionBuilder(key, valueType).buildJSONCondition(operator, value, sb)
|
cond, err := NewJSONConditionBuilder(key, valueType).buildJSONCondition(operator, value, sb)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -54,14 +52,14 @@ func (c *conditionBuilder) conditionFor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if this is a body JSON search - either by FieldContext
|
// 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 = GetBodyJSONKey(ctx, key, operator, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
tblFieldName, value = querybuilder.DataTypeCollisionHandledFieldName(key, value, tblFieldName, operator)
|
tblFieldName, value = querybuilder.DataTypeCollisionHandledFieldName(key, value, tblFieldName, operator)
|
||||||
|
|
||||||
// make use of case insensitive index for body
|
// make use of case insensitive index for body
|
||||||
if tblFieldName == "body" {
|
if tblFieldName == "body" || tblFieldName == messageSubColumn {
|
||||||
switch operator {
|
switch operator {
|
||||||
case qbtypes.FilterOperatorLike:
|
case qbtypes.FilterOperatorLike:
|
||||||
return sb.ILike(tblFieldName, value), nil
|
return sb.ILike(tblFieldName, value), nil
|
||||||
@@ -108,7 +106,6 @@ func (c *conditionBuilder) conditionFor(
|
|||||||
return sb.ILike(tblFieldName, fmt.Sprintf("%%%s%%", value)), nil
|
return sb.ILike(tblFieldName, fmt.Sprintf("%%%s%%", value)), nil
|
||||||
case qbtypes.FilterOperatorNotContains:
|
case qbtypes.FilterOperatorNotContains:
|
||||||
return sb.NotILike(tblFieldName, fmt.Sprintf("%%%s%%", value)), nil
|
return sb.NotILike(tblFieldName, fmt.Sprintf("%%%s%%", value)), nil
|
||||||
|
|
||||||
case qbtypes.FilterOperatorRegexp:
|
case qbtypes.FilterOperatorRegexp:
|
||||||
// Note: Escape $$ to $$$$ to avoid sqlbuilder interpreting materialized $ signs
|
// Note: Escape $$ to $$$$ to avoid sqlbuilder interpreting materialized $ signs
|
||||||
// Only needed because we are using sprintf instead of sb.Match (not implemented in sqlbuilder)
|
// 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:
|
case schema.ColumnTypeEnumJSON:
|
||||||
if operator == qbtypes.FilterOperatorExists {
|
if operator == qbtypes.FilterOperatorExists {
|
||||||
return sb.IsNotNull(tblFieldName), nil
|
return sb.IsNotNull(tblFieldName), nil
|
||||||
} else {
|
|
||||||
return sb.IsNull(tblFieldName), nil
|
|
||||||
}
|
}
|
||||||
|
return sb.IsNull(tblFieldName), nil
|
||||||
case schema.ColumnTypeEnumLowCardinality:
|
case schema.ColumnTypeEnumLowCardinality:
|
||||||
switch elementType := column.Type.(schema.LowCardinalityColumnType).ElementType; elementType.GetType() {
|
switch elementType := column.Type.(schema.LowCardinalityColumnType).ElementType; elementType.GetType() {
|
||||||
case schema.ColumnTypeEnumString:
|
case schema.ColumnTypeEnumString:
|
||||||
@@ -247,19 +243,30 @@ func (c *conditionBuilder) ConditionFor(
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !(key.FieldContext == telemetrytypes.FieldContextBody && querybuilder.BodyJSONQueryEnabled) && operator.AddDefaultExistsFilter() {
|
// Skip adding exists filter for intrinsic fields i.e. Table level log context fields
|
||||||
// skip adding exists filter for intrinsic fields
|
buildExistCondition := operator.AddDefaultExistsFilter()
|
||||||
// with an exception for body json search
|
switch key.FieldContext {
|
||||||
field, _ := c.fm.FieldFor(ctx, key)
|
case telemetrytypes.FieldContextLog, telemetrytypes.FieldContextScope:
|
||||||
if slices.Contains(maps.Keys(IntrinsicFields), field) && key.FieldContext != telemetrytypes.FieldContextBody {
|
// 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
|
return condition, nil
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if buildExistCondition {
|
||||||
existsCondition, err := c.conditionFor(ctx, key, qbtypes.FilterOperatorExists, nil, sb)
|
existsCondition, err := c.conditionFor(ctx, key, qbtypes.FilterOperatorExists, nil, sb)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
return sb.And(condition, existsCondition), nil
|
return sb.And(condition, existsCondition), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return condition, nil
|
return condition, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,7 +127,8 @@ func TestConditionFor(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "Contains operator - body",
|
name: "Contains operator - body",
|
||||||
key: telemetrytypes.TelemetryFieldKey{
|
key: telemetrytypes.TelemetryFieldKey{
|
||||||
Name: "body",
|
Name: "body",
|
||||||
|
FieldContext: telemetrytypes.FieldContextLog,
|
||||||
},
|
},
|
||||||
operator: qbtypes.FilterOperatorContains,
|
operator: qbtypes.FilterOperatorContains,
|
||||||
value: 521509198310,
|
value: 521509198310,
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
package telemetrylogs
|
package telemetrylogs
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/SigNoz/signoz-otel-collector/constants"
|
"github.com/SigNoz/signoz-otel-collector/constants"
|
||||||
|
"github.com/SigNoz/signoz/pkg/querybuilder"
|
||||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||||
)
|
)
|
||||||
@@ -17,7 +20,7 @@ const (
|
|||||||
LogsV2TimestampColumn = "timestamp"
|
LogsV2TimestampColumn = "timestamp"
|
||||||
LogsV2ObservedTimestampColumn = "observed_timestamp"
|
LogsV2ObservedTimestampColumn = "observed_timestamp"
|
||||||
LogsV2BodyColumn = "body"
|
LogsV2BodyColumn = "body"
|
||||||
LogsV2BodyJSONColumn = constants.BodyV2Column
|
LogsV2BodyV2Column = constants.BodyV2Column
|
||||||
LogsV2BodyPromotedColumn = constants.BodyPromotedColumn
|
LogsV2BodyPromotedColumn = constants.BodyPromotedColumn
|
||||||
LogsV2TraceIDColumn = "trace_id"
|
LogsV2TraceIDColumn = "trace_id"
|
||||||
LogsV2SpanIDColumn = "span_id"
|
LogsV2SpanIDColumn = "span_id"
|
||||||
@@ -34,8 +37,14 @@ const (
|
|||||||
LogsV2ResourcesStringColumn = "resources_string"
|
LogsV2ResourcesStringColumn = "resources_string"
|
||||||
LogsV2ScopeStringColumn = "scope_string"
|
LogsV2ScopeStringColumn = "scope_string"
|
||||||
|
|
||||||
BodyJSONColumnPrefix = constants.BodyV2ColumnPrefix
|
BodyV2ColumnPrefix = constants.BodyV2ColumnPrefix
|
||||||
BodyPromotedColumnPrefix = constants.BodyPromotedColumnPrefix
|
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 (
|
var (
|
||||||
@@ -118,3 +127,11 @@ var (
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func bodyAliasExpression() string {
|
||||||
|
if !querybuilder.BodyJSONQueryEnabled {
|
||||||
|
return LogsV2BodyColumn
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s as body", LogsV2BodyV2Column)
|
||||||
|
}
|
||||||
|
|||||||
@@ -30,7 +30,8 @@ var (
|
|||||||
"severity_text": {Name: "severity_text", Type: schema.LowCardinalityColumnType{ElementType: schema.ColumnTypeString}},
|
"severity_text": {Name: "severity_text", Type: schema.LowCardinalityColumnType{ElementType: schema.ColumnTypeString}},
|
||||||
"severity_number": {Name: "severity_number", Type: schema.ColumnTypeUInt8},
|
"severity_number": {Name: "severity_number", Type: schema.ColumnTypeUInt8},
|
||||||
"body": {Name: "body", Type: schema.ColumnTypeString},
|
"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)),
|
MaxDynamicTypes: utils.ToPointer(uint(32)),
|
||||||
MaxDynamicPaths: utils.ToPointer(uint(0)),
|
MaxDynamicPaths: utils.ToPointer(uint(0)),
|
||||||
}},
|
}},
|
||||||
@@ -88,21 +89,26 @@ func (m *fieldMapper) getColumn(_ context.Context, key *telemetrytypes.Telemetry
|
|||||||
return logsV2Columns["attributes_bool"], nil
|
return logsV2Columns["attributes_bool"], nil
|
||||||
}
|
}
|
||||||
case telemetrytypes.FieldContextBody:
|
case telemetrytypes.FieldContextBody:
|
||||||
// Body context is for JSON body fields
|
// Body context is for JSON body fields. Use body_v2 if feature flag is enabled.
|
||||||
// Use body_json if feature flag is enabled
|
|
||||||
if querybuilder.BodyJSONQueryEnabled {
|
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
|
// Fall back to legacy body column
|
||||||
return logsV2Columns["body"], nil
|
return logsV2Columns["body"], nil
|
||||||
case telemetrytypes.FieldContextLog, telemetrytypes.FieldContextUnspecified:
|
case telemetrytypes.FieldContextLog, telemetrytypes.FieldContextUnspecified:
|
||||||
|
if key.Name == LogsV2BodyColumn && querybuilder.BodyJSONQueryEnabled {
|
||||||
|
return logsV2Columns[messageSubColumn], nil
|
||||||
|
}
|
||||||
col, ok := logsV2Columns[key.Name]
|
col, ok := logsV2Columns[key.Name]
|
||||||
if !ok {
|
if !ok {
|
||||||
// check if the key has body JSON search
|
// check if the key has body JSON search
|
||||||
if strings.HasPrefix(key.Name, telemetrytypes.BodyJSONStringSearchPrefix) {
|
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 {
|
if querybuilder.BodyJSONQueryEnabled {
|
||||||
return logsV2Columns[LogsV2BodyJSONColumn], nil
|
return logsV2Columns[LogsV2BodyV2Column], nil
|
||||||
}
|
}
|
||||||
// Fall back to legacy body column
|
// Fall back to legacy body column
|
||||||
return logsV2Columns["body"], nil
|
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
|
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:
|
case telemetrytypes.FieldContextBody:
|
||||||
|
if key.Name == messageSubField {
|
||||||
|
return messageSubColumn, nil
|
||||||
|
}
|
||||||
|
|
||||||
if key.JSONDataType == nil {
|
if key.JSONDataType == nil {
|
||||||
return "", qbtypes.ErrColumnNotFound
|
return "", qbtypes.ErrColumnNotFound
|
||||||
}
|
}
|
||||||
@@ -246,34 +256,37 @@ func (m *fieldMapper) buildFieldForJSON(key *telemetrytypes.TelemetryFieldKey) (
|
|||||||
node := plan[0]
|
node := plan[0]
|
||||||
|
|
||||||
expr := fmt.Sprintf("dynamicElement(%s, '%s')", node.FieldPath(), node.TerminalConfig.ElemType.StringValue())
|
expr := fmt.Sprintf("dynamicElement(%s, '%s')", node.FieldPath(), node.TerminalConfig.ElemType.StringValue())
|
||||||
if key.Materialized {
|
// TODO(Piyush): Promoted path logic commented out. Materialized now means type hint
|
||||||
if len(plan) < 2 {
|
// promotion will be extracted from key field evolution
|
||||||
return "", errors.Newf(errors.TypeUnexpected, CodePromotedPlanMissing,
|
// (direct sub-column access), not a promoted body_promoted.* column.
|
||||||
"plan length is less than 2 for promoted path: %s", key.Name)
|
// 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]
|
// node := plan[1]
|
||||||
promotedExpr := fmt.Sprintf(
|
// promotedExpr := fmt.Sprintf(
|
||||||
"dynamicElement(%s, '%s')",
|
// "dynamicElement(%s, '%s')",
|
||||||
node.FieldPath(),
|
// node.FieldPath(),
|
||||||
node.TerminalConfig.ElemType.StringValue(),
|
// node.TerminalConfig.ElemType.StringValue(),
|
||||||
)
|
// )
|
||||||
|
|
||||||
// dynamicElement returns NULL for scalar types or an empty array for array types.
|
// // dynamicElement returns NULL for scalar types or an empty array for array types.
|
||||||
if node.TerminalConfig.ElemType.IsArray {
|
// if node.TerminalConfig.ElemType.IsArray {
|
||||||
expr = fmt.Sprintf(
|
// expr = fmt.Sprintf(
|
||||||
"if(length(%s) > 0, %s, %s)",
|
// "if(length(%s) > 0, %s, %s)",
|
||||||
promotedExpr,
|
// promotedExpr,
|
||||||
promotedExpr,
|
// promotedExpr,
|
||||||
expr,
|
// expr,
|
||||||
)
|
// )
|
||||||
} else {
|
// } else {
|
||||||
// promoted column first then body_json column
|
// // promoted column first then body_json column
|
||||||
// TODO(Piyush): Change this in future for better performance
|
// // TODO(Piyush): Change this in future for better performance
|
||||||
expr = fmt.Sprintf("coalesce(%s, %s)", promotedExpr, expr)
|
// expr = fmt.Sprintf("coalesce(%s, %s)", promotedExpr, expr)
|
||||||
}
|
// }
|
||||||
|
|
||||||
}
|
// }
|
||||||
|
|
||||||
return expr, nil
|
return expr, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ func NewJSONConditionBuilder(key *telemetrytypes.TelemetryFieldKey, valueType te
|
|||||||
return &jsonConditionBuilder{key: key, valueType: telemetrytypes.MappingFieldDataTypeToJSONDataType[valueType]}
|
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) {
|
func (c *jsonConditionBuilder) buildJSONCondition(operator qbtypes.FilterOperator, value any, sb *sqlbuilder.SelectBuilder) (string, error) {
|
||||||
conditions := []string{}
|
conditions := []string{}
|
||||||
for _, node := range c.key.JSONPlan {
|
for _, node := range c.key.JSONPlan {
|
||||||
@@ -40,6 +40,7 @@ func (c *jsonConditionBuilder) buildJSONCondition(operator qbtypes.FilterOperato
|
|||||||
}
|
}
|
||||||
conditions = append(conditions, condition)
|
conditions = append(conditions, condition)
|
||||||
}
|
}
|
||||||
|
|
||||||
return sb.Or(conditions...), nil
|
return sb.Or(conditions...), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,9 +289,9 @@ func (c *jsonConditionBuilder) applyOperator(sb *sqlbuilder.SelectBuilder, field
|
|||||||
}
|
}
|
||||||
return sb.NotIn(fieldExpr, values...), nil
|
return sb.NotIn(fieldExpr, values...), nil
|
||||||
case qbtypes.FilterOperatorExists:
|
case qbtypes.FilterOperatorExists:
|
||||||
return fmt.Sprintf("%s IS NOT NULL", fieldExpr), nil
|
return sb.IsNotNull(fieldExpr), nil
|
||||||
case qbtypes.FilterOperatorNotExists:
|
case qbtypes.FilterOperatorNotExists:
|
||||||
return fmt.Sprintf("%s IS NULL", fieldExpr), nil
|
return sb.IsNull(fieldExpr), nil
|
||||||
// between and not between
|
// between and not between
|
||||||
case qbtypes.FilterOperatorBetween, qbtypes.FilterOperatorNotBetween:
|
case qbtypes.FilterOperatorBetween, qbtypes.FilterOperatorNotBetween:
|
||||||
values, ok := value.([]any)
|
values, ok := value.([]any)
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -65,7 +65,7 @@ func (b *logQueryStatementBuilder) Build(
|
|||||||
start = querybuilder.ToNanoSecs(start)
|
start = querybuilder.ToNanoSecs(start)
|
||||||
end = querybuilder.ToNanoSecs(end)
|
end = querybuilder.ToNanoSecs(end)
|
||||||
|
|
||||||
keySelectors := getKeySelectors(query)
|
keySelectors, warnings := getKeySelectors(query)
|
||||||
keys, _, err := b.metadataStore.GetKeysMulti(ctx, keySelectors)
|
keys, _, err := b.metadataStore.GetKeysMulti(ctx, keySelectors)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -76,20 +76,29 @@ func (b *logQueryStatementBuilder) Build(
|
|||||||
// Create SQL builder
|
// Create SQL builder
|
||||||
q := sqlbuilder.NewSelectBuilder()
|
q := sqlbuilder.NewSelectBuilder()
|
||||||
|
|
||||||
|
var stmt *qbtypes.Statement
|
||||||
switch requestType {
|
switch requestType {
|
||||||
case qbtypes.RequestTypeRaw, qbtypes.RequestTypeRawStream:
|
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:
|
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:
|
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 keySelectors []*telemetrytypes.FieldKeySelector
|
||||||
|
var warnings []string
|
||||||
|
|
||||||
for idx := range query.Aggregations {
|
for idx := range query.Aggregations {
|
||||||
aggExpr := query.Aggregations[idx]
|
aggExpr := query.Aggregations[idx]
|
||||||
@@ -136,7 +145,19 @@ func getKeySelectors(query qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]) []
|
|||||||
keySelectors[idx].SelectorMatchType = telemetrytypes.FieldSelectorMatchTypeExact
|
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] {
|
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 {
|
func (b *logQueryStatementBuilder) adjustKey(key *telemetrytypes.TelemetryFieldKey, keys map[string][]*telemetrytypes.TelemetryFieldKey) []string {
|
||||||
|
|
||||||
// First check if it matches with any intrinsic fields
|
// First check if it matches with any intrinsic fields
|
||||||
var intrinsicOrCalculatedField telemetrytypes.TelemetryFieldKey
|
var intrinsicOrCalculatedField telemetrytypes.TelemetryFieldKey
|
||||||
if _, ok := IntrinsicFields[key.Name]; ok {
|
if _, ok := IntrinsicFields[key.Name]; ok {
|
||||||
@@ -212,7 +232,6 @@ func (b *logQueryStatementBuilder) adjustKey(key *telemetrytypes.TelemetryFieldK
|
|||||||
}
|
}
|
||||||
|
|
||||||
return querybuilder.AdjustKey(key, keys, nil)
|
return querybuilder.AdjustKey(key, keys, nil)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildListQuery builds a query for list panel type
|
// buildListQuery builds a query for list panel type
|
||||||
@@ -249,11 +268,7 @@ func (b *logQueryStatementBuilder) buildListQuery(
|
|||||||
sb.SelectMore(LogsV2SeverityNumberColumn)
|
sb.SelectMore(LogsV2SeverityNumberColumn)
|
||||||
sb.SelectMore(LogsV2ScopeNameColumn)
|
sb.SelectMore(LogsV2ScopeNameColumn)
|
||||||
sb.SelectMore(LogsV2ScopeVersionColumn)
|
sb.SelectMore(LogsV2ScopeVersionColumn)
|
||||||
sb.SelectMore(LogsV2BodyColumn)
|
sb.SelectMore(bodyAliasExpression())
|
||||||
if querybuilder.BodyJSONQueryEnabled {
|
|
||||||
sb.SelectMore(LogsV2BodyJSONColumn)
|
|
||||||
sb.SelectMore(LogsV2BodyPromotedColumn)
|
|
||||||
}
|
|
||||||
sb.SelectMore(LogsV2AttributesStringColumn)
|
sb.SelectMore(LogsV2AttributesStringColumn)
|
||||||
sb.SelectMore(LogsV2AttributesNumberColumn)
|
sb.SelectMore(LogsV2AttributesNumberColumn)
|
||||||
sb.SelectMore(LogsV2AttributesBoolColumn)
|
sb.SelectMore(LogsV2AttributesBoolColumn)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/pkg/errors"
|
||||||
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
|
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
|
||||||
"github.com/SigNoz/signoz/pkg/querybuilder"
|
"github.com/SigNoz/signoz/pkg/querybuilder"
|
||||||
"github.com/SigNoz/signoz/pkg/querybuilder/resourcefilter"
|
"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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -27,13 +27,6 @@ func buildCompleteFieldKeyMap() map[string][]*telemetrytypes.TelemetryFieldKey {
|
|||||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"body": {
|
|
||||||
{
|
|
||||||
Name: "body",
|
|
||||||
FieldContext: telemetrytypes.FieldContextLog,
|
|
||||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"http.status_code": {
|
"http.status_code": {
|
||||||
{
|
{
|
||||||
Name: "http.status_code",
|
Name: "http.status_code",
|
||||||
@@ -938,6 +931,13 @@ func buildCompleteFieldKeyMap() map[string][]*telemetrytypes.TelemetryFieldKey {
|
|||||||
Materialized: true,
|
Materialized: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"body": {
|
||||||
|
{
|
||||||
|
Name: "body",
|
||||||
|
FieldContext: telemetrytypes.FieldContextLog,
|
||||||
|
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, keys := range keysMap {
|
for _, keys := range keysMap {
|
||||||
@@ -945,6 +945,7 @@ func buildCompleteFieldKeyMap() map[string][]*telemetrytypes.TelemetryFieldKey {
|
|||||||
key.Signal = telemetrytypes.SignalLogs
|
key.Signal = telemetrytypes.SignalLogs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return keysMap
|
return keysMap
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ func (t *telemetryMetaStore) fetchBodyJSONPaths(ctx context.Context,
|
|||||||
instrumentationtypes.CodeNamespace: "metadata",
|
instrumentationtypes.CodeNamespace: "metadata",
|
||||||
instrumentationtypes.CodeFunctionName: "fetchBodyJSONPaths",
|
instrumentationtypes.CodeFunctionName: "fetchBodyJSONPaths",
|
||||||
})
|
})
|
||||||
|
|
||||||
query, args, limit := buildGetBodyJSONPathsQuery(fieldKeySelectors)
|
query, args, limit := buildGetBodyJSONPathsQuery(fieldKeySelectors)
|
||||||
rows, err := t.telemetrystore.ClickhouseDB().Query(ctx, query, args...)
|
rows, err := t.telemetrystore.ClickhouseDB().Query(ctx, query, args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -184,7 +185,6 @@ func buildGetBodyJSONPathsQuery(fieldKeySelectors []*telemetrytypes.FieldKeySele
|
|||||||
limit += fieldKeySelector.Limit
|
limit += fieldKeySelector.Limit
|
||||||
}
|
}
|
||||||
sb.Where(sb.Or(orClauses...))
|
sb.Where(sb.Or(orClauses...))
|
||||||
|
|
||||||
// Group by path to get unique paths with aggregated types
|
// Group by path to get unique paths with aggregated types
|
||||||
sb.GroupBy("path")
|
sb.GroupBy("path")
|
||||||
|
|
||||||
@@ -319,7 +319,7 @@ func (t *telemetryMetaStore) ListJSONValues(ctx context.Context, path string, li
|
|||||||
if promoted {
|
if promoted {
|
||||||
path = telemetrylogs.BodyPromotedColumnPrefix + path
|
path = telemetrylogs.BodyPromotedColumnPrefix + path
|
||||||
} else {
|
} else {
|
||||||
path = telemetrylogs.BodyJSONColumnPrefix + path
|
path = telemetrylogs.BodyV2ColumnPrefix + path
|
||||||
}
|
}
|
||||||
|
|
||||||
from := fmt.Sprintf("%s.%s", telemetrylogs.DBName, telemetrylogs.LogsV2TableName)
|
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
|
// TODO(Piyush): Remove this function
|
||||||
func CleanPathPrefixes(path string) string {
|
func CleanPathPrefixes(path string) string {
|
||||||
path = strings.TrimPrefix(path, telemetrytypes.BodyJSONStringSearchPrefix)
|
path = strings.TrimPrefix(path, telemetrytypes.BodyJSONStringSearchPrefix)
|
||||||
path = strings.TrimPrefix(path, telemetrylogs.BodyJSONColumnPrefix)
|
path = strings.TrimPrefix(path, telemetrylogs.BodyV2ColumnPrefix)
|
||||||
path = strings.TrimPrefix(path, telemetrylogs.BodyPromotedColumnPrefix)
|
path = strings.TrimPrefix(path, telemetrylogs.BodyPromotedColumnPrefix)
|
||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ func NewTelemetryMetaStore(
|
|||||||
jsonColumnMetadata: map[telemetrytypes.Signal]map[telemetrytypes.FieldContext]telemetrytypes.JSONColumnMetadata{
|
jsonColumnMetadata: map[telemetrytypes.Signal]map[telemetrytypes.FieldContext]telemetrytypes.JSONColumnMetadata{
|
||||||
telemetrytypes.SignalLogs: {
|
telemetrytypes.SignalLogs: {
|
||||||
telemetrytypes.FieldContextBody: telemetrytypes.JSONColumnMetadata{
|
telemetrytypes.FieldContextBody: telemetrytypes.JSONColumnMetadata{
|
||||||
BaseColumn: telemetrylogs.LogsV2BodyJSONColumn,
|
BaseColumn: telemetrylogs.LogsV2BodyV2Column,
|
||||||
PromotedColumn: telemetrylogs.LogsV2BodyPromotedColumn,
|
PromotedColumn: telemetrylogs.LogsV2BodyPromotedColumn,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ type JSONAccessNode struct {
|
|||||||
// Node information
|
// Node information
|
||||||
Name string
|
Name string
|
||||||
IsTerminal bool
|
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)
|
// Precomputed type information (single source of truth)
|
||||||
AvailableTypes []JSONDataType
|
AvailableTypes []JSONDataType
|
||||||
|
|||||||
@@ -1,12 +1,19 @@
|
|||||||
package telemetrytypes
|
package telemetrytypes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
otelconstants "github.com/SigNoz/signoz-otel-collector/constants"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
bodyV2Column = otelconstants.BodyV2Column
|
||||||
|
bodyPromotedColumn = otelconstants.BodyPromotedColumn
|
||||||
|
)
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Helper Functions for Test Data Creation
|
// Helper Functions for Test Data Creation
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -109,8 +116,8 @@ func TestNode_Alias(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Root node returns name as-is",
|
name: "Root node returns name as-is",
|
||||||
node: NewRootJSONAccessNode("body_json", 32, 0),
|
node: NewRootJSONAccessNode(bodyV2Column, 32, 0),
|
||||||
expected: "body_json",
|
expected: bodyV2Column,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Node without parent returns backticked name",
|
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",
|
name: "Node with root parent uses dot separator",
|
||||||
node: &JSONAccessNode{
|
node: &JSONAccessNode{
|
||||||
Name: "age",
|
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",
|
name: "Node with non-root parent uses array separator",
|
||||||
@@ -134,10 +141,10 @@ func TestNode_Alias(t *testing.T) {
|
|||||||
Name: "name",
|
Name: "name",
|
||||||
Parent: &JSONAccessNode{
|
Parent: &JSONAccessNode{
|
||||||
Name: "education",
|
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",
|
name: "Nested array path with multiple levels",
|
||||||
@@ -147,11 +154,11 @@ func TestNode_Alias(t *testing.T) {
|
|||||||
Name: "awards",
|
Name: "awards",
|
||||||
Parent: &JSONAccessNode{
|
Parent: &JSONAccessNode{
|
||||||
Name: "education",
|
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",
|
name: "Simple field path from root",
|
||||||
node: &JSONAccessNode{
|
node: &JSONAccessNode{
|
||||||
Name: "user",
|
Name: "user",
|
||||||
Parent: NewRootJSONAccessNode("body_json", 32, 0),
|
Parent: NewRootJSONAccessNode(bodyV2Column, 32, 0),
|
||||||
},
|
},
|
||||||
// FieldPath() always wraps the field name in backticks
|
// FieldPath() always wraps the field name in backticks
|
||||||
expected: "body_json" + ".`user`",
|
expected: bodyV2Column + ".`user`",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Field path with backtick-required key",
|
name: "Field path with backtick-required key",
|
||||||
node: &JSONAccessNode{
|
node: &JSONAccessNode{
|
||||||
Name: "user-name", // requires backtick
|
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",
|
name: "Nested field path",
|
||||||
@@ -192,11 +199,11 @@ func TestNode_FieldPath(t *testing.T) {
|
|||||||
Name: "age",
|
Name: "age",
|
||||||
Parent: &JSONAccessNode{
|
Parent: &JSONAccessNode{
|
||||||
Name: "user",
|
Name: "user",
|
||||||
Parent: NewRootJSONAccessNode("body_json", 32, 0),
|
Parent: NewRootJSONAccessNode(bodyV2Column, 32, 0),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// FieldPath() always wraps the field name in backticks
|
// FieldPath() always wraps the field name in backticks
|
||||||
expected: "`" + "body_json" + ".user`.`age`",
|
expected: "`" + bodyV2Column + ".user`.`age`",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Array element field path",
|
name: "Array element field path",
|
||||||
@@ -204,11 +211,11 @@ func TestNode_FieldPath(t *testing.T) {
|
|||||||
Name: "name",
|
Name: "name",
|
||||||
Parent: &JSONAccessNode{
|
Parent: &JSONAccessNode{
|
||||||
Name: "education",
|
Name: "education",
|
||||||
Parent: NewRootJSONAccessNode("body_json", 32, 0),
|
Parent: NewRootJSONAccessNode(bodyV2Column, 32, 0),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// FieldPath() always wraps the field name in backticks
|
// 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",
|
name: "Simple path not promoted",
|
||||||
key: makeKey("user.name", String, false),
|
key: makeKey("user.name", String, false),
|
||||||
expectedYAML: `
|
expectedYAML: fmt.Sprintf(`
|
||||||
- name: user.name
|
- name: user.name
|
||||||
column: body_json
|
column: %s
|
||||||
availableTypes:
|
availableTypes:
|
||||||
- String
|
- String
|
||||||
maxDynamicTypes: 16
|
maxDynamicTypes: 16
|
||||||
isTerminal: true
|
isTerminal: true
|
||||||
elemType: String
|
elemType: String
|
||||||
`,
|
`, bodyV2Column),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Simple path promoted",
|
name: "Simple path promoted",
|
||||||
key: makeKey("user.name", String, true),
|
key: makeKey("user.name", String, true),
|
||||||
expectedYAML: `
|
expectedYAML: fmt.Sprintf(`
|
||||||
- name: user.name
|
- name: user.name
|
||||||
column: body_json
|
column: %s
|
||||||
availableTypes:
|
availableTypes:
|
||||||
- String
|
- String
|
||||||
maxDynamicTypes: 16
|
maxDynamicTypes: 16
|
||||||
isTerminal: true
|
isTerminal: true
|
||||||
elemType: String
|
elemType: String
|
||||||
- name: user.name
|
- name: user.name
|
||||||
column: body_json_promoted
|
column: %s
|
||||||
availableTypes:
|
availableTypes:
|
||||||
- String
|
- String
|
||||||
maxDynamicTypes: 16
|
maxDynamicTypes: 16
|
||||||
maxDynamicPaths: 256
|
maxDynamicPaths: 256
|
||||||
isTerminal: true
|
isTerminal: true
|
||||||
elemType: String
|
elemType: String
|
||||||
`,
|
`, bodyV2Column, bodyPromotedColumn),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Empty path returns error",
|
name: "Empty path returns error",
|
||||||
@@ -278,8 +285,8 @@ func TestPlanJSON_BasicStructure(t *testing.T) {
|
|||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
err := tt.key.SetJSONAccessPlan(JSONColumnMetadata{
|
err := tt.key.SetJSONAccessPlan(JSONColumnMetadata{
|
||||||
BaseColumn: "body_json",
|
BaseColumn: bodyV2Column,
|
||||||
PromotedColumn: "body_json_promoted",
|
PromotedColumn: bodyPromotedColumn,
|
||||||
}, types)
|
}, types)
|
||||||
if tt.expectErr {
|
if tt.expectErr {
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
@@ -304,9 +311,9 @@ func TestPlanJSON_ArrayPaths(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "Single array level - JSON branch only",
|
name: "Single array level - JSON branch only",
|
||||||
path: "education[].name",
|
path: "education[].name",
|
||||||
expectedYAML: `
|
expectedYAML: fmt.Sprintf(`
|
||||||
- name: education
|
- name: education
|
||||||
column: body_json
|
column: %s
|
||||||
availableTypes:
|
availableTypes:
|
||||||
- Array(JSON)
|
- Array(JSON)
|
||||||
maxDynamicTypes: 16
|
maxDynamicTypes: 16
|
||||||
@@ -318,14 +325,14 @@ func TestPlanJSON_ArrayPaths(t *testing.T) {
|
|||||||
maxDynamicTypes: 8
|
maxDynamicTypes: 8
|
||||||
isTerminal: true
|
isTerminal: true
|
||||||
elemType: String
|
elemType: String
|
||||||
`,
|
`, bodyV2Column),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Single array level - both JSON and Dynamic branches",
|
name: "Single array level - both JSON and Dynamic branches",
|
||||||
path: "education[].awards[].type",
|
path: "education[].awards[].type",
|
||||||
expectedYAML: `
|
expectedYAML: fmt.Sprintf(`
|
||||||
- name: education
|
- name: education
|
||||||
column: body_json
|
column: %s
|
||||||
availableTypes:
|
availableTypes:
|
||||||
- Array(JSON)
|
- Array(JSON)
|
||||||
maxDynamicTypes: 16
|
maxDynamicTypes: 16
|
||||||
@@ -352,14 +359,14 @@ func TestPlanJSON_ArrayPaths(t *testing.T) {
|
|||||||
maxDynamicPaths: 256
|
maxDynamicPaths: 256
|
||||||
isTerminal: true
|
isTerminal: true
|
||||||
elemType: String
|
elemType: String
|
||||||
`,
|
`, bodyV2Column),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Deeply nested array path",
|
name: "Deeply nested array path",
|
||||||
path: "interests[].entities[].reviews[].entries[].metadata[].positions[].name",
|
path: "interests[].entities[].reviews[].entries[].metadata[].positions[].name",
|
||||||
expectedYAML: `
|
expectedYAML: fmt.Sprintf(`
|
||||||
- name: interests
|
- name: interests
|
||||||
column: body_json
|
column: %s
|
||||||
availableTypes:
|
availableTypes:
|
||||||
- Array(JSON)
|
- Array(JSON)
|
||||||
maxDynamicTypes: 16
|
maxDynamicTypes: 16
|
||||||
@@ -399,14 +406,14 @@ func TestPlanJSON_ArrayPaths(t *testing.T) {
|
|||||||
- String
|
- String
|
||||||
isTerminal: true
|
isTerminal: true
|
||||||
elemType: String
|
elemType: String
|
||||||
`,
|
`, bodyV2Column),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "ArrayAnyIndex replacement [*] to []",
|
name: "ArrayAnyIndex replacement [*] to []",
|
||||||
path: "education[*].name",
|
path: "education[*].name",
|
||||||
expectedYAML: `
|
expectedYAML: fmt.Sprintf(`
|
||||||
- name: education
|
- name: education
|
||||||
column: body_json
|
column: %s
|
||||||
availableTypes:
|
availableTypes:
|
||||||
- Array(JSON)
|
- Array(JSON)
|
||||||
maxDynamicTypes: 16
|
maxDynamicTypes: 16
|
||||||
@@ -418,7 +425,7 @@ func TestPlanJSON_ArrayPaths(t *testing.T) {
|
|||||||
maxDynamicTypes: 8
|
maxDynamicTypes: 8
|
||||||
isTerminal: true
|
isTerminal: true
|
||||||
elemType: String
|
elemType: String
|
||||||
`,
|
`, bodyV2Column),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -426,8 +433,8 @@ func TestPlanJSON_ArrayPaths(t *testing.T) {
|
|||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
key := makeKey(tt.path, String, false)
|
key := makeKey(tt.path, String, false)
|
||||||
err := key.SetJSONAccessPlan(JSONColumnMetadata{
|
err := key.SetJSONAccessPlan(JSONColumnMetadata{
|
||||||
BaseColumn: "body_json",
|
BaseColumn: bodyV2Column,
|
||||||
PromotedColumn: "body_json_promoted",
|
PromotedColumn: bodyPromotedColumn,
|
||||||
}, types)
|
}, types)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, key.JSONPlan)
|
require.NotNil(t, key.JSONPlan)
|
||||||
@@ -445,15 +452,15 @@ func TestPlanJSON_PromotedVsNonPromoted(t *testing.T) {
|
|||||||
t.Run("Non-promoted plan", func(t *testing.T) {
|
t.Run("Non-promoted plan", func(t *testing.T) {
|
||||||
key := makeKey(path, String, false)
|
key := makeKey(path, String, false)
|
||||||
err := key.SetJSONAccessPlan(JSONColumnMetadata{
|
err := key.SetJSONAccessPlan(JSONColumnMetadata{
|
||||||
BaseColumn: "body_json",
|
BaseColumn: bodyV2Column,
|
||||||
PromotedColumn: "body_json_promoted",
|
PromotedColumn: bodyPromotedColumn,
|
||||||
}, types)
|
}, types)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, key.JSONPlan, 1)
|
require.Len(t, key.JSONPlan, 1)
|
||||||
|
|
||||||
expectedYAML := `
|
expectedYAML := fmt.Sprintf(`
|
||||||
- name: education
|
- name: education
|
||||||
column: body_json
|
column: %s
|
||||||
availableTypes:
|
availableTypes:
|
||||||
- Array(JSON)
|
- Array(JSON)
|
||||||
maxDynamicTypes: 16
|
maxDynamicTypes: 16
|
||||||
@@ -480,7 +487,7 @@ func TestPlanJSON_PromotedVsNonPromoted(t *testing.T) {
|
|||||||
maxDynamicPaths: 256
|
maxDynamicPaths: 256
|
||||||
isTerminal: true
|
isTerminal: true
|
||||||
elemType: String
|
elemType: String
|
||||||
`
|
`, bodyV2Column)
|
||||||
got := plansToYAML(t, key.JSONPlan)
|
got := plansToYAML(t, key.JSONPlan)
|
||||||
require.YAMLEq(t, expectedYAML, got)
|
require.YAMLEq(t, expectedYAML, got)
|
||||||
})
|
})
|
||||||
@@ -488,15 +495,15 @@ func TestPlanJSON_PromotedVsNonPromoted(t *testing.T) {
|
|||||||
t.Run("Promoted plan", func(t *testing.T) {
|
t.Run("Promoted plan", func(t *testing.T) {
|
||||||
key := makeKey(path, String, true)
|
key := makeKey(path, String, true)
|
||||||
err := key.SetJSONAccessPlan(JSONColumnMetadata{
|
err := key.SetJSONAccessPlan(JSONColumnMetadata{
|
||||||
BaseColumn: "body_json",
|
BaseColumn: bodyV2Column,
|
||||||
PromotedColumn: "body_json_promoted",
|
PromotedColumn: bodyPromotedColumn,
|
||||||
}, types)
|
}, types)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, key.JSONPlan, 2)
|
require.Len(t, key.JSONPlan, 2)
|
||||||
|
|
||||||
expectedYAML := `
|
expectedYAML := fmt.Sprintf(`
|
||||||
- name: education
|
- name: education
|
||||||
column: body_json
|
column: %s
|
||||||
availableTypes:
|
availableTypes:
|
||||||
- Array(JSON)
|
- Array(JSON)
|
||||||
maxDynamicTypes: 16
|
maxDynamicTypes: 16
|
||||||
@@ -524,7 +531,7 @@ func TestPlanJSON_PromotedVsNonPromoted(t *testing.T) {
|
|||||||
isTerminal: true
|
isTerminal: true
|
||||||
elemType: String
|
elemType: String
|
||||||
- name: education
|
- name: education
|
||||||
column: body_json_promoted
|
column: %s
|
||||||
availableTypes:
|
availableTypes:
|
||||||
- Array(JSON)
|
- Array(JSON)
|
||||||
maxDynamicTypes: 16
|
maxDynamicTypes: 16
|
||||||
@@ -554,7 +561,7 @@ func TestPlanJSON_PromotedVsNonPromoted(t *testing.T) {
|
|||||||
maxDynamicPaths: 256
|
maxDynamicPaths: 256
|
||||||
isTerminal: true
|
isTerminal: true
|
||||||
elemType: String
|
elemType: String
|
||||||
`
|
`, bodyV2Column, bodyPromotedColumn)
|
||||||
got := plansToYAML(t, key.JSONPlan)
|
got := plansToYAML(t, key.JSONPlan)
|
||||||
require.YAMLEq(t, expectedYAML, got)
|
require.YAMLEq(t, expectedYAML, got)
|
||||||
})
|
})
|
||||||
@@ -575,11 +582,11 @@ func TestPlanJSON_EdgeCases(t *testing.T) {
|
|||||||
expectErr: true,
|
expectErr: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Very deep nesting - validates progression doesn't go negative",
|
name: "Very deep nesting - validates progression doesn't go negative",
|
||||||
path: "interests[].entities[].reviews[].entries[].metadata[].positions[].name",
|
path: "interests[].entities[].reviews[].entries[].metadata[].positions[].name",
|
||||||
expectedYAML: `
|
expectedYAML: fmt.Sprintf(`
|
||||||
- name: interests
|
- name: interests
|
||||||
column: body_json
|
column: %s
|
||||||
availableTypes:
|
availableTypes:
|
||||||
- Array(JSON)
|
- Array(JSON)
|
||||||
maxDynamicTypes: 16
|
maxDynamicTypes: 16
|
||||||
@@ -619,14 +626,14 @@ func TestPlanJSON_EdgeCases(t *testing.T) {
|
|||||||
- String
|
- String
|
||||||
isTerminal: true
|
isTerminal: true
|
||||||
elemType: String
|
elemType: String
|
||||||
`,
|
`, bodyV2Column),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Path with mixed scalar and array types",
|
name: "Path with mixed scalar and array types",
|
||||||
path: "education[].type",
|
path: "education[].type",
|
||||||
expectedYAML: `
|
expectedYAML: fmt.Sprintf(`
|
||||||
- name: education
|
- name: education
|
||||||
column: body_json
|
column: %s
|
||||||
availableTypes:
|
availableTypes:
|
||||||
- Array(JSON)
|
- Array(JSON)
|
||||||
maxDynamicTypes: 16
|
maxDynamicTypes: 16
|
||||||
@@ -639,20 +646,20 @@ func TestPlanJSON_EdgeCases(t *testing.T) {
|
|||||||
maxDynamicTypes: 8
|
maxDynamicTypes: 8
|
||||||
isTerminal: true
|
isTerminal: true
|
||||||
elemType: String
|
elemType: String
|
||||||
`,
|
`, bodyV2Column),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Exists with only array types available",
|
name: "Exists with only array types available",
|
||||||
path: "education",
|
path: "education",
|
||||||
expectedYAML: `
|
expectedYAML: fmt.Sprintf(`
|
||||||
- name: education
|
- name: education
|
||||||
column: body_json
|
column: %s
|
||||||
availableTypes:
|
availableTypes:
|
||||||
- Array(JSON)
|
- Array(JSON)
|
||||||
maxDynamicTypes: 16
|
maxDynamicTypes: 16
|
||||||
isTerminal: true
|
isTerminal: true
|
||||||
elemType: Array(JSON)
|
elemType: Array(JSON)
|
||||||
`,
|
`, bodyV2Column),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -668,8 +675,8 @@ func TestPlanJSON_EdgeCases(t *testing.T) {
|
|||||||
}
|
}
|
||||||
key := makeKey(tt.path, keyType, false)
|
key := makeKey(tt.path, keyType, false)
|
||||||
err := key.SetJSONAccessPlan(JSONColumnMetadata{
|
err := key.SetJSONAccessPlan(JSONColumnMetadata{
|
||||||
BaseColumn: "body_json",
|
BaseColumn: bodyV2Column,
|
||||||
PromotedColumn: "body_json_promoted",
|
PromotedColumn: bodyPromotedColumn,
|
||||||
}, types)
|
}, types)
|
||||||
if tt.expectErr {
|
if tt.expectErr {
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
@@ -687,15 +694,15 @@ func TestPlanJSON_TreeStructure(t *testing.T) {
|
|||||||
path := "education[].awards[].participated[].team[].branch"
|
path := "education[].awards[].participated[].team[].branch"
|
||||||
key := makeKey(path, String, false)
|
key := makeKey(path, String, false)
|
||||||
err := key.SetJSONAccessPlan(JSONColumnMetadata{
|
err := key.SetJSONAccessPlan(JSONColumnMetadata{
|
||||||
BaseColumn: "body_json",
|
BaseColumn: bodyV2Column,
|
||||||
PromotedColumn: "body_json_promoted",
|
PromotedColumn: bodyPromotedColumn,
|
||||||
}, types)
|
}, types)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, key.JSONPlan, 1)
|
require.Len(t, key.JSONPlan, 1)
|
||||||
|
|
||||||
expectedYAML := `
|
expectedYAML := fmt.Sprintf(`
|
||||||
- name: education
|
- name: education
|
||||||
column: body_json
|
column: %s
|
||||||
availableTypes:
|
availableTypes:
|
||||||
- Array(JSON)
|
- Array(JSON)
|
||||||
maxDynamicTypes: 16
|
maxDynamicTypes: 16
|
||||||
@@ -780,7 +787,7 @@ func TestPlanJSON_TreeStructure(t *testing.T) {
|
|||||||
maxDynamicPaths: 64
|
maxDynamicPaths: 64
|
||||||
isTerminal: true
|
isTerminal: true
|
||||||
elemType: String
|
elemType: String
|
||||||
`
|
`, bodyV2Column)
|
||||||
|
|
||||||
got := plansToYAML(t, key.JSONPlan)
|
got := plansToYAML(t, key.JSONPlan)
|
||||||
require.YAMLEq(t, expectedYAML, got)
|
require.YAMLEq(t, expectedYAML, got)
|
||||||
|
|||||||
Reference in New Issue
Block a user