mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-09-20 16:03:13 +08:00
commit
16502feaad
8
.github/workflows/push.yaml
vendored
8
.github/workflows/push.yaml
vendored
@ -34,7 +34,7 @@ jobs:
|
|||||||
id: short-sha
|
id: short-sha
|
||||||
- name: Get branch name
|
- name: Get branch name
|
||||||
id: branch-name
|
id: branch-name
|
||||||
uses: tj-actions/branch-names@v5.1
|
uses: tj-actions/branch-names@v7.0.7
|
||||||
- name: Set docker tag environment
|
- name: Set docker tag environment
|
||||||
run: |
|
run: |
|
||||||
if [ '${{ steps.branch-name.outputs.is_tag }}' == 'true' ]; then
|
if [ '${{ steps.branch-name.outputs.is_tag }}' == 'true' ]; then
|
||||||
@ -78,7 +78,7 @@ jobs:
|
|||||||
id: short-sha
|
id: short-sha
|
||||||
- name: Get branch name
|
- name: Get branch name
|
||||||
id: branch-name
|
id: branch-name
|
||||||
uses: tj-actions/branch-names@v5.1
|
uses: tj-actions/branch-names@v7.0.7
|
||||||
- name: Set docker tag environment
|
- name: Set docker tag environment
|
||||||
run: |
|
run: |
|
||||||
if [ '${{ steps.branch-name.outputs.is_tag }}' == 'true' ]; then
|
if [ '${{ steps.branch-name.outputs.is_tag }}' == 'true' ]; then
|
||||||
@ -127,7 +127,7 @@ jobs:
|
|||||||
id: short-sha
|
id: short-sha
|
||||||
- name: Get branch name
|
- name: Get branch name
|
||||||
id: branch-name
|
id: branch-name
|
||||||
uses: tj-actions/branch-names@v5.1
|
uses: tj-actions/branch-names@v7.0.7
|
||||||
- name: Set docker tag environment
|
- name: Set docker tag environment
|
||||||
run: |
|
run: |
|
||||||
if [ '${{ steps.branch-name.outputs.is_tag }}' == 'true' ]; then
|
if [ '${{ steps.branch-name.outputs.is_tag }}' == 'true' ]; then
|
||||||
@ -176,7 +176,7 @@ jobs:
|
|||||||
id: short-sha
|
id: short-sha
|
||||||
- name: Get branch name
|
- name: Get branch name
|
||||||
id: branch-name
|
id: branch-name
|
||||||
uses: tj-actions/branch-names@v5.1
|
uses: tj-actions/branch-names@v7.0.7
|
||||||
- name: Set docker tag environment
|
- name: Set docker tag environment
|
||||||
run: |
|
run: |
|
||||||
if [ '${{ steps.branch-name.outputs.is_tag }}' == 'true' ]; then
|
if [ '${{ steps.branch-name.outputs.is_tag }}' == 'true' ]; then
|
||||||
|
2
.github/workflows/staging-deployment.yaml
vendored
2
.github/workflows/staging-deployment.yaml
vendored
@ -29,7 +29,7 @@ jobs:
|
|||||||
export PATH="/usr/local/go/bin/:$PATH" # needed for Golang to work
|
export PATH="/usr/local/go/bin/:$PATH" # needed for Golang to work
|
||||||
docker system prune --force
|
docker system prune --force
|
||||||
docker pull signoz/signoz-otel-collector:main
|
docker pull signoz/signoz-otel-collector:main
|
||||||
docker pull signoz/signoz/signoz-schema-migrator:main
|
docker pull signoz/signoz-schema-migrator:main
|
||||||
cd ~/signoz
|
cd ~/signoz
|
||||||
git status
|
git status
|
||||||
git add .
|
git add .
|
||||||
|
@ -146,7 +146,7 @@ services:
|
|||||||
condition: on-failure
|
condition: on-failure
|
||||||
|
|
||||||
query-service:
|
query-service:
|
||||||
image: signoz/query-service:0.34.4
|
image: signoz/query-service:0.35.0
|
||||||
command:
|
command:
|
||||||
[
|
[
|
||||||
"-config=/root/config/prometheus.yml",
|
"-config=/root/config/prometheus.yml",
|
||||||
@ -186,7 +186,7 @@ services:
|
|||||||
<<: *db-depend
|
<<: *db-depend
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
image: signoz/frontend:0.34.4
|
image: signoz/frontend:0.35.0
|
||||||
deploy:
|
deploy:
|
||||||
restart_policy:
|
restart_policy:
|
||||||
condition: on-failure
|
condition: on-failure
|
||||||
|
@ -164,7 +164,7 @@ services:
|
|||||||
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
|
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
|
||||||
|
|
||||||
query-service:
|
query-service:
|
||||||
image: signoz/query-service:${DOCKER_TAG:-0.34.4}
|
image: signoz/query-service:${DOCKER_TAG:-0.35.0}
|
||||||
container_name: signoz-query-service
|
container_name: signoz-query-service
|
||||||
command:
|
command:
|
||||||
[
|
[
|
||||||
@ -203,7 +203,7 @@ services:
|
|||||||
<<: *db-depend
|
<<: *db-depend
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
image: signoz/frontend:${DOCKER_TAG:-0.34.4}
|
image: signoz/frontend:${DOCKER_TAG:-0.35.0}
|
||||||
container_name: signoz-frontend
|
container_name: signoz-frontend
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
depends_on:
|
depends_on:
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# use a minimal alpine image
|
# use a minimal alpine image
|
||||||
FROM alpine:3.18.3
|
FROM alpine:3.18.5
|
||||||
|
|
||||||
# Add Maintainer Info
|
# Add Maintainer Info
|
||||||
LABEL maintainer="signoz"
|
LABEL maintainer="signoz"
|
||||||
|
@ -49,7 +49,8 @@ export const Onboarding = Loadable(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const DashboardPage = Loadable(
|
export const DashboardPage = Loadable(
|
||||||
() => import(/* webpackChunkName: "DashboardPage" */ 'pages/Dashboard'),
|
() =>
|
||||||
|
import(/* webpackChunkName: "DashboardPage" */ 'pages/DashboardsListPage'),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const NewDashboardPage = Loadable(
|
export const NewDashboardPage = Loadable(
|
||||||
|
@ -4,7 +4,7 @@ import { AxiosError } from 'axios';
|
|||||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
import { PayloadProps, Props } from 'types/api/dashboard/update';
|
import { PayloadProps, Props } from 'types/api/dashboard/update';
|
||||||
|
|
||||||
const update = async (
|
const updateDashboard = async (
|
||||||
props: Props,
|
props: Props,
|
||||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||||
try {
|
try {
|
||||||
@ -23,4 +23,4 @@ const update = async (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default update;
|
export default updateDashboard;
|
||||||
|
@ -0,0 +1,30 @@
|
|||||||
|
import { ApiV2Instance as axios } from 'api';
|
||||||
|
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
|
import {
|
||||||
|
Props,
|
||||||
|
VariableResponseProps,
|
||||||
|
} from 'types/api/dashboard/variables/query';
|
||||||
|
|
||||||
|
const dashboardVariablesQuery = async (
|
||||||
|
props: Props,
|
||||||
|
): Promise<SuccessResponse<VariableResponseProps> | ErrorResponse> => {
|
||||||
|
try {
|
||||||
|
const response = await axios.post(`/variables/query`, props);
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
error: null,
|
||||||
|
message: response.data.status,
|
||||||
|
payload: response.data.data,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
const formattedError = ErrorResponseHandler(error as AxiosError);
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-throw-literal
|
||||||
|
throw { message: 'Error fetching data', details: formattedError };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default dashboardVariablesQuery;
|
@ -1,24 +0,0 @@
|
|||||||
import { ApiV2Instance as axios } from 'api';
|
|
||||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
|
||||||
import { AxiosError } from 'axios';
|
|
||||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
|
||||||
import { PayloadProps, Props } from 'types/api/dashboard/variables/query';
|
|
||||||
|
|
||||||
const query = async (
|
|
||||||
props: Props,
|
|
||||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
|
||||||
try {
|
|
||||||
const response = await axios.post(`/variables/query`, props);
|
|
||||||
|
|
||||||
return {
|
|
||||||
statusCode: 200,
|
|
||||||
error: null,
|
|
||||||
message: response.data.status,
|
|
||||||
payload: response.data.data,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return ErrorResponseHandler(error as AxiosError);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default query;
|
|
@ -153,7 +153,7 @@ export const deleteViewHandler = ({
|
|||||||
if (viewId === viewKey) {
|
if (viewId === viewKey) {
|
||||||
redirectWithQueryBuilderData(
|
redirectWithQueryBuilderData(
|
||||||
updateAllQueriesOperators(
|
updateAllQueriesOperators(
|
||||||
initialQueriesMap.traces,
|
initialQueriesMap[sourcePage],
|
||||||
panelType || PANEL_TYPES.LIST,
|
panelType || PANEL_TYPES.LIST,
|
||||||
sourcePage,
|
sourcePage,
|
||||||
),
|
),
|
||||||
|
@ -27,4 +27,5 @@ export enum QueryParams {
|
|||||||
viewName = 'viewName',
|
viewName = 'viewName',
|
||||||
viewKey = 'viewKey',
|
viewKey = 'viewKey',
|
||||||
expandedWidgetId = 'expandedWidgetId',
|
expandedWidgetId = 'expandedWidgetId',
|
||||||
|
pagination = 'pagination',
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ import { InfoCircleOutlined } from '@ant-design/icons';
|
|||||||
import Spinner from 'components/Spinner';
|
import Spinner from 'components/Spinner';
|
||||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import GridPanelSwitch from 'container/GridPanelSwitch';
|
import GridPanelSwitch from 'container/GridPanelSwitch';
|
||||||
|
import { getFormatNameByOptionId } from 'container/NewWidget/RightContainer/alertFomatCategories';
|
||||||
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
|
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
|
||||||
import { Time } from 'container/TopNav/DateTimeSelection/config';
|
import { Time } from 'container/TopNav/DateTimeSelection/config';
|
||||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||||
@ -19,7 +20,7 @@ import { EQueryType } from 'types/common/dashboard';
|
|||||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
|
|
||||||
import { ChartContainer, FailedMessageContainer } from './styles';
|
import { ChartContainer, FailedMessageContainer } from './styles';
|
||||||
import { covertIntoDataFormats } from './utils';
|
import { getThresholdLabel } from './utils';
|
||||||
|
|
||||||
export interface ChartPreviewProps {
|
export interface ChartPreviewProps {
|
||||||
name: string;
|
name: string;
|
||||||
@ -50,12 +51,6 @@ function ChartPreview({
|
|||||||
(state) => state.globalTime,
|
(state) => state.globalTime,
|
||||||
);
|
);
|
||||||
|
|
||||||
const thresholdValue = covertIntoDataFormats({
|
|
||||||
value: threshold,
|
|
||||||
sourceUnit: alertDef?.condition.targetUnit,
|
|
||||||
targetUnit: query?.unit,
|
|
||||||
});
|
|
||||||
|
|
||||||
const canQuery = useMemo((): boolean => {
|
const canQuery = useMemo((): boolean => {
|
||||||
if (!query || query == null) {
|
if (!query || query == null) {
|
||||||
return false;
|
return false;
|
||||||
@ -110,6 +105,9 @@ function ChartPreview({
|
|||||||
|
|
||||||
const isDarkMode = useIsDarkMode();
|
const isDarkMode = useIsDarkMode();
|
||||||
|
|
||||||
|
const optionName =
|
||||||
|
getFormatNameByOptionId(alertDef?.condition.targetUnit || '') || '';
|
||||||
|
|
||||||
const options = useMemo(
|
const options = useMemo(
|
||||||
() =>
|
() =>
|
||||||
getUPlotChartOptions({
|
getUPlotChartOptions({
|
||||||
@ -124,10 +122,16 @@ function ChartPreview({
|
|||||||
keyIndex: 0,
|
keyIndex: 0,
|
||||||
moveThreshold: (): void => {},
|
moveThreshold: (): void => {},
|
||||||
selectedGraph: PANEL_TYPES.TIME_SERIES, // no impact
|
selectedGraph: PANEL_TYPES.TIME_SERIES, // no impact
|
||||||
thresholdValue,
|
thresholdValue: threshold,
|
||||||
thresholdLabel: `${t(
|
thresholdLabel: `${t(
|
||||||
'preview_chart_threshold_label',
|
'preview_chart_threshold_label',
|
||||||
)} (y=${thresholdValue} ${query?.unit || ''})`,
|
)} (y=${getThresholdLabel(
|
||||||
|
optionName,
|
||||||
|
threshold,
|
||||||
|
alertDef?.condition.targetUnit,
|
||||||
|
query?.unit,
|
||||||
|
)})`,
|
||||||
|
thresholdUnit: alertDef?.condition.targetUnit,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
@ -136,8 +140,10 @@ function ChartPreview({
|
|||||||
queryResponse?.data?.payload,
|
queryResponse?.data?.payload,
|
||||||
containerDimensions,
|
containerDimensions,
|
||||||
isDarkMode,
|
isDarkMode,
|
||||||
|
threshold,
|
||||||
t,
|
t,
|
||||||
thresholdValue,
|
optionName,
|
||||||
|
alertDef?.condition.targetUnit,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -51,6 +51,21 @@ export function covertIntoDataFormats({
|
|||||||
return Number.isNaN(result) ? 0 : result;
|
return Number.isNaN(result) ? 0 : result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getThresholdLabel = (
|
||||||
|
optionName: string,
|
||||||
|
value: number,
|
||||||
|
unit?: string,
|
||||||
|
yAxisUnit?: string,
|
||||||
|
): string => {
|
||||||
|
if (
|
||||||
|
unit === MiscellaneousFormats.PercentUnit ||
|
||||||
|
yAxisUnit === MiscellaneousFormats.PercentUnit
|
||||||
|
) {
|
||||||
|
return `${value * 100}%`;
|
||||||
|
}
|
||||||
|
return `${value} ${optionName}`;
|
||||||
|
};
|
||||||
|
|
||||||
interface IUnit {
|
interface IUnit {
|
||||||
value: number;
|
value: number;
|
||||||
sourceUnit?: string;
|
sourceUnit?: string;
|
||||||
|
@ -25,19 +25,26 @@ export const getDefaultTableDataSet = (
|
|||||||
data: uPlot.AlignedData,
|
data: uPlot.AlignedData,
|
||||||
): ExtendedChartDataset[] =>
|
): ExtendedChartDataset[] =>
|
||||||
options.series.map(
|
options.series.map(
|
||||||
(item: uPlot.Series, index: number): ExtendedChartDataset => ({
|
(item: uPlot.Series, index: number): ExtendedChartDataset => {
|
||||||
|
let arr: number[];
|
||||||
|
if (data[index]) {
|
||||||
|
arr = data[index] as number[];
|
||||||
|
} else {
|
||||||
|
arr = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
...item,
|
...item,
|
||||||
index,
|
index,
|
||||||
show: true,
|
show: true,
|
||||||
sum: convertToTwoDecimalsOrZero(
|
sum: convertToTwoDecimalsOrZero(arr.reduce((a, b) => a + b, 0) || 0),
|
||||||
(data[index] as number[]).reduce((a, b) => a + b, 0),
|
|
||||||
),
|
|
||||||
avg: convertToTwoDecimalsOrZero(
|
avg: convertToTwoDecimalsOrZero(
|
||||||
(data[index] as number[]).reduce((a, b) => a + b, 0) / data[index].length,
|
(arr.reduce((a, b) => a + b, 0) || 0) / (arr.length || 1),
|
||||||
),
|
),
|
||||||
max: convertToTwoDecimalsOrZero(Math.max(...(data[index] as number[]))),
|
max: convertToTwoDecimalsOrZero(Math.max(...arr)),
|
||||||
min: convertToTwoDecimalsOrZero(Math.min(...(data[index] as number[]))),
|
min: convertToTwoDecimalsOrZero(Math.min(...arr)),
|
||||||
}),
|
};
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export const getAbbreviatedLabel = (label: string): string => {
|
export const getAbbreviatedLabel = (label: string): string => {
|
||||||
|
@ -47,7 +47,7 @@ function WidgetGraphComponent({
|
|||||||
const [deleteModal, setDeleteModal] = useState(false);
|
const [deleteModal, setDeleteModal] = useState(false);
|
||||||
const [hovered, setHovered] = useState(false);
|
const [hovered, setHovered] = useState(false);
|
||||||
const { notifications } = useNotifications();
|
const { notifications } = useNotifications();
|
||||||
const { pathname } = useLocation();
|
const { pathname, search } = useLocation();
|
||||||
|
|
||||||
const params = useUrlQuery();
|
const params = useUrlQuery();
|
||||||
|
|
||||||
@ -183,10 +183,20 @@ function WidgetGraphComponent({
|
|||||||
const queryParams = {
|
const queryParams = {
|
||||||
[QueryParams.expandedWidgetId]: widget.id,
|
[QueryParams.expandedWidgetId]: widget.id,
|
||||||
};
|
};
|
||||||
|
const updatedSearch = createQueryParams(queryParams);
|
||||||
|
const existingSearch = new URLSearchParams(search);
|
||||||
|
const isExpandedWidgetIdPresent = existingSearch.has(
|
||||||
|
QueryParams.expandedWidgetId,
|
||||||
|
);
|
||||||
|
if (isExpandedWidgetIdPresent) {
|
||||||
|
existingSearch.delete(QueryParams.expandedWidgetId);
|
||||||
|
}
|
||||||
|
const separator = existingSearch.toString() ? '&' : '';
|
||||||
|
const newSearch = `${existingSearch}${separator}${updatedSearch}`;
|
||||||
|
|
||||||
history.push({
|
history.push({
|
||||||
pathname,
|
pathname,
|
||||||
search: createQueryParams(queryParams),
|
search: newSearch,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -199,9 +209,12 @@ function WidgetGraphComponent({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onToggleModelHandler = (): void => {
|
const onToggleModelHandler = (): void => {
|
||||||
|
const existingSearchParams = new URLSearchParams(search);
|
||||||
|
existingSearchParams.delete(QueryParams.expandedWidgetId);
|
||||||
|
const updatedQueryParams = Object.fromEntries(existingSearchParams.entries());
|
||||||
history.push({
|
history.push({
|
||||||
pathname,
|
pathname,
|
||||||
search: createQueryParams({}),
|
search: createQueryParams(updatedQueryParams),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -0,0 +1,30 @@
|
|||||||
|
.widget-header-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
height: 30px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.widget-header-title {
|
||||||
|
max-width: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.widget-header-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.widget-header-more-options {
|
||||||
|
visibility: hidden;
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.widget-header-hover {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.widget-api-actions {
|
||||||
|
padding-right: 0.25rem;
|
||||||
|
}
|
@ -1,21 +1,23 @@
|
|||||||
|
import './WidgetHeader.styles.scss';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AlertOutlined,
|
AlertOutlined,
|
||||||
CopyOutlined,
|
CopyOutlined,
|
||||||
DeleteOutlined,
|
DeleteOutlined,
|
||||||
DownOutlined,
|
|
||||||
EditFilled,
|
EditFilled,
|
||||||
ExclamationCircleOutlined,
|
ExclamationCircleOutlined,
|
||||||
FullscreenOutlined,
|
FullscreenOutlined,
|
||||||
|
MoreOutlined,
|
||||||
WarningOutlined,
|
WarningOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { Dropdown, MenuProps, Tooltip, Typography } from 'antd';
|
import { Button, Dropdown, MenuProps, Tooltip, Typography } from 'antd';
|
||||||
import Spinner from 'components/Spinner';
|
import Spinner from 'components/Spinner';
|
||||||
import { QueryParams } from 'constants/query';
|
import { QueryParams } from 'constants/query';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import useComponentPermission from 'hooks/useComponentPermission';
|
import useComponentPermission from 'hooks/useComponentPermission';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import { ReactNode, useCallback, useMemo, useState } from 'react';
|
import { ReactNode, useCallback, useMemo } from 'react';
|
||||||
import { UseQueryResult } from 'react-query';
|
import { UseQueryResult } from 'react-query';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
@ -23,23 +25,9 @@ import { ErrorResponse, SuccessResponse } from 'types/api';
|
|||||||
import { Widgets } from 'types/api/dashboard/getAll';
|
import { Widgets } from 'types/api/dashboard/getAll';
|
||||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||||
import AppReducer from 'types/reducer/app';
|
import AppReducer from 'types/reducer/app';
|
||||||
import { popupContainer } from 'utils/selectPopupContainer';
|
|
||||||
|
|
||||||
import {
|
import { errorTooltipPosition, WARNING_MESSAGE } from './config';
|
||||||
errorTooltipPosition,
|
|
||||||
overlayStyles,
|
|
||||||
spinnerStyles,
|
|
||||||
tooltipStyles,
|
|
||||||
WARNING_MESSAGE,
|
|
||||||
} from './config';
|
|
||||||
import { MENUITEM_KEYS_VS_LABELS, MenuItemKeys } from './contants';
|
import { MENUITEM_KEYS_VS_LABELS, MenuItemKeys } from './contants';
|
||||||
import {
|
|
||||||
ArrowContainer,
|
|
||||||
HeaderContainer,
|
|
||||||
HeaderContentContainer,
|
|
||||||
ThesholdContainer,
|
|
||||||
WidgetHeaderContainer,
|
|
||||||
} from './styles';
|
|
||||||
import { MenuItem } from './types';
|
import { MenuItem } from './types';
|
||||||
import { generateMenuList, isTWidgetOptions } from './utils';
|
import { generateMenuList, isTWidgetOptions } from './utils';
|
||||||
|
|
||||||
@ -72,9 +60,6 @@ function WidgetHeader({
|
|||||||
headerMenuList,
|
headerMenuList,
|
||||||
isWarning,
|
isWarning,
|
||||||
}: IWidgetHeaderProps): JSX.Element | null {
|
}: IWidgetHeaderProps): JSX.Element | null {
|
||||||
const [localHover, setLocalHover] = useState(false);
|
|
||||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
|
||||||
|
|
||||||
const onEditHandler = useCallback((): void => {
|
const onEditHandler = useCallback((): void => {
|
||||||
const widgetId = widget.id;
|
const widgetId = widget.id;
|
||||||
history.push(
|
history.push(
|
||||||
@ -112,7 +97,6 @@ function WidgetHeader({
|
|||||||
|
|
||||||
if (functionToCall) {
|
if (functionToCall) {
|
||||||
functionToCall();
|
functionToCall();
|
||||||
setIsOpen(false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -169,10 +153,6 @@ function WidgetHeader({
|
|||||||
|
|
||||||
const updatedMenuList = useMemo(() => generateMenuList(actions), [actions]);
|
const updatedMenuList = useMemo(() => generateMenuList(actions), [actions]);
|
||||||
|
|
||||||
const onClickHandler = (): void => {
|
|
||||||
setIsOpen(!isOpen);
|
|
||||||
};
|
|
||||||
|
|
||||||
const menu = useMemo(
|
const menu = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
items: updatedMenuList,
|
items: updatedMenuList,
|
||||||
@ -186,49 +166,49 @@ function WidgetHeader({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WidgetHeaderContainer>
|
<div className="widget-header-container">
|
||||||
<Dropdown
|
<Typography.Text
|
||||||
getPopupContainer={popupContainer}
|
ellipsis
|
||||||
destroyPopupOnHide
|
data-testid={title}
|
||||||
open={isOpen}
|
className="widget-header-title"
|
||||||
onOpenChange={setIsOpen}
|
|
||||||
menu={menu}
|
|
||||||
trigger={['click']}
|
|
||||||
overlayStyle={overlayStyles}
|
|
||||||
>
|
>
|
||||||
<HeaderContainer
|
|
||||||
onMouseOver={(): void => setLocalHover(true)}
|
|
||||||
onMouseOut={(): void => setLocalHover(false)}
|
|
||||||
hover={localHover}
|
|
||||||
onClick={onClickHandler}
|
|
||||||
>
|
|
||||||
<HeaderContentContainer>
|
|
||||||
<Typography.Text style={{ maxWidth: '80%' }} ellipsis data-testid={title}>
|
|
||||||
{title}
|
{title}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
<ArrowContainer hover={parentHover}>
|
<div className="widget-header-actions">
|
||||||
<DownOutlined />
|
<div className="widget-api-actions">{threshold}</div>
|
||||||
</ArrowContainer>
|
|
||||||
</HeaderContentContainer>
|
|
||||||
</HeaderContainer>
|
|
||||||
</Dropdown>
|
|
||||||
|
|
||||||
<ThesholdContainer>{threshold}</ThesholdContainer>
|
|
||||||
{queryResponse.isFetching && !queryResponse.isError && (
|
{queryResponse.isFetching && !queryResponse.isError && (
|
||||||
<Spinner height="5vh" style={spinnerStyles} />
|
<Spinner style={{ paddingRight: '0.25rem' }} />
|
||||||
)}
|
)}
|
||||||
{queryResponse.isError && (
|
{queryResponse.isError && (
|
||||||
<Tooltip title={errorMessage} placement={errorTooltipPosition}>
|
<Tooltip
|
||||||
<ExclamationCircleOutlined style={tooltipStyles} />
|
title={errorMessage}
|
||||||
|
placement={errorTooltipPosition}
|
||||||
|
className="widget-api-actions"
|
||||||
|
>
|
||||||
|
<ExclamationCircleOutlined />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isWarning && (
|
{isWarning && (
|
||||||
<Tooltip title={WARNING_MESSAGE} placement={errorTooltipPosition}>
|
<Tooltip
|
||||||
<WarningOutlined style={tooltipStyles} />
|
title={WARNING_MESSAGE}
|
||||||
|
placement={errorTooltipPosition}
|
||||||
|
className="widget-api-actions"
|
||||||
|
>
|
||||||
|
<WarningOutlined />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</WidgetHeaderContainer>
|
<Dropdown menu={menu} trigger={['hover']} placement="bottomRight">
|
||||||
|
<Button
|
||||||
|
type="default"
|
||||||
|
icon={<MoreOutlined />}
|
||||||
|
className={`widget-header-more-options ${
|
||||||
|
parentHover ? 'widget-header-hover' : ''
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,8 +41,6 @@ export const WidgetHeaderContainer = styled.div`
|
|||||||
|
|
||||||
export const ArrowContainer = styled.span<{ hover: boolean }>`
|
export const ArrowContainer = styled.span<{ hover: boolean }>`
|
||||||
visibility: ${({ hover }): string => (hover ? 'visible' : 'hidden')};
|
visibility: ${({ hover }): string => (hover ? 'visible' : 'hidden')};
|
||||||
position: absolute;
|
|
||||||
right: -1rem;
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Typography = styled(TypographyComponent)`
|
export const Typography = styled(TypographyComponent)`
|
||||||
|
@ -8,5 +8,18 @@
|
|||||||
.upgrade-link {
|
.upgrade-link {
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
padding-right: 4px;
|
padding-right: 4px;
|
||||||
|
display: inline !important;
|
||||||
color: white;
|
color: white;
|
||||||
|
text-decoration: underline;
|
||||||
|
text-decoration-color: white;
|
||||||
|
text-decoration-thickness: 2px;
|
||||||
|
text-underline-offset: 2px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: white;
|
||||||
|
text-decoration: underline;
|
||||||
|
text-decoration-color: white;
|
||||||
|
text-decoration-thickness: 2px;
|
||||||
|
text-underline-offset: 2px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||||
|
/* eslint-disable jsx-a11y/anchor-is-valid */
|
||||||
|
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||||
import './Header.styles.scss';
|
import './Header.styles.scss';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -135,16 +138,17 @@ function HeaderContainer(): JSX.Element {
|
|||||||
<>
|
<>
|
||||||
{showTrialExpiryBanner && (
|
{showTrialExpiryBanner && (
|
||||||
<div className="trial-expiry-banner">
|
<div className="trial-expiry-banner">
|
||||||
You are in free trial period. Your free trial will end on
|
You are in free trial period. Your free trial will end on{' '}
|
||||||
<span>
|
<span>
|
||||||
{getFormattedDate(licenseData?.payload?.trialEnd || Date.now())}.
|
{getFormattedDate(licenseData?.payload?.trialEnd || Date.now())}.
|
||||||
</span>
|
</span>
|
||||||
{role === 'ADMIN' ? (
|
{role === 'ADMIN' ? (
|
||||||
<span>
|
<span>
|
||||||
Please
|
{' '}
|
||||||
<Button className="upgrade-link" type="link" onClick={handleUpgrade}>
|
Please{' '}
|
||||||
|
<a className="upgrade-link" onClick={handleUpgrade}>
|
||||||
upgrade
|
upgrade
|
||||||
</Button>
|
</a>
|
||||||
to continue using SigNoz features.
|
to continue using SigNoz features.
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
|
378
frontend/src/container/ListOfDashboard/DashboardsList.tsx
Normal file
378
frontend/src/container/ListOfDashboard/DashboardsList.tsx
Normal file
@ -0,0 +1,378 @@
|
|||||||
|
import { PlusOutlined } from '@ant-design/icons';
|
||||||
|
import { Card, Col, Dropdown, Input, Row, TableColumnProps } from 'antd';
|
||||||
|
import { ItemType } from 'antd/es/menu/hooks/useItems';
|
||||||
|
import createDashboard from 'api/dashboard/create';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import {
|
||||||
|
DynamicColumnsKey,
|
||||||
|
TableDataSource,
|
||||||
|
} from 'components/ResizeTable/contants';
|
||||||
|
import DynamicColumnTable from 'components/ResizeTable/DynamicColumnTable';
|
||||||
|
import LabelColumn from 'components/TableRenderer/LabelColumn';
|
||||||
|
import TextToolTip from 'components/TextToolTip';
|
||||||
|
import ROUTES from 'constants/routes';
|
||||||
|
import { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard';
|
||||||
|
import useComponentPermission from 'hooks/useComponentPermission';
|
||||||
|
import useDebouncedFn from 'hooks/useDebouncedFunction';
|
||||||
|
import history from 'lib/history';
|
||||||
|
import { Key, useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { generatePath } from 'react-router-dom';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
|
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||||
|
import AppReducer from 'types/reducer/app';
|
||||||
|
|
||||||
|
import DateComponent from '../../components/ResizeTable/TableComponent/DateComponent';
|
||||||
|
import ImportJSON from './ImportJSON';
|
||||||
|
import { ButtonContainer, NewDashboardButton, TableContainer } from './styles';
|
||||||
|
import DeleteButton from './TableComponents/DeleteButton';
|
||||||
|
import Name from './TableComponents/Name';
|
||||||
|
|
||||||
|
const { Search } = Input;
|
||||||
|
|
||||||
|
function DashboardsList(): JSX.Element {
|
||||||
|
const {
|
||||||
|
data: dashboardListResponse = [],
|
||||||
|
isLoading: isDashboardListLoading,
|
||||||
|
refetch: refetchDashboardList,
|
||||||
|
} = useGetAllDashboard();
|
||||||
|
|
||||||
|
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||||
|
|
||||||
|
const [action, createNewDashboard] = useComponentPermission(
|
||||||
|
['action', 'create_new_dashboards'],
|
||||||
|
role,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { t } = useTranslation('dashboard');
|
||||||
|
|
||||||
|
const [
|
||||||
|
isImportJSONModalVisible,
|
||||||
|
setIsImportJSONModalVisible,
|
||||||
|
] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const [uploadedGrafana, setUploadedGrafana] = useState<boolean>(false);
|
||||||
|
const [isFilteringDashboards, setIsFilteringDashboards] = useState(false);
|
||||||
|
|
||||||
|
const [dashboards, setDashboards] = useState<Dashboard[]>();
|
||||||
|
|
||||||
|
const sortDashboardsByCreatedAt = (dashboards: Dashboard[]): void => {
|
||||||
|
const sortedDashboards = dashboards.sort(
|
||||||
|
(a, b) =>
|
||||||
|
new Date(b.created_at).getTime() - new Date(a.created_at).getTime(),
|
||||||
|
);
|
||||||
|
setDashboards(sortedDashboards);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
sortDashboardsByCreatedAt(dashboardListResponse);
|
||||||
|
}, [dashboardListResponse]);
|
||||||
|
|
||||||
|
const [newDashboardState, setNewDashboardState] = useState({
|
||||||
|
loading: false,
|
||||||
|
error: false,
|
||||||
|
errorMessage: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const dynamicColumns: TableColumnProps<Data>[] = [
|
||||||
|
{
|
||||||
|
title: 'Created At',
|
||||||
|
dataIndex: 'createdAt',
|
||||||
|
width: 30,
|
||||||
|
key: DynamicColumnsKey.CreatedAt,
|
||||||
|
sorter: (a: Data, b: Data): number => {
|
||||||
|
console.log({ a });
|
||||||
|
const prev = new Date(a.createdAt).getTime();
|
||||||
|
const next = new Date(b.createdAt).getTime();
|
||||||
|
|
||||||
|
return prev - next;
|
||||||
|
},
|
||||||
|
render: DateComponent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Created By',
|
||||||
|
dataIndex: 'createdBy',
|
||||||
|
width: 30,
|
||||||
|
key: DynamicColumnsKey.CreatedBy,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Last Updated Time',
|
||||||
|
width: 30,
|
||||||
|
dataIndex: 'lastUpdatedTime',
|
||||||
|
key: DynamicColumnsKey.UpdatedAt,
|
||||||
|
sorter: (a: Data, b: Data): number => {
|
||||||
|
const prev = new Date(a.lastUpdatedTime).getTime();
|
||||||
|
const next = new Date(b.lastUpdatedTime).getTime();
|
||||||
|
|
||||||
|
return prev - next;
|
||||||
|
},
|
||||||
|
render: DateComponent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Last Updated By',
|
||||||
|
dataIndex: 'lastUpdatedBy',
|
||||||
|
width: 30,
|
||||||
|
key: DynamicColumnsKey.UpdatedBy,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const columns = useMemo(() => {
|
||||||
|
const tableColumns: TableColumnProps<Data>[] = [
|
||||||
|
{
|
||||||
|
title: 'Name',
|
||||||
|
dataIndex: 'name',
|
||||||
|
width: 40,
|
||||||
|
render: Name,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Description',
|
||||||
|
width: 50,
|
||||||
|
dataIndex: 'description',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Tags',
|
||||||
|
dataIndex: 'tags',
|
||||||
|
width: 50,
|
||||||
|
render: (value): JSX.Element => <LabelColumn labels={value} />,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (action) {
|
||||||
|
tableColumns.push({
|
||||||
|
title: 'Action',
|
||||||
|
dataIndex: '',
|
||||||
|
width: 40,
|
||||||
|
render: DeleteButton,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return tableColumns;
|
||||||
|
}, [action]);
|
||||||
|
|
||||||
|
const data: Data[] =
|
||||||
|
dashboards?.map((e) => ({
|
||||||
|
createdAt: e.created_at,
|
||||||
|
description: e.data.description || '',
|
||||||
|
id: e.uuid,
|
||||||
|
lastUpdatedTime: e.updated_at,
|
||||||
|
name: e.data.title,
|
||||||
|
tags: e.data.tags || [],
|
||||||
|
key: e.uuid,
|
||||||
|
createdBy: e.created_by,
|
||||||
|
isLocked: !!e.isLocked || false,
|
||||||
|
lastUpdatedBy: e.updated_by,
|
||||||
|
refetchDashboardList,
|
||||||
|
})) || [];
|
||||||
|
|
||||||
|
const onNewDashboardHandler = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
setNewDashboardState({
|
||||||
|
...newDashboardState,
|
||||||
|
loading: true,
|
||||||
|
});
|
||||||
|
const response = await createDashboard({
|
||||||
|
title: t('new_dashboard_title', {
|
||||||
|
ns: 'dashboard',
|
||||||
|
}),
|
||||||
|
uploadedGrafana: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.statusCode === 200) {
|
||||||
|
history.push(
|
||||||
|
generatePath(ROUTES.DASHBOARD, {
|
||||||
|
dashboardId: response.payload.uuid,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setNewDashboardState({
|
||||||
|
...newDashboardState,
|
||||||
|
loading: false,
|
||||||
|
error: true,
|
||||||
|
errorMessage: response.error || 'Something went wrong',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setNewDashboardState({
|
||||||
|
...newDashboardState,
|
||||||
|
error: true,
|
||||||
|
errorMessage: (error as AxiosError).toString() || 'Something went Wrong',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [newDashboardState, t]);
|
||||||
|
|
||||||
|
const getText = useCallback(() => {
|
||||||
|
if (!newDashboardState.error && !newDashboardState.loading) {
|
||||||
|
return 'New Dashboard';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newDashboardState.loading) {
|
||||||
|
return 'Loading';
|
||||||
|
}
|
||||||
|
|
||||||
|
return newDashboardState.errorMessage;
|
||||||
|
}, [
|
||||||
|
newDashboardState.error,
|
||||||
|
newDashboardState.errorMessage,
|
||||||
|
newDashboardState.loading,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const onModalHandler = (uploadedGrafana: boolean): void => {
|
||||||
|
setIsImportJSONModalVisible((state) => !state);
|
||||||
|
setUploadedGrafana(uploadedGrafana);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMenuItems = useMemo(() => {
|
||||||
|
const menuItems: ItemType[] = [
|
||||||
|
{
|
||||||
|
key: t('import_json').toString(),
|
||||||
|
label: t('import_json'),
|
||||||
|
onClick: (): void => onModalHandler(false),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: t('import_grafana_json').toString(),
|
||||||
|
label: t('import_grafana_json'),
|
||||||
|
onClick: (): void => onModalHandler(true),
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (createNewDashboard) {
|
||||||
|
menuItems.unshift({
|
||||||
|
key: t('create_dashboard').toString(),
|
||||||
|
label: t('create_dashboard'),
|
||||||
|
disabled: isDashboardListLoading,
|
||||||
|
onClick: onNewDashboardHandler,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return menuItems;
|
||||||
|
}, [createNewDashboard, isDashboardListLoading, onNewDashboardHandler, t]);
|
||||||
|
|
||||||
|
const searchArrayOfObjects = (searchValue: string): any[] => {
|
||||||
|
// Convert the searchValue to lowercase for case-insensitive search
|
||||||
|
const searchValueLowerCase = searchValue.toLowerCase();
|
||||||
|
|
||||||
|
// Use the filter method to find matching objects
|
||||||
|
return dashboardListResponse.filter((item: any) => {
|
||||||
|
// Convert each property value to lowercase for case-insensitive search
|
||||||
|
const itemValues = Object.values(item?.data).map((value: any) =>
|
||||||
|
value.toString().toLowerCase(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check if any property value contains the searchValue
|
||||||
|
return itemValues.some((value) => value.includes(searchValueLowerCase));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearch = useDebouncedFn((event: unknown): void => {
|
||||||
|
setIsFilteringDashboards(true);
|
||||||
|
const searchText = (event as React.BaseSyntheticEvent)?.target?.value || '';
|
||||||
|
const filteredDashboards = searchArrayOfObjects(searchText);
|
||||||
|
setDashboards(filteredDashboards);
|
||||||
|
setIsFilteringDashboards(false);
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
const GetHeader = useMemo(
|
||||||
|
() => (
|
||||||
|
<Row gutter={16} align="middle">
|
||||||
|
<Col span={18}>
|
||||||
|
<Search
|
||||||
|
disabled={isDashboardListLoading}
|
||||||
|
placeholder="Search by Name, Description, Tags"
|
||||||
|
onChange={handleSearch}
|
||||||
|
loading={isFilteringDashboards}
|
||||||
|
style={{ marginBottom: 16, marginTop: 16 }}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
<Col
|
||||||
|
span={6}
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ButtonContainer>
|
||||||
|
<TextToolTip
|
||||||
|
{...{
|
||||||
|
text: `More details on how to create dashboards`,
|
||||||
|
url: 'https://signoz.io/docs/userguide/dashboards',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ButtonContainer>
|
||||||
|
|
||||||
|
<Dropdown
|
||||||
|
menu={{ items: getMenuItems }}
|
||||||
|
disabled={isDashboardListLoading}
|
||||||
|
placement="bottomRight"
|
||||||
|
>
|
||||||
|
<NewDashboardButton
|
||||||
|
icon={<PlusOutlined />}
|
||||||
|
type="primary"
|
||||||
|
data-testid="create-new-dashboard"
|
||||||
|
loading={newDashboardState.loading}
|
||||||
|
danger={newDashboardState.error}
|
||||||
|
>
|
||||||
|
{getText()}
|
||||||
|
</NewDashboardButton>
|
||||||
|
</Dropdown>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
),
|
||||||
|
[
|
||||||
|
isDashboardListLoading,
|
||||||
|
handleSearch,
|
||||||
|
isFilteringDashboards,
|
||||||
|
getMenuItems,
|
||||||
|
newDashboardState.loading,
|
||||||
|
newDashboardState.error,
|
||||||
|
getText,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
{GetHeader}
|
||||||
|
|
||||||
|
<TableContainer>
|
||||||
|
<ImportJSON
|
||||||
|
isImportJSONModalVisible={isImportJSONModalVisible}
|
||||||
|
uploadedGrafana={uploadedGrafana}
|
||||||
|
onModalHandler={(): void => onModalHandler(false)}
|
||||||
|
/>
|
||||||
|
<DynamicColumnTable
|
||||||
|
tablesource={TableDataSource.Dashboard}
|
||||||
|
dynamicColumns={dynamicColumns}
|
||||||
|
columns={columns}
|
||||||
|
pagination={{
|
||||||
|
pageSize: 10,
|
||||||
|
defaultPageSize: 10,
|
||||||
|
total: data?.length || 0,
|
||||||
|
}}
|
||||||
|
showHeader
|
||||||
|
bordered
|
||||||
|
sticky
|
||||||
|
loading={isDashboardListLoading}
|
||||||
|
dataSource={data}
|
||||||
|
showSorterTooltip
|
||||||
|
/>
|
||||||
|
</TableContainer>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Data {
|
||||||
|
key: Key;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
tags: string[];
|
||||||
|
createdBy: string;
|
||||||
|
createdAt: string;
|
||||||
|
lastUpdatedTime: string;
|
||||||
|
lastUpdatedBy: string;
|
||||||
|
isLocked: boolean;
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DashboardsList;
|
@ -2,7 +2,7 @@ import { Typography } from 'antd';
|
|||||||
import convertDateToAmAndPm from 'lib/convertDateToAmAndPm';
|
import convertDateToAmAndPm from 'lib/convertDateToAmAndPm';
|
||||||
import getFormattedDate from 'lib/getFormatedDate';
|
import getFormattedDate from 'lib/getFormatedDate';
|
||||||
|
|
||||||
import { Data } from '..';
|
import { Data } from '../DashboardsList';
|
||||||
|
|
||||||
function Created(createdBy: Data['createdBy']): JSX.Element {
|
function Created(createdBy: Data['createdBy']): JSX.Element {
|
||||||
const time = new Date(createdBy);
|
const time = new Date(createdBy);
|
||||||
|
@ -11,7 +11,7 @@ import { AppState } from 'store/reducers';
|
|||||||
import AppReducer from 'types/reducer/app';
|
import AppReducer from 'types/reducer/app';
|
||||||
import { USER_ROLES } from 'types/roles';
|
import { USER_ROLES } from 'types/roles';
|
||||||
|
|
||||||
import { Data } from '..';
|
import { Data } from '../DashboardsList';
|
||||||
import { TableLinkText } from './styles';
|
import { TableLinkText } from './styles';
|
||||||
|
|
||||||
interface DeleteButtonProps {
|
interface DeleteButtonProps {
|
||||||
|
@ -2,7 +2,7 @@ import { LockFilled } from '@ant-design/icons';
|
|||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
|
|
||||||
import { Data } from '..';
|
import { Data } from '../DashboardsList';
|
||||||
import { TableLinkText } from './styles';
|
import { TableLinkText } from './styles';
|
||||||
|
|
||||||
function Name(name: Data['name'], data: Data): JSX.Element {
|
function Name(name: Data['name'], data: Data): JSX.Element {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
/* eslint-disable react/destructuring-assignment */
|
/* eslint-disable react/destructuring-assignment */
|
||||||
import { Tag } from 'antd';
|
import { Tag } from 'antd';
|
||||||
|
|
||||||
import { Data } from '../index';
|
import { Data } from '../DashboardsList';
|
||||||
|
|
||||||
function Tags(data: Data['tags']): JSX.Element {
|
function Tags(data: Data['tags']): JSX.Element {
|
||||||
return (
|
return (
|
||||||
|
@ -1,378 +1,3 @@
|
|||||||
import { PlusOutlined } from '@ant-design/icons';
|
import DashboardsList from './DashboardsList';
|
||||||
import { Card, Col, Dropdown, Input, Row, TableColumnProps } from 'antd';
|
|
||||||
import { ItemType } from 'antd/es/menu/hooks/useItems';
|
|
||||||
import createDashboard from 'api/dashboard/create';
|
|
||||||
import { AxiosError } from 'axios';
|
|
||||||
import {
|
|
||||||
DynamicColumnsKey,
|
|
||||||
TableDataSource,
|
|
||||||
} from 'components/ResizeTable/contants';
|
|
||||||
import DynamicColumnTable from 'components/ResizeTable/DynamicColumnTable';
|
|
||||||
import LabelColumn from 'components/TableRenderer/LabelColumn';
|
|
||||||
import TextToolTip from 'components/TextToolTip';
|
|
||||||
import ROUTES from 'constants/routes';
|
|
||||||
import { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard';
|
|
||||||
import useComponentPermission from 'hooks/useComponentPermission';
|
|
||||||
import useDebouncedFn from 'hooks/useDebouncedFunction';
|
|
||||||
import history from 'lib/history';
|
|
||||||
import { Key, useCallback, useEffect, useMemo, useState } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import { generatePath } from 'react-router-dom';
|
|
||||||
import { AppState } from 'store/reducers';
|
|
||||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
|
||||||
import AppReducer from 'types/reducer/app';
|
|
||||||
|
|
||||||
import DateComponent from '../../components/ResizeTable/TableComponent/DateComponent';
|
export default DashboardsList;
|
||||||
import ImportJSON from './ImportJSON';
|
|
||||||
import { ButtonContainer, NewDashboardButton, TableContainer } from './styles';
|
|
||||||
import DeleteButton from './TableComponents/DeleteButton';
|
|
||||||
import Name from './TableComponents/Name';
|
|
||||||
|
|
||||||
const { Search } = Input;
|
|
||||||
|
|
||||||
function ListOfAllDashboard(): JSX.Element {
|
|
||||||
const {
|
|
||||||
data: dashboardListResponse = [],
|
|
||||||
isLoading: isDashboardListLoading,
|
|
||||||
refetch: refetchDashboardList,
|
|
||||||
} = useGetAllDashboard();
|
|
||||||
|
|
||||||
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
|
|
||||||
|
|
||||||
const [action, createNewDashboard] = useComponentPermission(
|
|
||||||
['action', 'create_new_dashboards'],
|
|
||||||
role,
|
|
||||||
);
|
|
||||||
|
|
||||||
const { t } = useTranslation('dashboard');
|
|
||||||
|
|
||||||
const [
|
|
||||||
isImportJSONModalVisible,
|
|
||||||
setIsImportJSONModalVisible,
|
|
||||||
] = useState<boolean>(false);
|
|
||||||
|
|
||||||
const [uploadedGrafana, setUploadedGrafana] = useState<boolean>(false);
|
|
||||||
const [isFilteringDashboards, setIsFilteringDashboards] = useState(false);
|
|
||||||
|
|
||||||
const [dashboards, setDashboards] = useState<Dashboard[]>();
|
|
||||||
|
|
||||||
const sortDashboardsByCreatedAt = (dashboards: Dashboard[]): void => {
|
|
||||||
const sortedDashboards = dashboards.sort(
|
|
||||||
(a, b) =>
|
|
||||||
new Date(b.created_at).getTime() - new Date(a.created_at).getTime(),
|
|
||||||
);
|
|
||||||
setDashboards(sortedDashboards);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
sortDashboardsByCreatedAt(dashboardListResponse);
|
|
||||||
}, [dashboardListResponse]);
|
|
||||||
|
|
||||||
const [newDashboardState, setNewDashboardState] = useState({
|
|
||||||
loading: false,
|
|
||||||
error: false,
|
|
||||||
errorMessage: '',
|
|
||||||
});
|
|
||||||
|
|
||||||
const dynamicColumns: TableColumnProps<Data>[] = [
|
|
||||||
{
|
|
||||||
title: 'Created At',
|
|
||||||
dataIndex: 'createdAt',
|
|
||||||
width: 30,
|
|
||||||
key: DynamicColumnsKey.CreatedAt,
|
|
||||||
sorter: (a: Data, b: Data): number => {
|
|
||||||
console.log({ a });
|
|
||||||
const prev = new Date(a.createdAt).getTime();
|
|
||||||
const next = new Date(b.createdAt).getTime();
|
|
||||||
|
|
||||||
return prev - next;
|
|
||||||
},
|
|
||||||
render: DateComponent,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Created By',
|
|
||||||
dataIndex: 'createdBy',
|
|
||||||
width: 30,
|
|
||||||
key: DynamicColumnsKey.CreatedBy,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Last Updated Time',
|
|
||||||
width: 30,
|
|
||||||
dataIndex: 'lastUpdatedTime',
|
|
||||||
key: DynamicColumnsKey.UpdatedAt,
|
|
||||||
sorter: (a: Data, b: Data): number => {
|
|
||||||
const prev = new Date(a.lastUpdatedTime).getTime();
|
|
||||||
const next = new Date(b.lastUpdatedTime).getTime();
|
|
||||||
|
|
||||||
return prev - next;
|
|
||||||
},
|
|
||||||
render: DateComponent,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Last Updated By',
|
|
||||||
dataIndex: 'lastUpdatedBy',
|
|
||||||
width: 30,
|
|
||||||
key: DynamicColumnsKey.UpdatedBy,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const columns = useMemo(() => {
|
|
||||||
const tableColumns: TableColumnProps<Data>[] = [
|
|
||||||
{
|
|
||||||
title: 'Name',
|
|
||||||
dataIndex: 'name',
|
|
||||||
width: 40,
|
|
||||||
render: Name,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Description',
|
|
||||||
width: 50,
|
|
||||||
dataIndex: 'description',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Tags',
|
|
||||||
dataIndex: 'tags',
|
|
||||||
width: 50,
|
|
||||||
render: (value): JSX.Element => <LabelColumn labels={value} />,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
if (action) {
|
|
||||||
tableColumns.push({
|
|
||||||
title: 'Action',
|
|
||||||
dataIndex: '',
|
|
||||||
width: 40,
|
|
||||||
render: DeleteButton,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return tableColumns;
|
|
||||||
}, [action]);
|
|
||||||
|
|
||||||
const data: Data[] =
|
|
||||||
dashboards?.map((e) => ({
|
|
||||||
createdAt: e.created_at,
|
|
||||||
description: e.data.description || '',
|
|
||||||
id: e.uuid,
|
|
||||||
lastUpdatedTime: e.updated_at,
|
|
||||||
name: e.data.title,
|
|
||||||
tags: e.data.tags || [],
|
|
||||||
key: e.uuid,
|
|
||||||
createdBy: e.created_by,
|
|
||||||
isLocked: !!e.isLocked || false,
|
|
||||||
lastUpdatedBy: e.updated_by,
|
|
||||||
refetchDashboardList,
|
|
||||||
})) || [];
|
|
||||||
|
|
||||||
const onNewDashboardHandler = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
setNewDashboardState({
|
|
||||||
...newDashboardState,
|
|
||||||
loading: true,
|
|
||||||
});
|
|
||||||
const response = await createDashboard({
|
|
||||||
title: t('new_dashboard_title', {
|
|
||||||
ns: 'dashboard',
|
|
||||||
}),
|
|
||||||
uploadedGrafana: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.statusCode === 200) {
|
|
||||||
history.push(
|
|
||||||
generatePath(ROUTES.DASHBOARD, {
|
|
||||||
dashboardId: response.payload.uuid,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
setNewDashboardState({
|
|
||||||
...newDashboardState,
|
|
||||||
loading: false,
|
|
||||||
error: true,
|
|
||||||
errorMessage: response.error || 'Something went wrong',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
setNewDashboardState({
|
|
||||||
...newDashboardState,
|
|
||||||
error: true,
|
|
||||||
errorMessage: (error as AxiosError).toString() || 'Something went Wrong',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [newDashboardState, t]);
|
|
||||||
|
|
||||||
const getText = useCallback(() => {
|
|
||||||
if (!newDashboardState.error && !newDashboardState.loading) {
|
|
||||||
return 'New Dashboard';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newDashboardState.loading) {
|
|
||||||
return 'Loading';
|
|
||||||
}
|
|
||||||
|
|
||||||
return newDashboardState.errorMessage;
|
|
||||||
}, [
|
|
||||||
newDashboardState.error,
|
|
||||||
newDashboardState.errorMessage,
|
|
||||||
newDashboardState.loading,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const onModalHandler = (uploadedGrafana: boolean): void => {
|
|
||||||
setIsImportJSONModalVisible((state) => !state);
|
|
||||||
setUploadedGrafana(uploadedGrafana);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getMenuItems = useMemo(() => {
|
|
||||||
const menuItems: ItemType[] = [
|
|
||||||
{
|
|
||||||
key: t('import_json').toString(),
|
|
||||||
label: t('import_json'),
|
|
||||||
onClick: (): void => onModalHandler(false),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: t('import_grafana_json').toString(),
|
|
||||||
label: t('import_grafana_json'),
|
|
||||||
onClick: (): void => onModalHandler(true),
|
|
||||||
disabled: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
if (createNewDashboard) {
|
|
||||||
menuItems.unshift({
|
|
||||||
key: t('create_dashboard').toString(),
|
|
||||||
label: t('create_dashboard'),
|
|
||||||
disabled: isDashboardListLoading,
|
|
||||||
onClick: onNewDashboardHandler,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return menuItems;
|
|
||||||
}, [createNewDashboard, isDashboardListLoading, onNewDashboardHandler, t]);
|
|
||||||
|
|
||||||
const searchArrayOfObjects = (searchValue: string): any[] => {
|
|
||||||
// Convert the searchValue to lowercase for case-insensitive search
|
|
||||||
const searchValueLowerCase = searchValue.toLowerCase();
|
|
||||||
|
|
||||||
// Use the filter method to find matching objects
|
|
||||||
return dashboardListResponse.filter((item: any) => {
|
|
||||||
// Convert each property value to lowercase for case-insensitive search
|
|
||||||
const itemValues = Object.values(item?.data).map((value: any) =>
|
|
||||||
value.toString().toLowerCase(),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Check if any property value contains the searchValue
|
|
||||||
return itemValues.some((value) => value.includes(searchValueLowerCase));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSearch = useDebouncedFn((event: unknown): void => {
|
|
||||||
setIsFilteringDashboards(true);
|
|
||||||
const searchText = (event as React.BaseSyntheticEvent)?.target?.value || '';
|
|
||||||
const filteredDashboards = searchArrayOfObjects(searchText);
|
|
||||||
setDashboards(filteredDashboards);
|
|
||||||
setIsFilteringDashboards(false);
|
|
||||||
}, 500);
|
|
||||||
|
|
||||||
const GetHeader = useMemo(
|
|
||||||
() => (
|
|
||||||
<Row gutter={16} align="middle">
|
|
||||||
<Col span={18}>
|
|
||||||
<Search
|
|
||||||
disabled={isDashboardListLoading}
|
|
||||||
placeholder="Search by Name, Description, Tags"
|
|
||||||
onChange={handleSearch}
|
|
||||||
loading={isFilteringDashboards}
|
|
||||||
style={{ marginBottom: 16, marginTop: 16 }}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
|
|
||||||
<Col
|
|
||||||
span={6}
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'flex-end',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ButtonContainer>
|
|
||||||
<TextToolTip
|
|
||||||
{...{
|
|
||||||
text: `More details on how to create dashboards`,
|
|
||||||
url: 'https://signoz.io/docs/userguide/dashboards',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</ButtonContainer>
|
|
||||||
|
|
||||||
<Dropdown
|
|
||||||
menu={{ items: getMenuItems }}
|
|
||||||
disabled={isDashboardListLoading}
|
|
||||||
placement="bottomRight"
|
|
||||||
>
|
|
||||||
<NewDashboardButton
|
|
||||||
icon={<PlusOutlined />}
|
|
||||||
type="primary"
|
|
||||||
data-testid="create-new-dashboard"
|
|
||||||
loading={newDashboardState.loading}
|
|
||||||
danger={newDashboardState.error}
|
|
||||||
>
|
|
||||||
{getText()}
|
|
||||||
</NewDashboardButton>
|
|
||||||
</Dropdown>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
),
|
|
||||||
[
|
|
||||||
isDashboardListLoading,
|
|
||||||
handleSearch,
|
|
||||||
isFilteringDashboards,
|
|
||||||
getMenuItems,
|
|
||||||
newDashboardState.loading,
|
|
||||||
newDashboardState.error,
|
|
||||||
getText,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card>
|
|
||||||
{GetHeader}
|
|
||||||
|
|
||||||
<TableContainer>
|
|
||||||
<ImportJSON
|
|
||||||
isImportJSONModalVisible={isImportJSONModalVisible}
|
|
||||||
uploadedGrafana={uploadedGrafana}
|
|
||||||
onModalHandler={(): void => onModalHandler(false)}
|
|
||||||
/>
|
|
||||||
<DynamicColumnTable
|
|
||||||
tablesource={TableDataSource.Dashboard}
|
|
||||||
dynamicColumns={dynamicColumns}
|
|
||||||
columns={columns}
|
|
||||||
pagination={{
|
|
||||||
pageSize: 10,
|
|
||||||
defaultPageSize: 10,
|
|
||||||
total: data?.length || 0,
|
|
||||||
}}
|
|
||||||
showHeader
|
|
||||||
bordered
|
|
||||||
sticky
|
|
||||||
loading={isDashboardListLoading}
|
|
||||||
dataSource={data}
|
|
||||||
showSorterTooltip
|
|
||||||
/>
|
|
||||||
</TableContainer>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Data {
|
|
||||||
key: Key;
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
tags: string[];
|
|
||||||
createdBy: string;
|
|
||||||
createdAt: string;
|
|
||||||
lastUpdatedTime: string;
|
|
||||||
lastUpdatedBy: string;
|
|
||||||
isLocked: boolean;
|
|
||||||
id: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ListOfAllDashboard;
|
|
||||||
|
@ -8,9 +8,10 @@ export const getWidgetQueryBuilder = ({
|
|||||||
title = '',
|
title = '',
|
||||||
panelTypes,
|
panelTypes,
|
||||||
yAxisUnit = '',
|
yAxisUnit = '',
|
||||||
|
id,
|
||||||
}: GetWidgetQueryBuilderProps): Widgets => ({
|
}: GetWidgetQueryBuilderProps): Widgets => ({
|
||||||
description: '',
|
description: '',
|
||||||
id: v4(),
|
id: id || v4(),
|
||||||
isStacked: false,
|
isStacked: false,
|
||||||
nullZeroValues: '',
|
nullZeroValues: '',
|
||||||
opacity: '0',
|
opacity: '0',
|
||||||
|
@ -16,7 +16,7 @@ import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
|||||||
import { EQueryType } from 'types/common/dashboard';
|
import { EQueryType } from 'types/common/dashboard';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
import { GraphTitle, MENU_ITEMS } from '../constant';
|
import { GraphTitle, MENU_ITEMS, SERVICE_CHART_ID } from '../constant';
|
||||||
import { getWidgetQueryBuilder } from '../MetricsApplication.factory';
|
import { getWidgetQueryBuilder } from '../MetricsApplication.factory';
|
||||||
import { Card, GraphContainer, Row } from '../styles';
|
import { Card, GraphContainer, Row } from '../styles';
|
||||||
import { Button } from './styles';
|
import { Button } from './styles';
|
||||||
@ -66,6 +66,7 @@ function DBCall(): JSX.Element {
|
|||||||
title: GraphTitle.DATABASE_CALLS_RPS,
|
title: GraphTitle.DATABASE_CALLS_RPS,
|
||||||
panelTypes: PANEL_TYPES.TIME_SERIES,
|
panelTypes: PANEL_TYPES.TIME_SERIES,
|
||||||
yAxisUnit: 'reqps',
|
yAxisUnit: 'reqps',
|
||||||
|
id: SERVICE_CHART_ID.dbCallsRPS,
|
||||||
}),
|
}),
|
||||||
[servicename, tagFilterItems],
|
[servicename, tagFilterItems],
|
||||||
);
|
);
|
||||||
@ -85,6 +86,7 @@ function DBCall(): JSX.Element {
|
|||||||
title: GraphTitle.DATABASE_CALLS_AVG_DURATION,
|
title: GraphTitle.DATABASE_CALLS_AVG_DURATION,
|
||||||
panelTypes: PANEL_TYPES.TIME_SERIES,
|
panelTypes: PANEL_TYPES.TIME_SERIES,
|
||||||
yAxisUnit: 'ms',
|
yAxisUnit: 'ms',
|
||||||
|
id: SERVICE_CHART_ID.dbCallsAvgDuration,
|
||||||
}),
|
}),
|
||||||
[servicename, tagFilterItems],
|
[servicename, tagFilterItems],
|
||||||
);
|
);
|
||||||
|
@ -17,7 +17,7 @@ import { useParams } from 'react-router-dom';
|
|||||||
import { EQueryType } from 'types/common/dashboard';
|
import { EQueryType } from 'types/common/dashboard';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
import { GraphTitle, legend, MENU_ITEMS } from '../constant';
|
import { GraphTitle, legend, MENU_ITEMS, SERVICE_CHART_ID } from '../constant';
|
||||||
import { getWidgetQueryBuilder } from '../MetricsApplication.factory';
|
import { getWidgetQueryBuilder } from '../MetricsApplication.factory';
|
||||||
import { Card, GraphContainer, Row } from '../styles';
|
import { Card, GraphContainer, Row } from '../styles';
|
||||||
import { Button } from './styles';
|
import { Button } from './styles';
|
||||||
@ -57,6 +57,7 @@ function External(): JSX.Element {
|
|||||||
title: GraphTitle.EXTERNAL_CALL_ERROR_PERCENTAGE,
|
title: GraphTitle.EXTERNAL_CALL_ERROR_PERCENTAGE,
|
||||||
panelTypes: PANEL_TYPES.TIME_SERIES,
|
panelTypes: PANEL_TYPES.TIME_SERIES,
|
||||||
yAxisUnit: '%',
|
yAxisUnit: '%',
|
||||||
|
id: SERVICE_CHART_ID.externalCallErrorPercentage,
|
||||||
}),
|
}),
|
||||||
[servicename, tagFilterItems],
|
[servicename, tagFilterItems],
|
||||||
);
|
);
|
||||||
@ -82,6 +83,7 @@ function External(): JSX.Element {
|
|||||||
title: GraphTitle.EXTERNAL_CALL_DURATION,
|
title: GraphTitle.EXTERNAL_CALL_DURATION,
|
||||||
panelTypes: PANEL_TYPES.TIME_SERIES,
|
panelTypes: PANEL_TYPES.TIME_SERIES,
|
||||||
yAxisUnit: 'ms',
|
yAxisUnit: 'ms',
|
||||||
|
id: SERVICE_CHART_ID.externalCallDuration,
|
||||||
}),
|
}),
|
||||||
[servicename, tagFilterItems],
|
[servicename, tagFilterItems],
|
||||||
);
|
);
|
||||||
@ -103,6 +105,7 @@ function External(): JSX.Element {
|
|||||||
title: GraphTitle.EXTERNAL_CALL_RPS_BY_ADDRESS,
|
title: GraphTitle.EXTERNAL_CALL_RPS_BY_ADDRESS,
|
||||||
panelTypes: PANEL_TYPES.TIME_SERIES,
|
panelTypes: PANEL_TYPES.TIME_SERIES,
|
||||||
yAxisUnit: 'reqps',
|
yAxisUnit: 'reqps',
|
||||||
|
id: SERVICE_CHART_ID.externalCallRPSByAddress,
|
||||||
}),
|
}),
|
||||||
[servicename, tagFilterItems],
|
[servicename, tagFilterItems],
|
||||||
);
|
);
|
||||||
@ -124,6 +127,7 @@ function External(): JSX.Element {
|
|||||||
title: GraphTitle.EXTERNAL_CALL_DURATION_BY_ADDRESS,
|
title: GraphTitle.EXTERNAL_CALL_DURATION_BY_ADDRESS,
|
||||||
panelTypes: PANEL_TYPES.TIME_SERIES,
|
panelTypes: PANEL_TYPES.TIME_SERIES,
|
||||||
yAxisUnit: 'ms',
|
yAxisUnit: 'ms',
|
||||||
|
id: SERVICE_CHART_ID.externalCallDurationByAddress,
|
||||||
}),
|
}),
|
||||||
[servicename, tagFilterItems],
|
[servicename, tagFilterItems],
|
||||||
);
|
);
|
||||||
|
@ -26,7 +26,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
|
|||||||
import { Tags } from 'types/reducer/trace';
|
import { Tags } from 'types/reducer/trace';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
import { GraphTitle } from '../constant';
|
import { GraphTitle, SERVICE_CHART_ID } from '../constant';
|
||||||
import { getWidgetQueryBuilder } from '../MetricsApplication.factory';
|
import { getWidgetQueryBuilder } from '../MetricsApplication.factory';
|
||||||
import {
|
import {
|
||||||
errorPercentage,
|
errorPercentage,
|
||||||
@ -131,6 +131,7 @@ function Application(): JSX.Element {
|
|||||||
title: GraphTitle.RATE_PER_OPS,
|
title: GraphTitle.RATE_PER_OPS,
|
||||||
panelTypes: PANEL_TYPES.TIME_SERIES,
|
panelTypes: PANEL_TYPES.TIME_SERIES,
|
||||||
yAxisUnit: 'ops',
|
yAxisUnit: 'ops',
|
||||||
|
id: SERVICE_CHART_ID.rps,
|
||||||
}),
|
}),
|
||||||
[servicename, tagFilterItems, topLevelOperationsRoute],
|
[servicename, tagFilterItems, topLevelOperationsRoute],
|
||||||
);
|
);
|
||||||
@ -152,6 +153,7 @@ function Application(): JSX.Element {
|
|||||||
title: GraphTitle.ERROR_PERCENTAGE,
|
title: GraphTitle.ERROR_PERCENTAGE,
|
||||||
panelTypes: PANEL_TYPES.TIME_SERIES,
|
panelTypes: PANEL_TYPES.TIME_SERIES,
|
||||||
yAxisUnit: '%',
|
yAxisUnit: '%',
|
||||||
|
id: SERVICE_CHART_ID.errorPercentage,
|
||||||
}),
|
}),
|
||||||
[servicename, tagFilterItems, topLevelOperationsRoute],
|
[servicename, tagFilterItems, topLevelOperationsRoute],
|
||||||
);
|
);
|
||||||
|
@ -8,7 +8,10 @@ import {
|
|||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import Graph from 'container/GridCardLayout/GridCard';
|
import Graph from 'container/GridCardLayout/GridCard';
|
||||||
import DisplayThreshold from 'container/GridCardLayout/WidgetHeader/DisplayThreshold';
|
import DisplayThreshold from 'container/GridCardLayout/WidgetHeader/DisplayThreshold';
|
||||||
import { GraphTitle } from 'container/MetricsApplication/constant';
|
import {
|
||||||
|
GraphTitle,
|
||||||
|
SERVICE_CHART_ID,
|
||||||
|
} from 'container/MetricsApplication/constant';
|
||||||
import { getWidgetQueryBuilder } from 'container/MetricsApplication/MetricsApplication.factory';
|
import { getWidgetQueryBuilder } from 'container/MetricsApplication/MetricsApplication.factory';
|
||||||
import { apDexMetricsQueryBuilderQueries } from 'container/MetricsApplication/MetricsPageQueries/OverviewQueries';
|
import { apDexMetricsQueryBuilderQueries } from 'container/MetricsApplication/MetricsPageQueries/OverviewQueries';
|
||||||
import { ReactNode, useMemo } from 'react';
|
import { ReactNode, useMemo } from 'react';
|
||||||
@ -59,6 +62,7 @@ function ApDexMetrics({
|
|||||||
</Space>
|
</Space>
|
||||||
),
|
),
|
||||||
panelTypes: PANEL_TYPES.TIME_SERIES,
|
panelTypes: PANEL_TYPES.TIME_SERIES,
|
||||||
|
id: SERVICE_CHART_ID.apdex,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
delta,
|
delta,
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
import { FeatureKeys } from 'constants/features';
|
import { FeatureKeys } from 'constants/features';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import Graph from 'container/GridCardLayout/GridCard';
|
import Graph from 'container/GridCardLayout/GridCard';
|
||||||
import { GraphTitle } from 'container/MetricsApplication/constant';
|
import {
|
||||||
|
GraphTitle,
|
||||||
|
SERVICE_CHART_ID,
|
||||||
|
} from 'container/MetricsApplication/constant';
|
||||||
import { getWidgetQueryBuilder } from 'container/MetricsApplication/MetricsApplication.factory';
|
import { getWidgetQueryBuilder } from 'container/MetricsApplication/MetricsApplication.factory';
|
||||||
import { latency } from 'container/MetricsApplication/MetricsPageQueries/OverviewQueries';
|
import { latency } from 'container/MetricsApplication/MetricsPageQueries/OverviewQueries';
|
||||||
import { Card, GraphContainer } from 'container/MetricsApplication/styles';
|
import { Card, GraphContainer } from 'container/MetricsApplication/styles';
|
||||||
@ -59,6 +62,7 @@ function ServiceOverview({
|
|||||||
title: GraphTitle.LATENCY,
|
title: GraphTitle.LATENCY,
|
||||||
panelTypes: PANEL_TYPES.TIME_SERIES,
|
panelTypes: PANEL_TYPES.TIME_SERIES,
|
||||||
yAxisUnit: 'ns',
|
yAxisUnit: 'ns',
|
||||||
|
id: SERVICE_CHART_ID.latency,
|
||||||
}),
|
}),
|
||||||
[servicename, isSpanMetricEnable, topLevelOperationsRoute, tagFilterItems],
|
[servicename, isSpanMetricEnable, topLevelOperationsRoute, tagFilterItems],
|
||||||
);
|
);
|
||||||
|
@ -79,3 +79,17 @@ export const topOperationMetricsDownloadOptions: DownloadOptions = {
|
|||||||
isDownloadEnabled: true,
|
isDownloadEnabled: true,
|
||||||
fileName: 'top-operation',
|
fileName: 'top-operation',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
export const SERVICE_CHART_ID = {
|
||||||
|
latency: 'SERVICE_OVERVIEW_LATENCY',
|
||||||
|
error: 'SERVICE_OVERVIEW_ERROR',
|
||||||
|
rps: 'SERVICE_OVERVIEW_RPS',
|
||||||
|
apdex: 'SERVICE_OVERVIEW_APDEX',
|
||||||
|
errorPercentage: 'SERVICE_OVERVIEW_ERROR_PERCENTAGE',
|
||||||
|
dbCallsRPS: 'SERVICE_DATABASE_CALLS_RPS',
|
||||||
|
dbCallsAvgDuration: 'SERVICE_DATABASE_CALLS_AVG_DURATION',
|
||||||
|
externalCallDurationByAddress: 'SERVICE_EXTERNAL_CALLS_DURATION_BY_ADDRESS',
|
||||||
|
externalCallErrorPercentage: 'SERVICE_EXTERNAL_CALLS_ERROR_PERCENTAGE',
|
||||||
|
externalCallDuration: 'SERVICE_EXTERNAL_CALLS_DURATION',
|
||||||
|
externalCallRPSByAddress: 'SERVICE_EXTERNAL_CALLS_RPS_BY_ADDRESS',
|
||||||
|
};
|
||||||
|
@ -9,6 +9,7 @@ export interface GetWidgetQueryBuilderProps {
|
|||||||
title?: ReactNode;
|
title?: ReactNode;
|
||||||
panelTypes: Widgets['panelTypes'];
|
panelTypes: Widgets['panelTypes'];
|
||||||
yAxisUnit?: Widgets['yAxisUnit'];
|
yAxisUnit?: Widgets['yAxisUnit'];
|
||||||
|
id?: Widgets['id'];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NavigateToTraceProps {
|
export interface NavigateToTraceProps {
|
||||||
|
@ -50,7 +50,7 @@ function DashboardDescription(): JSX.Element {
|
|||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<Row gutter={16}>
|
<Row gutter={16}>
|
||||||
<Col flex={1} span={12}>
|
<Col flex={1} span={9}>
|
||||||
<Typography.Title
|
<Typography.Title
|
||||||
level={4}
|
level={4}
|
||||||
style={{ padding: 0, margin: 0 }}
|
style={{ padding: 0, margin: 0 }}
|
||||||
@ -80,12 +80,12 @@ function DashboardDescription(): JSX.Element {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={8}>
|
<Col span={12}>
|
||||||
<Row justify="end">
|
<Row justify="end">
|
||||||
<DashboardVariableSelection />
|
<DashboardVariableSelection />
|
||||||
</Row>
|
</Row>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={4} style={{ textAlign: 'right' }}>
|
<Col span={3} style={{ textAlign: 'right' }}>
|
||||||
{selectedData && (
|
{selectedData && (
|
||||||
<ShareModal
|
<ShareModal
|
||||||
isJSONModalVisible={openDashboardJSON}
|
isJSONModalVisible={openDashboardJSON}
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
.query-container {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row wrap;
|
||||||
|
min-width: 0;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
@ -1,21 +1,16 @@
|
|||||||
/* eslint-disable sonarjs/cognitive-complexity */
|
/* eslint-disable sonarjs/cognitive-complexity */
|
||||||
|
import './VariableItem.styles.scss';
|
||||||
|
|
||||||
import { orange } from '@ant-design/colors';
|
import { orange } from '@ant-design/colors';
|
||||||
import {
|
import { Button, Divider, Input, Select, Switch, Tag, Typography } from 'antd';
|
||||||
Button,
|
import dashboardVariablesQuery from 'api/dashboard/variables/dashboardVariablesQuery';
|
||||||
Col,
|
|
||||||
Divider,
|
|
||||||
Input,
|
|
||||||
Select,
|
|
||||||
Switch,
|
|
||||||
Tag,
|
|
||||||
Typography,
|
|
||||||
} from 'antd';
|
|
||||||
import query from 'api/dashboard/variables/query';
|
|
||||||
import Editor from 'components/Editor';
|
import Editor from 'components/Editor';
|
||||||
|
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||||
import { commaValuesParser } from 'lib/dashbaordVariables/customCommaValuesParser';
|
import { commaValuesParser } from 'lib/dashbaordVariables/customCommaValuesParser';
|
||||||
import sortValues from 'lib/dashbaordVariables/sortVariableValues';
|
import sortValues from 'lib/dashbaordVariables/sortVariableValues';
|
||||||
import { map } from 'lodash-es';
|
import { map } from 'lodash-es';
|
||||||
import { useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { useQuery } from 'react-query';
|
||||||
import {
|
import {
|
||||||
IDashboardVariable,
|
IDashboardVariable,
|
||||||
TSortVariableValuesType,
|
TSortVariableValuesType,
|
||||||
@ -79,8 +74,6 @@ function VariableItem({
|
|||||||
);
|
);
|
||||||
const [previewValues, setPreviewValues] = useState<string[]>([]);
|
const [previewValues, setPreviewValues] = useState<string[]>([]);
|
||||||
|
|
||||||
// Internal states
|
|
||||||
const [previewLoading, setPreviewLoading] = useState<boolean>(false);
|
|
||||||
// Error messages
|
// Error messages
|
||||||
const [errorName, setErrorName] = useState<boolean>(false);
|
const [errorName, setErrorName] = useState<boolean>(false);
|
||||||
const [errorPreview, setErrorPreview] = useState<string | null>(null);
|
const [errorPreview, setErrorPreview] = useState<string | null>(null);
|
||||||
@ -131,37 +124,56 @@ function VariableItem({
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Fetches the preview values for the SQL variable query
|
// Fetches the preview values for the SQL variable query
|
||||||
const handleQueryResult = async (): Promise<void> => {
|
const handleQueryResult = (response: any): void => {
|
||||||
setPreviewLoading(true);
|
if (response?.payload?.variableValues)
|
||||||
setErrorPreview(null);
|
setPreviewValues(
|
||||||
try {
|
sortValues(
|
||||||
const variableQueryResponse = await query({
|
response.payload?.variableValues || [],
|
||||||
query: variableQueryValue,
|
variableSortType,
|
||||||
|
) as never,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const { isFetching: previewLoading, refetch: runQuery } = useQuery(
|
||||||
|
[REACT_QUERY_KEY.DASHBOARD_BY_ID, variableData.name, variableName],
|
||||||
|
{
|
||||||
|
enabled: false,
|
||||||
|
queryFn: () =>
|
||||||
|
dashboardVariablesQuery({
|
||||||
|
query: variableData.queryValue || '',
|
||||||
variables: variablePropsToPayloadVariables(existingVariables),
|
variables: variablePropsToPayloadVariables(existingVariables),
|
||||||
});
|
}),
|
||||||
setPreviewLoading(false);
|
refetchOnWindowFocus: false,
|
||||||
if (variableQueryResponse.error) {
|
onSuccess: (response) => {
|
||||||
let message = variableQueryResponse.error;
|
handleQueryResult(response);
|
||||||
if (variableQueryResponse.error.includes('Syntax error:')) {
|
},
|
||||||
|
onError: (error: {
|
||||||
|
details: {
|
||||||
|
error: string;
|
||||||
|
};
|
||||||
|
}) => {
|
||||||
|
const { details } = error;
|
||||||
|
|
||||||
|
if (details.error) {
|
||||||
|
let message = details.error;
|
||||||
|
if (details.error.includes('Syntax error:')) {
|
||||||
message =
|
message =
|
||||||
'Please make sure query is valid and dependent variables are selected';
|
'Please make sure query is valid and dependent variables are selected';
|
||||||
}
|
}
|
||||||
setErrorPreview(message);
|
setErrorPreview(message);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
if (variableQueryResponse.payload?.variableValues)
|
},
|
||||||
setPreviewValues(
|
},
|
||||||
sortValues(
|
|
||||||
variableQueryResponse.payload?.variableValues || [],
|
|
||||||
variableSortType,
|
|
||||||
) as never,
|
|
||||||
);
|
);
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
const handleTestRunQuery = useCallback(() => {
|
||||||
}
|
runQuery();
|
||||||
};
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col>
|
<div className="variable-item-container">
|
||||||
|
<div className="variable-item-content">
|
||||||
{/* <Typography.Title level={3}>Add Variable</Typography.Title> */}
|
{/* <Typography.Title level={3}>Add Variable</Typography.Title> */}
|
||||||
<VariableItemRow>
|
<VariableItemRow>
|
||||||
<LabelContainer>
|
<LabelContainer>
|
||||||
@ -223,7 +235,7 @@ function VariableItem({
|
|||||||
Options
|
Options
|
||||||
</Typography.Title>
|
</Typography.Title>
|
||||||
{queryType === 'QUERY' && (
|
{queryType === 'QUERY' && (
|
||||||
<VariableItemRow>
|
<div className="query-container">
|
||||||
<LabelContainer>
|
<LabelContainer>
|
||||||
<Typography>Query</Typography>
|
<Typography>Query</Typography>
|
||||||
</LabelContainer>
|
</LabelContainer>
|
||||||
@ -233,11 +245,24 @@ function VariableItem({
|
|||||||
language="sql"
|
language="sql"
|
||||||
value={variableQueryValue}
|
value={variableQueryValue}
|
||||||
onChange={(e): void => setVariableQueryValue(e)}
|
onChange={(e): void => setVariableQueryValue(e)}
|
||||||
height="300px"
|
height="240px"
|
||||||
|
options={{
|
||||||
|
fontSize: 13,
|
||||||
|
wordWrap: 'on',
|
||||||
|
lineNumbers: 'off',
|
||||||
|
glyphMargin: false,
|
||||||
|
folding: false,
|
||||||
|
lineDecorationsWidth: 0,
|
||||||
|
lineNumbersMinChars: 0,
|
||||||
|
minimap: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
onClick={handleQueryResult}
|
size="small"
|
||||||
|
onClick={handleTestRunQuery}
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
@ -247,7 +272,7 @@ function VariableItem({
|
|||||||
Test Run Query
|
Test Run Query
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</VariableItemRow>
|
</div>
|
||||||
)}
|
)}
|
||||||
{queryType === 'CUSTOM' && (
|
{queryType === 'CUSTOM' && (
|
||||||
<VariableItemRow>
|
<VariableItemRow>
|
||||||
@ -347,16 +372,20 @@ function VariableItem({
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="variable-item-footer">
|
||||||
<Divider />
|
<Divider />
|
||||||
<VariableItemRow>
|
<VariableItemRow>
|
||||||
<Button type="primary" onClick={handleSave} disabled={errorName}>
|
<Button type="primary" onClick={handleSave} disabled={errorName}>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="dashed" onClick={onCancel}>
|
<Button type="default" onClick={onCancel}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
</VariableItemRow>
|
</VariableItemRow>
|
||||||
</Col>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ import { Button, Modal, Row, Space, Tag } from 'antd';
|
|||||||
import { ResizeTable } from 'components/ResizeTable';
|
import { ResizeTable } from 'components/ResizeTable';
|
||||||
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
|
import { PencilIcon, TrashIcon } from 'lucide-react';
|
||||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||||
import { useRef, useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@ -134,7 +135,7 @@ function VariablesSetting(): JSX.Element {
|
|||||||
key: 'name',
|
key: 'name',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Definition',
|
title: 'Description',
|
||||||
dataIndex: 'description',
|
dataIndex: 'description',
|
||||||
width: 100,
|
width: 100,
|
||||||
key: 'description',
|
key: 'description',
|
||||||
@ -147,19 +148,19 @@ function VariablesSetting(): JSX.Element {
|
|||||||
<Space>
|
<Space>
|
||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
style={{ padding: 0, cursor: 'pointer', color: blue[5] }}
|
style={{ padding: 8, cursor: 'pointer', color: blue[5] }}
|
||||||
onClick={(): void => onVariableViewModeEnter('EDIT', _)}
|
onClick={(): void => onVariableViewModeEnter('EDIT', _)}
|
||||||
>
|
>
|
||||||
Edit
|
<PencilIcon size={14} />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
style={{ padding: 0, color: red[6], cursor: 'pointer' }}
|
style={{ padding: 8, color: red[6], cursor: 'pointer' }}
|
||||||
onClick={(): void => {
|
onClick={(): void => {
|
||||||
if (_.name) onVariableDeleteHandler(_.name);
|
if (_.name) onVariableDeleteHandler(_.name);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Delete
|
<TrashIcon size={14} />
|
||||||
</Button>
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
),
|
),
|
||||||
@ -187,9 +188,10 @@ function VariablesSetting(): JSX.Element {
|
|||||||
onVariableViewModeEnter('ADD', {} as IDashboardVariable)
|
onVariableViewModeEnter('ADD', {} as IDashboardVariable)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<PlusOutlined /> New Variables
|
<PlusOutlined /> Add Variable
|
||||||
</Button>
|
</Button>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<ResizeTable columns={columns} dataSource={variablesTableData} />
|
<ResizeTable columns={columns} dataSource={variablesTableData} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@ -13,7 +13,7 @@ function DashboardSettingsContent(): JSX.Element {
|
|||||||
{ label: 'Variables', key: 'variables', children: <VariablesSetting /> },
|
{ label: 'Variables', key: 'variables', children: <VariablesSetting /> },
|
||||||
];
|
];
|
||||||
|
|
||||||
return <Tabs items={items} />;
|
return <Tabs items={items} animated />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DashboardSettingsContent;
|
export default DashboardSettingsContent;
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
.variable-name {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
min-width: 100px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
color: gray;
|
||||||
|
}
|
@ -0,0 +1,110 @@
|
|||||||
|
import { Row } from 'antd';
|
||||||
|
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||||
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
|
import { map, sortBy } from 'lodash-es';
|
||||||
|
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||||
|
import { memo, useState } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
|
import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||||
|
import AppReducer from 'types/reducer/app';
|
||||||
|
|
||||||
|
import VariableItem from './VariableItem';
|
||||||
|
|
||||||
|
function DashboardVariableSelection(): JSX.Element | null {
|
||||||
|
const { selectedDashboard, setSelectedDashboard } = useDashboard();
|
||||||
|
|
||||||
|
const { data } = selectedDashboard || {};
|
||||||
|
|
||||||
|
const { variables } = data || {};
|
||||||
|
|
||||||
|
const [update, setUpdate] = useState<boolean>(false);
|
||||||
|
const [lastUpdatedVar, setLastUpdatedVar] = useState<string>('');
|
||||||
|
|
||||||
|
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||||
|
|
||||||
|
const onVarChanged = (name: string): void => {
|
||||||
|
setLastUpdatedVar(name);
|
||||||
|
setUpdate(!update);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateMutation = useUpdateDashboard();
|
||||||
|
const { notifications } = useNotifications();
|
||||||
|
|
||||||
|
const updateVariables = (
|
||||||
|
name: string,
|
||||||
|
updatedVariablesData: Dashboard['data']['variables'],
|
||||||
|
): void => {
|
||||||
|
if (!selectedDashboard) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMutation.mutateAsync(
|
||||||
|
{
|
||||||
|
...selectedDashboard,
|
||||||
|
data: {
|
||||||
|
...selectedDashboard.data,
|
||||||
|
variables: updatedVariablesData,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: (updatedDashboard) => {
|
||||||
|
if (updatedDashboard.payload) {
|
||||||
|
setSelectedDashboard(updatedDashboard.payload);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
notifications.error({
|
||||||
|
message: `Error updating ${name} variable`,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onValueUpdate = (
|
||||||
|
name: string,
|
||||||
|
value: IDashboardVariable['selectedValue'],
|
||||||
|
allSelected: boolean,
|
||||||
|
): void => {
|
||||||
|
const updatedVariablesData = { ...variables };
|
||||||
|
updatedVariablesData[name].selectedValue = value;
|
||||||
|
updatedVariablesData[name].allSelected = allSelected;
|
||||||
|
|
||||||
|
console.log('onValue Update', name);
|
||||||
|
|
||||||
|
if (role !== 'VIEWER' && selectedDashboard) {
|
||||||
|
updateVariables(name, updatedVariablesData);
|
||||||
|
}
|
||||||
|
onVarChanged(name);
|
||||||
|
|
||||||
|
setUpdate(!update);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!variables) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const variablesKeys = sortBy(Object.keys(variables));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Row>
|
||||||
|
{variablesKeys &&
|
||||||
|
map(variablesKeys, (variableName) => (
|
||||||
|
<VariableItem
|
||||||
|
key={`${variableName}${variables[variableName].modificationUUID}`}
|
||||||
|
existingVariables={variables}
|
||||||
|
lastUpdatedVar={lastUpdatedVar}
|
||||||
|
variableData={{
|
||||||
|
name: variableName,
|
||||||
|
...variables[variableName],
|
||||||
|
change: update,
|
||||||
|
}}
|
||||||
|
onValueUpdate={onValueUpdate}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(DashboardVariableSelection);
|
@ -1,6 +1,13 @@
|
|||||||
import '@testing-library/jest-dom/extend-expect';
|
import '@testing-library/jest-dom/extend-expect';
|
||||||
|
|
||||||
import { fireEvent, render, screen } from '@testing-library/react';
|
import {
|
||||||
|
act,
|
||||||
|
fireEvent,
|
||||||
|
render,
|
||||||
|
screen,
|
||||||
|
waitFor,
|
||||||
|
} from '@testing-library/react';
|
||||||
|
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||||
|
|
||||||
@ -25,7 +32,6 @@ const mockCustomVariableData: IDashboardVariable = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const mockOnValueUpdate = jest.fn();
|
const mockOnValueUpdate = jest.fn();
|
||||||
const mockOnAllSelectedUpdate = jest.fn();
|
|
||||||
|
|
||||||
describe('VariableItem', () => {
|
describe('VariableItem', () => {
|
||||||
let useEffectSpy: jest.SpyInstance;
|
let useEffectSpy: jest.SpyInstance;
|
||||||
@ -41,13 +47,14 @@ describe('VariableItem', () => {
|
|||||||
|
|
||||||
test('renders component with default props', () => {
|
test('renders component with default props', () => {
|
||||||
render(
|
render(
|
||||||
|
<MockQueryClientProvider>
|
||||||
<VariableItem
|
<VariableItem
|
||||||
variableData={mockVariableData}
|
variableData={mockVariableData}
|
||||||
existingVariables={{}}
|
existingVariables={{}}
|
||||||
onValueUpdate={mockOnValueUpdate}
|
onValueUpdate={mockOnValueUpdate}
|
||||||
onAllSelectedUpdate={mockOnAllSelectedUpdate}
|
|
||||||
lastUpdatedVar=""
|
lastUpdatedVar=""
|
||||||
/>,
|
/>
|
||||||
|
</MockQueryClientProvider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.getByText('$testVariable')).toBeInTheDocument();
|
expect(screen.getByText('$testVariable')).toBeInTheDocument();
|
||||||
@ -55,45 +62,55 @@ describe('VariableItem', () => {
|
|||||||
|
|
||||||
test('renders Input when the variable type is TEXTBOX', () => {
|
test('renders Input when the variable type is TEXTBOX', () => {
|
||||||
render(
|
render(
|
||||||
|
<MockQueryClientProvider>
|
||||||
<VariableItem
|
<VariableItem
|
||||||
variableData={mockVariableData}
|
variableData={mockVariableData}
|
||||||
existingVariables={{}}
|
existingVariables={{}}
|
||||||
onValueUpdate={mockOnValueUpdate}
|
onValueUpdate={mockOnValueUpdate}
|
||||||
onAllSelectedUpdate={mockOnAllSelectedUpdate}
|
|
||||||
lastUpdatedVar=""
|
lastUpdatedVar=""
|
||||||
/>,
|
/>
|
||||||
|
</MockQueryClientProvider>,
|
||||||
);
|
);
|
||||||
expect(screen.getByPlaceholderText('Enter value')).toBeInTheDocument();
|
expect(screen.getByPlaceholderText('Enter value')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('calls onChange event handler when Input value changes', () => {
|
test('calls onChange event handler when Input value changes', async () => {
|
||||||
render(
|
render(
|
||||||
|
<MockQueryClientProvider>
|
||||||
<VariableItem
|
<VariableItem
|
||||||
variableData={mockVariableData}
|
variableData={mockVariableData}
|
||||||
existingVariables={{}}
|
existingVariables={{}}
|
||||||
onValueUpdate={mockOnValueUpdate}
|
onValueUpdate={mockOnValueUpdate}
|
||||||
onAllSelectedUpdate={mockOnAllSelectedUpdate}
|
|
||||||
lastUpdatedVar=""
|
lastUpdatedVar=""
|
||||||
/>,
|
/>
|
||||||
|
</MockQueryClientProvider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
const inputElement = screen.getByPlaceholderText('Enter value');
|
const inputElement = screen.getByPlaceholderText('Enter value');
|
||||||
fireEvent.change(inputElement, { target: { value: 'newValue' } });
|
fireEvent.change(inputElement, { target: { value: 'newValue' } });
|
||||||
|
});
|
||||||
|
|
||||||
expect(mockOnValueUpdate).toHaveBeenCalledTimes(1);
|
await waitFor(() => {
|
||||||
expect(mockOnValueUpdate).toHaveBeenCalledWith('testVariable', 'newValue');
|
// expect(mockOnValueUpdate).toHaveBeenCalledTimes(1);
|
||||||
expect(mockOnAllSelectedUpdate).toHaveBeenCalledTimes(1);
|
expect(mockOnValueUpdate).toHaveBeenCalledWith(
|
||||||
expect(mockOnAllSelectedUpdate).toHaveBeenCalledWith('testVariable', false);
|
'testVariable',
|
||||||
|
'newValue',
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('renders a Select element when variable type is CUSTOM', () => {
|
test('renders a Select element when variable type is CUSTOM', () => {
|
||||||
render(
|
render(
|
||||||
|
<MockQueryClientProvider>
|
||||||
<VariableItem
|
<VariableItem
|
||||||
variableData={mockCustomVariableData}
|
variableData={mockCustomVariableData}
|
||||||
existingVariables={{}}
|
existingVariables={{}}
|
||||||
onValueUpdate={mockOnValueUpdate}
|
onValueUpdate={mockOnValueUpdate}
|
||||||
onAllSelectedUpdate={mockOnAllSelectedUpdate}
|
|
||||||
lastUpdatedVar=""
|
lastUpdatedVar=""
|
||||||
/>,
|
/>
|
||||||
|
</MockQueryClientProvider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.getByText('$customVariable')).toBeInTheDocument();
|
expect(screen.getByText('$customVariable')).toBeInTheDocument();
|
||||||
@ -107,13 +124,14 @@ describe('VariableItem', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render(
|
render(
|
||||||
|
<MockQueryClientProvider>
|
||||||
<VariableItem
|
<VariableItem
|
||||||
variableData={customVariableData}
|
variableData={customVariableData}
|
||||||
existingVariables={{}}
|
existingVariables={{}}
|
||||||
onValueUpdate={mockOnValueUpdate}
|
onValueUpdate={mockOnValueUpdate}
|
||||||
onAllSelectedUpdate={mockOnAllSelectedUpdate}
|
|
||||||
lastUpdatedVar=""
|
lastUpdatedVar=""
|
||||||
/>,
|
/>
|
||||||
|
</MockQueryClientProvider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.getByTitle('ALL')).toBeInTheDocument();
|
expect(screen.getByTitle('ALL')).toBeInTheDocument();
|
||||||
@ -121,48 +139,16 @@ describe('VariableItem', () => {
|
|||||||
|
|
||||||
test('calls useEffect when the component mounts', () => {
|
test('calls useEffect when the component mounts', () => {
|
||||||
render(
|
render(
|
||||||
|
<MockQueryClientProvider>
|
||||||
<VariableItem
|
<VariableItem
|
||||||
variableData={mockCustomVariableData}
|
variableData={mockCustomVariableData}
|
||||||
existingVariables={{}}
|
existingVariables={{}}
|
||||||
onValueUpdate={mockOnValueUpdate}
|
onValueUpdate={mockOnValueUpdate}
|
||||||
onAllSelectedUpdate={mockOnAllSelectedUpdate}
|
|
||||||
lastUpdatedVar=""
|
lastUpdatedVar=""
|
||||||
/>,
|
/>
|
||||||
|
</MockQueryClientProvider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(useEffect).toHaveBeenCalled();
|
expect(useEffect).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('calls useEffect only once when the component mounts', () => {
|
|
||||||
// Render the component
|
|
||||||
const { rerender } = render(
|
|
||||||
<VariableItem
|
|
||||||
variableData={mockCustomVariableData}
|
|
||||||
existingVariables={{}}
|
|
||||||
onValueUpdate={mockOnValueUpdate}
|
|
||||||
onAllSelectedUpdate={mockOnAllSelectedUpdate}
|
|
||||||
lastUpdatedVar=""
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Create an updated version of the mock data
|
|
||||||
const updatedMockCustomVariableData = {
|
|
||||||
...mockCustomVariableData,
|
|
||||||
selectedValue: 'option1',
|
|
||||||
};
|
|
||||||
|
|
||||||
// Re-render the component with the updated data
|
|
||||||
rerender(
|
|
||||||
<VariableItem
|
|
||||||
variableData={updatedMockCustomVariableData}
|
|
||||||
existingVariables={{}}
|
|
||||||
onValueUpdate={mockOnValueUpdate}
|
|
||||||
onAllSelectedUpdate={mockOnAllSelectedUpdate}
|
|
||||||
lastUpdatedVar=""
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Check if the useEffect is called with the correct arguments
|
|
||||||
expect(useEffectSpy).toHaveBeenCalledTimes(4);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
@ -1,27 +1,35 @@
|
|||||||
|
import './DashboardVariableSelection.styles.scss';
|
||||||
|
|
||||||
import { orange } from '@ant-design/colors';
|
import { orange } from '@ant-design/colors';
|
||||||
import { WarningOutlined } from '@ant-design/icons';
|
import { WarningOutlined } from '@ant-design/icons';
|
||||||
import { Input, Popover, Select, Typography } from 'antd';
|
import { Input, Popover, Select, Typography } from 'antd';
|
||||||
import query from 'api/dashboard/variables/query';
|
import dashboardVariablesQuery from 'api/dashboard/variables/dashboardVariablesQuery';
|
||||||
|
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||||
|
import useDebounce from 'hooks/useDebounce';
|
||||||
import { commaValuesParser } from 'lib/dashbaordVariables/customCommaValuesParser';
|
import { commaValuesParser } from 'lib/dashbaordVariables/customCommaValuesParser';
|
||||||
import sortValues from 'lib/dashbaordVariables/sortVariableValues';
|
import sortValues from 'lib/dashbaordVariables/sortVariableValues';
|
||||||
import map from 'lodash-es/map';
|
import map from 'lodash-es/map';
|
||||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
import { memo, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { useQuery } from 'react-query';
|
||||||
import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||||
|
import { VariableResponseProps } from 'types/api/dashboard/variables/query';
|
||||||
|
|
||||||
import { variablePropsToPayloadVariables } from '../utils';
|
import { variablePropsToPayloadVariables } from '../utils';
|
||||||
import { SelectItemStyle, VariableContainer, VariableName } from './styles';
|
import { SelectItemStyle, VariableContainer, VariableValue } from './styles';
|
||||||
import { areArraysEqual } from './util';
|
import { areArraysEqual } from './util';
|
||||||
|
|
||||||
const ALL_SELECT_VALUE = '__ALL__';
|
const ALL_SELECT_VALUE = '__ALL__';
|
||||||
|
|
||||||
|
const variableRegexPattern = /\{\{\s*?\.([^\s}]+)\s*?\}\}/g;
|
||||||
|
|
||||||
interface VariableItemProps {
|
interface VariableItemProps {
|
||||||
variableData: IDashboardVariable;
|
variableData: IDashboardVariable;
|
||||||
existingVariables: Record<string, IDashboardVariable>;
|
existingVariables: Record<string, IDashboardVariable>;
|
||||||
onValueUpdate: (
|
onValueUpdate: (
|
||||||
name: string,
|
name: string,
|
||||||
arg1: IDashboardVariable['selectedValue'],
|
arg1: IDashboardVariable['selectedValue'],
|
||||||
|
allSelected: boolean,
|
||||||
) => void;
|
) => void;
|
||||||
onAllSelectedUpdate: (name: string, arg1: boolean) => void;
|
|
||||||
lastUpdatedVar: string;
|
lastUpdatedVar: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,48 +46,74 @@ function VariableItem({
|
|||||||
variableData,
|
variableData,
|
||||||
existingVariables,
|
existingVariables,
|
||||||
onValueUpdate,
|
onValueUpdate,
|
||||||
onAllSelectedUpdate,
|
|
||||||
lastUpdatedVar,
|
lastUpdatedVar,
|
||||||
}: VariableItemProps): JSX.Element {
|
}: VariableItemProps): JSX.Element {
|
||||||
const [optionsData, setOptionsData] = useState<(string | number | boolean)[]>(
|
const [optionsData, setOptionsData] = useState<(string | number | boolean)[]>(
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
|
||||||
|
const [variableValue, setVaribleValue] = useState(
|
||||||
|
variableData?.selectedValue?.toString() || '',
|
||||||
|
);
|
||||||
|
|
||||||
|
const debouncedVariableValue = useDebounce(variableValue, 500);
|
||||||
|
|
||||||
const [errorMessage, setErrorMessage] = useState<null | string>(null);
|
const [errorMessage, setErrorMessage] = useState<null | string>(null);
|
||||||
|
|
||||||
/* eslint-disable sonarjs/cognitive-complexity */
|
useEffect(() => {
|
||||||
const getOptions = useCallback(async (): Promise<void> => {
|
const { selectedValue } = variableData;
|
||||||
if (variableData.type === 'QUERY') {
|
|
||||||
try {
|
|
||||||
setErrorMessage(null);
|
|
||||||
setIsLoading(true);
|
|
||||||
|
|
||||||
const response = await query({
|
if (selectedValue) {
|
||||||
query: variableData.queryValue || '',
|
setVaribleValue(selectedValue?.toString());
|
||||||
variables: variablePropsToPayloadVariables(existingVariables),
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [variableData]);
|
||||||
|
|
||||||
|
const getDependentVariables = (queryValue: string): string[] => {
|
||||||
|
const matches = queryValue.match(variableRegexPattern);
|
||||||
|
|
||||||
|
// Extract variable names from the matches array without {{ . }}
|
||||||
|
return matches
|
||||||
|
? matches.map((match) => match.replace(variableRegexPattern, '$1'))
|
||||||
|
: [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getQueryKey = (variableData: IDashboardVariable): string[] => {
|
||||||
|
let dependentVariablesStr = '';
|
||||||
|
|
||||||
|
const dependentVariables = getDependentVariables(
|
||||||
|
variableData.queryValue || '',
|
||||||
|
);
|
||||||
|
|
||||||
|
const variableName = variableData.name || '';
|
||||||
|
|
||||||
|
dependentVariables?.forEach((element) => {
|
||||||
|
dependentVariablesStr += `${element}${existingVariables[element]?.selectedValue}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
setIsLoading(false);
|
const variableKey = dependentVariablesStr.replace(/\s/g, '');
|
||||||
if (response.error) {
|
|
||||||
let message = response.error;
|
return [REACT_QUERY_KEY.DASHBOARD_BY_ID, variableName, variableKey];
|
||||||
if (response.error.includes('Syntax error:')) {
|
};
|
||||||
message =
|
|
||||||
'Please make sure query is valid and dependent variables are selected';
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
}
|
const getOptions = (variablesRes: VariableResponseProps | null): void => {
|
||||||
setErrorMessage(message);
|
if (variablesRes && variableData.type === 'QUERY') {
|
||||||
return;
|
try {
|
||||||
}
|
setErrorMessage(null);
|
||||||
if (response.payload?.variableValues) {
|
|
||||||
|
if (
|
||||||
|
variablesRes?.variableValues &&
|
||||||
|
Array.isArray(variablesRes?.variableValues)
|
||||||
|
) {
|
||||||
const newOptionsData = sortValues(
|
const newOptionsData = sortValues(
|
||||||
response.payload?.variableValues,
|
variablesRes?.variableValues,
|
||||||
variableData.sort,
|
variableData.sort,
|
||||||
);
|
);
|
||||||
// Since there is a chance of a variable being dependent on other
|
|
||||||
// variables, we need to check if the optionsData has changed
|
|
||||||
// If it has changed, we need to update the dependent variable
|
|
||||||
// So we compare the new optionsData with the old optionsData
|
|
||||||
const oldOptionsData = sortValues(optionsData, variableData.sort) as never;
|
const oldOptionsData = sortValues(optionsData, variableData.sort) as never;
|
||||||
|
|
||||||
if (!areArraysEqual(newOptionsData, oldOptionsData)) {
|
if (!areArraysEqual(newOptionsData, oldOptionsData)) {
|
||||||
/* eslint-disable no-useless-escape */
|
/* eslint-disable no-useless-escape */
|
||||||
const re = new RegExp(`\\{\\{\\s*?\\.${lastUpdatedVar}\\s*?\\}\\}`); // regex for `{{.var}}`
|
const re = new RegExp(`\\{\\{\\s*?\\.${lastUpdatedVar}\\s*?\\}\\}`); // regex for `{{.var}}`
|
||||||
@ -104,10 +138,10 @@ function VariableItem({
|
|||||||
[value] = newOptionsData;
|
[value] = newOptionsData;
|
||||||
}
|
}
|
||||||
if (variableData.name) {
|
if (variableData.name) {
|
||||||
onValueUpdate(variableData.name, value);
|
onValueUpdate(variableData.name, value, allSelected);
|
||||||
onAllSelectedUpdate(variableData.name, allSelected);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setOptionsData(newOptionsData);
|
setOptionsData(newOptionsData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -122,19 +156,37 @@ function VariableItem({
|
|||||||
) as never,
|
) as never,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [
|
|
||||||
variableData,
|
|
||||||
existingVariables,
|
|
||||||
onValueUpdate,
|
|
||||||
onAllSelectedUpdate,
|
|
||||||
optionsData,
|
|
||||||
lastUpdatedVar,
|
|
||||||
]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
getOptions();
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [variableData, existingVariables]);
|
};
|
||||||
|
|
||||||
|
const { isLoading } = useQuery(getQueryKey(variableData), {
|
||||||
|
enabled: variableData && variableData.type === 'QUERY',
|
||||||
|
queryFn: () =>
|
||||||
|
dashboardVariablesQuery({
|
||||||
|
query: variableData.queryValue || '',
|
||||||
|
variables: variablePropsToPayloadVariables(existingVariables),
|
||||||
|
}),
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
onSuccess: (response) => {
|
||||||
|
getOptions(response.payload);
|
||||||
|
},
|
||||||
|
onError: (error: {
|
||||||
|
details: {
|
||||||
|
error: string;
|
||||||
|
};
|
||||||
|
}) => {
|
||||||
|
const { details } = error;
|
||||||
|
|
||||||
|
if (details.error) {
|
||||||
|
let message = details.error;
|
||||||
|
if (details.error.includes('Syntax error:')) {
|
||||||
|
message =
|
||||||
|
'Please make sure query is valid and dependent variables are selected';
|
||||||
|
}
|
||||||
|
setErrorMessage(message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const handleChange = (value: string | string[]): void => {
|
const handleChange = (value: string | string[]): void => {
|
||||||
if (variableData.name)
|
if (variableData.name)
|
||||||
@ -143,11 +195,9 @@ function VariableItem({
|
|||||||
(Array.isArray(value) && value.includes(ALL_SELECT_VALUE)) ||
|
(Array.isArray(value) && value.includes(ALL_SELECT_VALUE)) ||
|
||||||
(Array.isArray(value) && value.length === 0)
|
(Array.isArray(value) && value.length === 0)
|
||||||
) {
|
) {
|
||||||
onValueUpdate(variableData.name, optionsData);
|
onValueUpdate(variableData.name, optionsData, true);
|
||||||
onAllSelectedUpdate(variableData.name, true);
|
|
||||||
} else {
|
} else {
|
||||||
onValueUpdate(variableData.name, value);
|
onValueUpdate(variableData.name, value, false);
|
||||||
onAllSelectedUpdate(variableData.name, false);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -165,16 +215,36 @@ function VariableItem({
|
|||||||
? 'multiple'
|
? 'multiple'
|
||||||
: undefined;
|
: undefined;
|
||||||
const enableSelectAll = variableData.multiSelect && variableData.showALLOption;
|
const enableSelectAll = variableData.multiSelect && variableData.showALLOption;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (debouncedVariableValue !== variableData?.selectedValue?.toString()) {
|
||||||
|
handleChange(debouncedVariableValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [debouncedVariableValue]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Fetch options for CUSTOM Type
|
||||||
|
if (variableData.type === 'CUSTOM') {
|
||||||
|
getOptions(null);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VariableContainer>
|
<VariableContainer>
|
||||||
<VariableName>${variableData.name}</VariableName>
|
<Typography.Text className="variable-name" ellipsis>
|
||||||
|
${variableData.name}
|
||||||
|
</Typography.Text>
|
||||||
|
<VariableValue>
|
||||||
{variableData.type === 'TEXTBOX' ? (
|
{variableData.type === 'TEXTBOX' ? (
|
||||||
<Input
|
<Input
|
||||||
placeholder="Enter value"
|
placeholder="Enter value"
|
||||||
bordered={false}
|
bordered={false}
|
||||||
value={variableData.selectedValue?.toString()}
|
value={variableValue}
|
||||||
onChange={(e): void => {
|
onChange={(e): void => {
|
||||||
handleChange(e.target.value || '');
|
setVaribleValue(e.target.value || '');
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
width:
|
width:
|
||||||
@ -182,7 +252,8 @@ function VariableItem({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
!errorMessage && (
|
!errorMessage &&
|
||||||
|
optionsData && (
|
||||||
<Select
|
<Select
|
||||||
value={selectValue}
|
value={selectValue}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
@ -215,11 +286,15 @@ function VariableItem({
|
|||||||
)}
|
)}
|
||||||
{errorMessage && (
|
{errorMessage && (
|
||||||
<span style={{ margin: '0 0.5rem' }}>
|
<span style={{ margin: '0 0.5rem' }}>
|
||||||
<Popover placement="top" content={<Typography>{errorMessage}</Typography>}>
|
<Popover
|
||||||
|
placement="top"
|
||||||
|
content={<Typography>{errorMessage}</Typography>}
|
||||||
|
>
|
||||||
<WarningOutlined style={{ color: orange[5] }} />
|
<WarningOutlined style={{ color: orange[5] }} />
|
||||||
</Popover>
|
</Popover>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
</VariableValue>
|
||||||
</VariableContainer>
|
</VariableContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,117 +1,3 @@
|
|||||||
import { Row } from 'antd';
|
import DashboardVariableSelection from './DashboardVariableSelection';
|
||||||
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
|
||||||
import { map, sortBy } from 'lodash-es';
|
|
||||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
|
||||||
import { memo, useState } from 'react';
|
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import { AppState } from 'store/reducers';
|
|
||||||
import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll';
|
|
||||||
import AppReducer from 'types/reducer/app';
|
|
||||||
|
|
||||||
import VariableItem from './VariableItem';
|
export default DashboardVariableSelection;
|
||||||
|
|
||||||
function DashboardVariableSelection(): JSX.Element | null {
|
|
||||||
const { selectedDashboard, setSelectedDashboard } = useDashboard();
|
|
||||||
|
|
||||||
const { data } = selectedDashboard || {};
|
|
||||||
|
|
||||||
const { variables } = data || {};
|
|
||||||
|
|
||||||
const [update, setUpdate] = useState<boolean>(false);
|
|
||||||
const [lastUpdatedVar, setLastUpdatedVar] = useState<string>('');
|
|
||||||
|
|
||||||
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
|
|
||||||
|
|
||||||
const onVarChanged = (name: string): void => {
|
|
||||||
setLastUpdatedVar(name);
|
|
||||||
setUpdate(!update);
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateMutation = useUpdateDashboard();
|
|
||||||
const { notifications } = useNotifications();
|
|
||||||
|
|
||||||
const updateVariables = (
|
|
||||||
updatedVariablesData: Dashboard['data']['variables'],
|
|
||||||
): void => {
|
|
||||||
if (!selectedDashboard) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateMutation.mutateAsync(
|
|
||||||
{
|
|
||||||
...selectedDashboard,
|
|
||||||
data: {
|
|
||||||
...selectedDashboard.data,
|
|
||||||
variables: updatedVariablesData,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
onSuccess: (updatedDashboard) => {
|
|
||||||
if (updatedDashboard.payload) {
|
|
||||||
setSelectedDashboard(updatedDashboard.payload);
|
|
||||||
notifications.success({
|
|
||||||
message: 'Variable updated successfully',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onError: () => {
|
|
||||||
notifications.error({
|
|
||||||
message: 'Error while updating variable',
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onValueUpdate = (
|
|
||||||
name: string,
|
|
||||||
value: IDashboardVariable['selectedValue'],
|
|
||||||
): void => {
|
|
||||||
const updatedVariablesData = { ...variables };
|
|
||||||
updatedVariablesData[name].selectedValue = value;
|
|
||||||
|
|
||||||
if (role !== 'VIEWER' && selectedDashboard) {
|
|
||||||
updateVariables(updatedVariablesData);
|
|
||||||
}
|
|
||||||
|
|
||||||
onVarChanged(name);
|
|
||||||
};
|
|
||||||
const onAllSelectedUpdate = (
|
|
||||||
name: string,
|
|
||||||
value: IDashboardVariable['allSelected'],
|
|
||||||
): void => {
|
|
||||||
const updatedVariablesData = { ...variables };
|
|
||||||
updatedVariablesData[name].allSelected = value;
|
|
||||||
|
|
||||||
if (role !== 'VIEWER') {
|
|
||||||
updateVariables(updatedVariablesData);
|
|
||||||
}
|
|
||||||
onVarChanged(name);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!variables) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Row>
|
|
||||||
{map(sortBy(Object.keys(variables)), (variableName) => (
|
|
||||||
<VariableItem
|
|
||||||
key={`${variableName}${variables[variableName].modificationUUID}`}
|
|
||||||
existingVariables={variables}
|
|
||||||
variableData={{
|
|
||||||
name: variableName,
|
|
||||||
...variables[variableName],
|
|
||||||
change: update,
|
|
||||||
}}
|
|
||||||
onValueUpdate={onValueUpdate}
|
|
||||||
onAllSelectedUpdate={onAllSelectedUpdate}
|
|
||||||
lastUpdatedVar={lastUpdatedVar}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Row>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default memo(DashboardVariableSelection);
|
|
||||||
|
@ -3,19 +3,40 @@ import { Typography } from 'antd';
|
|||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
export const VariableContainer = styled.div`
|
export const VariableContainer = styled.div`
|
||||||
|
max-width: 100%;
|
||||||
border: 1px solid ${grey[1]}66;
|
border: 1px solid ${grey[1]}66;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
padding-left: 0.5rem;
|
padding-left: 0.5rem;
|
||||||
|
margin-right: 8px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 0.3rem;
|
margin-bottom: 0.3rem;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 4px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const VariableName = styled(Typography)`
|
export const VariableName = styled(Typography)`
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
font-style: italic;
|
|
||||||
color: ${grey[0]};
|
color: ${grey[0]};
|
||||||
|
|
||||||
|
min-width: 100px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
flex: 1;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const VariableValue = styled(Typography)`
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: ${grey[0]};
|
||||||
|
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
max-width: 300px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const SelectItemStyle = {
|
export const SelectItemStyle = {
|
||||||
|
@ -6,6 +6,8 @@ import {
|
|||||||
CategoryNames,
|
CategoryNames,
|
||||||
DataFormats,
|
DataFormats,
|
||||||
DataRateFormats,
|
DataRateFormats,
|
||||||
|
HelperCategory,
|
||||||
|
HelperFormat,
|
||||||
MiscellaneousFormats,
|
MiscellaneousFormats,
|
||||||
ThroughputFormats,
|
ThroughputFormats,
|
||||||
TimeFormats,
|
TimeFormats,
|
||||||
@ -119,3 +121,10 @@ export const getCategoryByOptionId = (id: string): Category | undefined =>
|
|||||||
|
|
||||||
export const isCategoryName = (name: string): name is CategoryNames =>
|
export const isCategoryName = (name: string): name is CategoryNames =>
|
||||||
alertsCategory.some((category) => category.name === name);
|
alertsCategory.some((category) => category.name === name);
|
||||||
|
|
||||||
|
const allFormats: HelperFormat[] = alertsCategory.flatMap(
|
||||||
|
(category: HelperCategory) => category.formats,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const getFormatNameByOptionId = (id: string): string | undefined =>
|
||||||
|
allFormats.find((format) => format.id === id)?.name;
|
||||||
|
@ -107,48 +107,8 @@ function RightContainer({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* <TextContainer>
|
|
||||||
<Typography>Stacked Graphs :</Typography>
|
|
||||||
<Switch
|
|
||||||
checked={stacked}
|
|
||||||
onChange={(): void => {
|
|
||||||
setStacked((value) => !value);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</TextContainer> */}
|
|
||||||
|
|
||||||
{/* <Title light={'true'}>Fill Opacity: </Title> */}
|
|
||||||
|
|
||||||
{/* <Slider
|
|
||||||
value={parseInt(opacity, 10)}
|
|
||||||
marks={{
|
|
||||||
0: '0',
|
|
||||||
33: '33',
|
|
||||||
66: '66',
|
|
||||||
100: '100',
|
|
||||||
}}
|
|
||||||
onChange={(number): void => onChangeHandler(setOpacity, number.toString())}
|
|
||||||
step={1}
|
|
||||||
/> */}
|
|
||||||
|
|
||||||
{/* <Title light={'true'}>Null/Zero values: </Title>
|
|
||||||
|
|
||||||
<NullButtonContainer>
|
|
||||||
{nullValueButtons.map((button) => (
|
|
||||||
<Button
|
|
||||||
type={button.check === selectedNullZeroValue ? 'primary' : 'default'}
|
|
||||||
key={button.name}
|
|
||||||
onClick={(): void =>
|
|
||||||
onChangeHandler(setSelectedNullZeroValue, button.check)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{button.name}
|
|
||||||
</Button>
|
|
||||||
))}
|
|
||||||
</NullButtonContainer> */}
|
|
||||||
|
|
||||||
<Space style={{ marginTop: 10 }} direction="vertical">
|
<Space style={{ marginTop: 10 }} direction="vertical">
|
||||||
<Typography>Fill span gaps</Typography>
|
<Typography>Fill gaps</Typography>
|
||||||
|
|
||||||
<Switch
|
<Switch
|
||||||
checked={isFillSpans}
|
checked={isFillSpans}
|
||||||
|
@ -362,3 +362,13 @@ export type Category = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type DataTypeCategories = Category[];
|
export type DataTypeCategories = Category[];
|
||||||
|
|
||||||
|
export interface HelperFormat {
|
||||||
|
name: string;
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HelperCategory {
|
||||||
|
name: string;
|
||||||
|
formats: Format[];
|
||||||
|
}
|
||||||
|
@ -11,6 +11,7 @@ import ROUTES from 'constants/routes';
|
|||||||
import { stepsMap } from 'container/OnboardingContainer/constants/stepsConfig';
|
import { stepsMap } from 'container/OnboardingContainer/constants/stepsConfig';
|
||||||
import { DataSourceType } from 'container/OnboardingContainer/Steps/DataSource/DataSource';
|
import { DataSourceType } from 'container/OnboardingContainer/Steps/DataSource/DataSource';
|
||||||
import { hasFrameworks } from 'container/OnboardingContainer/utils/dataSourceUtils';
|
import { hasFrameworks } from 'container/OnboardingContainer/utils/dataSourceUtils';
|
||||||
|
import useAnalytics from 'hooks/analytics/useAnalytics';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import { isEmpty } from 'lodash-es';
|
import { isEmpty } from 'lodash-es';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
@ -71,6 +72,7 @@ export default function ModuleStepsContainer({
|
|||||||
|
|
||||||
const [current, setCurrent] = useState(0);
|
const [current, setCurrent] = useState(0);
|
||||||
const [metaData, setMetaData] = useState<MetaDataProps[]>(defaultMetaData);
|
const [metaData, setMetaData] = useState<MetaDataProps[]>(defaultMetaData);
|
||||||
|
const { trackEvent } = useAnalytics();
|
||||||
const lastStepIndex = selectedModuleSteps.length - 1;
|
const lastStepIndex = selectedModuleSteps.length - 1;
|
||||||
|
|
||||||
const isValidForm = (): boolean => {
|
const isValidForm = (): boolean => {
|
||||||
@ -126,6 +128,10 @@ export default function ModuleStepsContainer({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const redirectToModules = (): void => {
|
const redirectToModules = (): void => {
|
||||||
|
trackEvent('Onboarding Complete', {
|
||||||
|
module: selectedModule.id,
|
||||||
|
});
|
||||||
|
|
||||||
if (selectedModule.id === ModulesMap.APM) {
|
if (selectedModule.id === ModulesMap.APM) {
|
||||||
history.push(ROUTES.APPLICATION);
|
history.push(ROUTES.APPLICATION);
|
||||||
} else if (selectedModule.id === ModulesMap.LogsManagement) {
|
} else if (selectedModule.id === ModulesMap.LogsManagement) {
|
||||||
|
@ -0,0 +1,25 @@
|
|||||||
|
import { Input, InputProps } from 'antd';
|
||||||
|
import { ChangeEventHandler, useState } from 'react';
|
||||||
|
|
||||||
|
function CSVInput({ value, onChange, ...otherProps }: InputProps): JSX.Element {
|
||||||
|
const [inputValue, setInputValue] = useState(
|
||||||
|
((value as string[]) || []).join(', '),
|
||||||
|
);
|
||||||
|
|
||||||
|
const onChangeHandler = (onChange as unknown) as (v: string[]) => void;
|
||||||
|
|
||||||
|
const onInputChange: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
|
const newValue = e.target.value;
|
||||||
|
setInputValue(newValue);
|
||||||
|
|
||||||
|
if (onChangeHandler) {
|
||||||
|
const splitValues = newValue.split(',').map((v) => v.trim());
|
||||||
|
onChangeHandler(splitValues);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
|
return <Input value={inputValue} onChange={onInputChange} {...otherProps} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CSVInput;
|
@ -0,0 +1,107 @@
|
|||||||
|
import './styles.scss';
|
||||||
|
|
||||||
|
import { Form, Input, Select } from 'antd';
|
||||||
|
import { ModalFooterTitle } from 'container/PipelinePage/styles';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { formValidationRules } from '../config';
|
||||||
|
import { processorFields, ProcessorFormField } from './config';
|
||||||
|
import CSVInput from './FormFields/CSVInput';
|
||||||
|
import { FormWrapper, PipelineIndexIcon, StyledSelect } from './styles';
|
||||||
|
|
||||||
|
function ProcessorFieldInput({
|
||||||
|
fieldData,
|
||||||
|
}: ProcessorFieldInputProps): JSX.Element | null {
|
||||||
|
const { t } = useTranslation('pipeline');
|
||||||
|
|
||||||
|
// Watch form values so we can evaluate shouldRender on
|
||||||
|
// conditional fields when form values are updated.
|
||||||
|
const form = Form.useFormInstance();
|
||||||
|
Form.useWatch(fieldData?.dependencies || [], form);
|
||||||
|
|
||||||
|
if (fieldData.shouldRender && !fieldData.shouldRender(form)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do not render display elements for hidden inputs.
|
||||||
|
if (fieldData?.hidden) {
|
||||||
|
return (
|
||||||
|
<Form.Item
|
||||||
|
name={fieldData.name}
|
||||||
|
initialValue={fieldData.initialValue}
|
||||||
|
dependencies={fieldData.dependencies || []}
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
>
|
||||||
|
<Input type="hidden" />
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let inputField;
|
||||||
|
if (fieldData?.options) {
|
||||||
|
inputField = (
|
||||||
|
<StyledSelect>
|
||||||
|
{fieldData.options.map(({ value, label }) => (
|
||||||
|
<Select.Option key={value + label} value={value}>
|
||||||
|
{label}
|
||||||
|
</Select.Option>
|
||||||
|
))}
|
||||||
|
</StyledSelect>
|
||||||
|
);
|
||||||
|
} else if (Array.isArray(fieldData?.initialValue)) {
|
||||||
|
inputField = <CSVInput placeholder={t(fieldData.placeholder)} />;
|
||||||
|
} else {
|
||||||
|
inputField = <Input placeholder={t(fieldData.placeholder)} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
fieldData?.compact
|
||||||
|
? 'compact-processor-field-container'
|
||||||
|
: 'processor-field-container'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{!fieldData?.compact && (
|
||||||
|
<PipelineIndexIcon size="small">
|
||||||
|
{Number(fieldData.id) + 1}
|
||||||
|
</PipelineIndexIcon>
|
||||||
|
)}
|
||||||
|
<FormWrapper>
|
||||||
|
<Form.Item
|
||||||
|
required={false}
|
||||||
|
label={<ModalFooterTitle>{fieldData.fieldName}</ModalFooterTitle>}
|
||||||
|
name={fieldData.name}
|
||||||
|
initialValue={fieldData.initialValue}
|
||||||
|
rules={fieldData.rules ? fieldData.rules : formValidationRules}
|
||||||
|
dependencies={fieldData.dependencies || []}
|
||||||
|
>
|
||||||
|
{inputField}
|
||||||
|
</Form.Item>
|
||||||
|
</FormWrapper>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProcessorFieldInputProps {
|
||||||
|
fieldData: ProcessorFormField;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProcessorForm({ processorType }: ProcessorFormProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div className="processor-form-container">
|
||||||
|
{processorFields[processorType]?.map((fieldData: ProcessorFormField) => (
|
||||||
|
<ProcessorFieldInput
|
||||||
|
key={fieldData.name + String(fieldData.initialValue)}
|
||||||
|
fieldData={fieldData}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProcessorFormProps {
|
||||||
|
processorType: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProcessorForm;
|
@ -1,5 +1,7 @@
|
|||||||
|
import { FormInstance } from 'antd';
|
||||||
import { Rule, RuleRender } from 'antd/es/form';
|
import { Rule, RuleRender } from 'antd/es/form';
|
||||||
import { NamePath } from 'antd/es/form/interface';
|
import { NamePath } from 'antd/es/form/interface';
|
||||||
|
import { ProcessorData } from 'types/api/pipeline/def';
|
||||||
|
|
||||||
type ProcessorType = {
|
type ProcessorType = {
|
||||||
key: string;
|
key: string;
|
||||||
@ -14,6 +16,8 @@ export const processorTypes: Array<ProcessorType> = [
|
|||||||
{ key: 'regex_parser', value: 'regex_parser', label: 'Regex' },
|
{ key: 'regex_parser', value: 'regex_parser', label: 'Regex' },
|
||||||
{ key: 'json_parser', value: 'json_parser', label: 'Json Parser' },
|
{ key: 'json_parser', value: 'json_parser', label: 'Json Parser' },
|
||||||
{ key: 'trace_parser', value: 'trace_parser', label: 'Trace Parser' },
|
{ key: 'trace_parser', value: 'trace_parser', label: 'Trace Parser' },
|
||||||
|
{ key: 'time_parser', value: 'time_parser', label: 'Timestamp Parser' },
|
||||||
|
{ key: 'severity_parser', value: 'severity_parser', label: 'Severity Parser' },
|
||||||
{ key: 'add', value: 'add', label: 'Add' },
|
{ key: 'add', value: 'add', label: 'Add' },
|
||||||
{ key: 'remove', value: 'remove', label: 'Remove' },
|
{ key: 'remove', value: 'remove', label: 'Remove' },
|
||||||
// { key: 'retain', value: 'retain', label: 'Retain' }, @Chintan - Commented as per Nitya's suggestion
|
// { key: 'retain', value: 'retain', label: 'Retain' }, @Chintan - Commented as per Nitya's suggestion
|
||||||
@ -23,14 +27,31 @@ export const processorTypes: Array<ProcessorType> = [
|
|||||||
|
|
||||||
export const DEFAULT_PROCESSOR_TYPE = processorTypes[0].value;
|
export const DEFAULT_PROCESSOR_TYPE = processorTypes[0].value;
|
||||||
|
|
||||||
|
export type ProcessorFieldOption = {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO(Raj): Refactor Processor Form code after putting e2e UI tests in place.
|
||||||
export type ProcessorFormField = {
|
export type ProcessorFormField = {
|
||||||
id: number;
|
id: number;
|
||||||
fieldName: string;
|
fieldName: string;
|
||||||
placeholder: string;
|
placeholder: string;
|
||||||
name: string | NamePath;
|
name: string | NamePath;
|
||||||
rules?: Array<Rule>;
|
rules?: Array<Rule>;
|
||||||
initialValue?: string;
|
hidden?: boolean;
|
||||||
|
initialValue?: boolean | string | Array<string>;
|
||||||
dependencies?: Array<string | NamePath>;
|
dependencies?: Array<string | NamePath>;
|
||||||
|
options?: Array<ProcessorFieldOption>;
|
||||||
|
shouldRender?: (form: FormInstance) => boolean;
|
||||||
|
onFormValuesChanged?: (
|
||||||
|
changedValues: ProcessorData,
|
||||||
|
form: FormInstance,
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
// Should this field have its own row or should it
|
||||||
|
// be packed with other compact fields.
|
||||||
|
compact?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const traceParserFieldValidator: RuleRender = (form) => ({
|
const traceParserFieldValidator: RuleRender = (form) => ({
|
||||||
@ -206,6 +227,182 @@ export const processorFields: { [key: string]: Array<ProcessorFormField> } = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
time_parser: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
fieldName: 'Name of Timestamp Parsing Processor',
|
||||||
|
placeholder: 'processor_name_placeholder',
|
||||||
|
name: 'name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
fieldName: 'Parse Timestamp Value From',
|
||||||
|
placeholder: 'processor_parsefrom_placeholder',
|
||||||
|
name: 'parse_from',
|
||||||
|
initialValue: 'attributes.timestamp',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
fieldName: 'Timestamp Format Type',
|
||||||
|
placeholder: '',
|
||||||
|
name: 'layout_type',
|
||||||
|
initialValue: 'strptime',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: 'Unix Epoch',
|
||||||
|
value: 'epoch',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'strptime Format',
|
||||||
|
value: 'strptime',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onFormValuesChanged: (
|
||||||
|
changedValues: ProcessorData,
|
||||||
|
form: FormInstance,
|
||||||
|
): void => {
|
||||||
|
if (changedValues?.layout_type) {
|
||||||
|
const newLayoutValue =
|
||||||
|
changedValues.layout_type === 'strptime' ? '%Y-%m-%dT%H:%M:%S.%f%z' : 's';
|
||||||
|
|
||||||
|
form.setFieldValue('layout', newLayoutValue);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
fieldName: 'Epoch Format',
|
||||||
|
placeholder: '',
|
||||||
|
name: 'layout',
|
||||||
|
dependencies: ['layout_type'],
|
||||||
|
shouldRender: (form: FormInstance): boolean => {
|
||||||
|
const layoutType = form.getFieldValue('layout_type');
|
||||||
|
return layoutType === 'epoch';
|
||||||
|
},
|
||||||
|
initialValue: 's',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: 'seconds',
|
||||||
|
value: 's',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'milliseconds',
|
||||||
|
value: 'ms',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'microseconds',
|
||||||
|
value: 'us',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'nanoseconds',
|
||||||
|
value: 'ns',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'seconds.milliseconds (eg: 1136214245.123)',
|
||||||
|
value: 's.ms',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'seconds.microseconds (eg: 1136214245.123456)',
|
||||||
|
value: 's.us',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'seconds.nanoseconds (eg: 1136214245.123456789)',
|
||||||
|
value: 's.ns',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
fieldName: 'Timestamp Format',
|
||||||
|
placeholder: 'strptime directives based format. Eg: %Y-%m-%dT%H:%M:%S.%f%z',
|
||||||
|
name: 'layout',
|
||||||
|
dependencies: ['layout_type'],
|
||||||
|
shouldRender: (form: FormInstance): boolean => {
|
||||||
|
const layoutType = form.getFieldValue('layout_type');
|
||||||
|
return layoutType === 'strptime';
|
||||||
|
},
|
||||||
|
initialValue: '%Y-%m-%dT%H:%M:%S.%f%z',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
severity_parser: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
fieldName: 'Name of Severity Parsing Processor',
|
||||||
|
placeholder: 'processor_name_placeholder',
|
||||||
|
name: 'name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
fieldName: 'Parse Severity Value From',
|
||||||
|
placeholder: 'processor_parsefrom_placeholder',
|
||||||
|
name: 'parse_from',
|
||||||
|
initialValue: 'attributes.logLevel',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
fieldName: 'Values for level TRACE',
|
||||||
|
placeholder: 'Specify comma separated values. Eg: trace, 0',
|
||||||
|
name: ['mapping', 'trace'],
|
||||||
|
rules: [],
|
||||||
|
initialValue: ['trace'],
|
||||||
|
compact: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
fieldName: 'Values for level DEBUG',
|
||||||
|
placeholder: 'Specify comma separated values. Eg: debug, 2xx',
|
||||||
|
name: ['mapping', 'debug'],
|
||||||
|
rules: [],
|
||||||
|
initialValue: ['debug'],
|
||||||
|
compact: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
fieldName: 'Values for level INFO',
|
||||||
|
placeholder: 'Specify comma separated values. Eg: info, 3xx',
|
||||||
|
name: ['mapping', 'info'],
|
||||||
|
rules: [],
|
||||||
|
initialValue: ['info'],
|
||||||
|
compact: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
fieldName: 'Values for level WARN',
|
||||||
|
placeholder: 'Specify comma separated values. Eg: warning, 4xx',
|
||||||
|
name: ['mapping', 'warn'],
|
||||||
|
rules: [],
|
||||||
|
initialValue: ['warn'],
|
||||||
|
compact: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 7,
|
||||||
|
fieldName: 'Values for level ERROR',
|
||||||
|
placeholder: 'Specify comma separated values. Eg: error, 5xx',
|
||||||
|
name: ['mapping', 'error'],
|
||||||
|
rules: [],
|
||||||
|
initialValue: ['error'],
|
||||||
|
compact: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 8,
|
||||||
|
fieldName: 'Values for level FATAL',
|
||||||
|
placeholder: 'Specify comma separated values. Eg: fatal, panic',
|
||||||
|
name: ['mapping', 'fatal'],
|
||||||
|
rules: [],
|
||||||
|
initialValue: ['fatal'],
|
||||||
|
compact: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 9,
|
||||||
|
fieldName: 'Override Severity Text',
|
||||||
|
placeholder:
|
||||||
|
'Should the parsed severity set both severity and severityText?',
|
||||||
|
name: ['overwrite_text'],
|
||||||
|
rules: [],
|
||||||
|
initialValue: true,
|
||||||
|
hidden: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
retain: [
|
retain: [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
|
@ -11,9 +11,9 @@ import { v4 } from 'uuid';
|
|||||||
|
|
||||||
import { ModalButtonWrapper, ModalTitle } from '../styles';
|
import { ModalButtonWrapper, ModalTitle } from '../styles';
|
||||||
import { getEditedDataSource, getRecordIndex } from '../utils';
|
import { getEditedDataSource, getRecordIndex } from '../utils';
|
||||||
import { DEFAULT_PROCESSOR_TYPE } from './config';
|
import { DEFAULT_PROCESSOR_TYPE, processorFields } from './config';
|
||||||
import TypeSelect from './FormFields/TypeSelect';
|
import TypeSelect from './FormFields/TypeSelect';
|
||||||
import { renderProcessorForm } from './utils';
|
import ProcessorForm from './ProcessorForm';
|
||||||
|
|
||||||
function AddNewProcessor({
|
function AddNewProcessor({
|
||||||
isActionType,
|
isActionType,
|
||||||
@ -141,6 +141,17 @@ function AddNewProcessor({
|
|||||||
|
|
||||||
const isOpen = useMemo(() => isEdit || isAdd, [isAdd, isEdit]);
|
const isOpen = useMemo(() => isEdit || isAdd, [isAdd, isEdit]);
|
||||||
|
|
||||||
|
const onFormValuesChanged = useCallback(
|
||||||
|
(changedValues: ProcessorData): void => {
|
||||||
|
processorFields[processorType].forEach((field) => {
|
||||||
|
if (field.onFormValuesChanged) {
|
||||||
|
field.onFormValuesChanged(changedValues, form);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[form, processorType],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title={<ModalTitle level={4}>{modalTitle}</ModalTitle>}
|
title={<ModalTitle level={4}>{modalTitle}</ModalTitle>}
|
||||||
@ -157,9 +168,10 @@ function AddNewProcessor({
|
|||||||
onFinish={onFinish}
|
onFinish={onFinish}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
form={form}
|
form={form}
|
||||||
|
onValuesChange={onFormValuesChanged}
|
||||||
>
|
>
|
||||||
<TypeSelect value={processorType} onChange={handleProcessorType} />
|
<TypeSelect value={processorType} onChange={handleProcessorType} />
|
||||||
{renderProcessorForm(processorType)}
|
<ProcessorForm processorType={processorType} />
|
||||||
<Divider plain />
|
<Divider plain />
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<ModalButtonWrapper>
|
<ModalButtonWrapper>
|
||||||
|
@ -0,0 +1,27 @@
|
|||||||
|
|
||||||
|
.processor-form-container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap
|
||||||
|
}
|
||||||
|
|
||||||
|
.processor-field-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 0rem;
|
||||||
|
gap: 1rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compact-processor-field-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 0rem;
|
||||||
|
min-width: 40%;
|
||||||
|
flex-grow: 1;
|
||||||
|
margin-left: 2.5rem;
|
||||||
|
}
|
@ -1,9 +0,0 @@
|
|||||||
import { processorFields, ProcessorFormField } from './config';
|
|
||||||
import NameInput from './FormFields/NameInput';
|
|
||||||
|
|
||||||
export const renderProcessorForm = (
|
|
||||||
processorType: string,
|
|
||||||
): Array<JSX.Element> =>
|
|
||||||
processorFields[processorType]?.map((fieldData: ProcessorFormField) => (
|
|
||||||
<NameInput key={fieldData.id} fieldData={fieldData} />
|
|
||||||
));
|
|
@ -27,7 +27,8 @@ const usePipelinePreview = ({
|
|||||||
// ILog allows both number and string while the API needs a number
|
// ILog allows both number and string while the API needs a number
|
||||||
const simulationInput = inputLogs.map((l) => ({
|
const simulationInput = inputLogs.map((l) => ({
|
||||||
...l,
|
...l,
|
||||||
timestamp: new Date(l.timestamp).getTime(),
|
// log timestamps in query service API are unix nanos
|
||||||
|
timestamp: new Date(l.timestamp).getTime() * 10 ** 6,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const response = useQuery<PipelineSimulationResponse, AxiosError>({
|
const response = useQuery<PipelineSimulationResponse, AxiosError>({
|
||||||
@ -42,9 +43,15 @@ const usePipelinePreview = ({
|
|||||||
|
|
||||||
const { isFetching, isError, data, error } = response;
|
const { isFetching, isError, data, error } = response;
|
||||||
|
|
||||||
|
const outputLogs = (data?.logs || []).map((l: ILog) => ({
|
||||||
|
...l,
|
||||||
|
// log timestamps in query service API are unix nanos
|
||||||
|
timestamp: (l.timestamp as number) / 10 ** 6,
|
||||||
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isLoading: isFetching,
|
isLoading: isFetching,
|
||||||
outputLogs: data?.logs || [],
|
outputLogs,
|
||||||
isError,
|
isError,
|
||||||
errorMsg: error?.response?.data?.error || '',
|
errorMsg: error?.response?.data?.error || '',
|
||||||
};
|
};
|
||||||
|
@ -68,11 +68,7 @@ export const getOptions = (routes: string): Option[] => {
|
|||||||
return Options;
|
return Options;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const routesToHideBreadCrumbs = [
|
export const routesToHideBreadCrumbs = [ROUTES.SUPPORT, ROUTES.ALL_DASHBOARD];
|
||||||
ROUTES.SUPPORT,
|
|
||||||
ROUTES.ALL_DASHBOARD,
|
|
||||||
ROUTES.DASHBOARD,
|
|
||||||
];
|
|
||||||
|
|
||||||
export const routesToSkip = [
|
export const routesToSkip = [
|
||||||
ROUTES.SETTINGS,
|
ROUTES.SETTINGS,
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import { ResizeTable } from 'components/ResizeTable';
|
import { ResizeTable } from 'components/ResizeTable';
|
||||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||||
|
import { QueryParams } from 'constants/query';
|
||||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||||
import { useOptionsMenu } from 'container/OptionsMenu';
|
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import { Pagination, URL_PAGINATION } from 'hooks/queryPagination';
|
import { Pagination } from 'hooks/queryPagination';
|
||||||
import useDragColumns from 'hooks/useDragColumns';
|
import useDragColumns from 'hooks/useDragColumns';
|
||||||
import { getDraggedColumns } from 'hooks/useDragColumns/utils';
|
import { getDraggedColumns } from 'hooks/useDragColumns/utils';
|
||||||
import useUrlQueryData from 'hooks/useUrlQueryData';
|
import useUrlQueryData from 'hooks/useUrlQueryData';
|
||||||
@ -44,7 +45,7 @@ function ListView(): JSX.Element {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const { queryData: paginationQueryData } = useUrlQueryData<Pagination>(
|
const { queryData: paginationQueryData } = useUrlQueryData<Pagination>(
|
||||||
URL_PAGINATION,
|
QueryParams.pagination,
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data, isFetching, isError } = useGetQueryRange(
|
const { data, isFetching, isError } = useGetQueryRange(
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import { Typography } from 'antd';
|
import { Typography } from 'antd';
|
||||||
import { ResizeTable } from 'components/ResizeTable';
|
import { ResizeTable } from 'components/ResizeTable';
|
||||||
|
import { QueryParams } from 'constants/query';
|
||||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import { Pagination, URL_PAGINATION } from 'hooks/queryPagination';
|
import { Pagination } from 'hooks/queryPagination';
|
||||||
import useUrlQueryData from 'hooks/useUrlQueryData';
|
import useUrlQueryData from 'hooks/useUrlQueryData';
|
||||||
import { memo, useMemo } from 'react';
|
import { memo, useMemo } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
@ -24,7 +25,7 @@ function TracesView(): JSX.Element {
|
|||||||
>((state) => state.globalTime);
|
>((state) => state.globalTime);
|
||||||
|
|
||||||
const { queryData: paginationQueryData } = useUrlQueryData<Pagination>(
|
const { queryData: paginationQueryData } = useUrlQueryData<Pagination>(
|
||||||
URL_PAGINATION,
|
QueryParams.pagination,
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data, isLoading } = useGetQueryRange(
|
const { data, isLoading } = useGetQueryRange(
|
||||||
|
@ -1,3 +1 @@
|
|||||||
export const URL_PAGINATION = 'pagination';
|
|
||||||
|
|
||||||
export const DEFAULT_PER_PAGE_OPTIONS: number[] = [25, 50, 100, 200];
|
export const DEFAULT_PER_PAGE_OPTIONS: number[] = [25, 50, 100, 200];
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
|
import { QueryParams } from 'constants/query';
|
||||||
import { ControlsProps } from 'container/Controls';
|
import { ControlsProps } from 'container/Controls';
|
||||||
import useUrlQueryData from 'hooks/useUrlQueryData';
|
import useUrlQueryData from 'hooks/useUrlQueryData';
|
||||||
import { useCallback, useEffect, useMemo } from 'react';
|
import { useCallback, useEffect, useMemo } from 'react';
|
||||||
|
|
||||||
import { DEFAULT_PER_PAGE_OPTIONS, URL_PAGINATION } from './config';
|
import { DEFAULT_PER_PAGE_OPTIONS } from './config';
|
||||||
import { Pagination } from './types';
|
import { Pagination } from './types';
|
||||||
import {
|
import {
|
||||||
checkIsValidPaginationData,
|
checkIsValidPaginationData,
|
||||||
@ -22,7 +23,7 @@ const useQueryPagination = (
|
|||||||
query: paginationQuery,
|
query: paginationQuery,
|
||||||
queryData: paginationQueryData,
|
queryData: paginationQueryData,
|
||||||
redirectWithQuery: redirectWithCurrentPagination,
|
redirectWithQuery: redirectWithCurrentPagination,
|
||||||
} = useUrlQueryData<Pagination>(URL_PAGINATION);
|
} = useUrlQueryData<Pagination>(QueryParams.pagination);
|
||||||
|
|
||||||
const handleCountItemsPerPageChange = useCallback(
|
const handleCountItemsPerPageChange = useCallback(
|
||||||
(newLimit: Pagination['limit']) => {
|
(newLimit: Pagination['limit']) => {
|
||||||
|
@ -115,7 +115,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
//Set your SEGMENT_ID
|
//Set your CLARITY_PROJECT_ID
|
||||||
const CLARITY_PROJECT_ID =
|
const CLARITY_PROJECT_ID =
|
||||||
'<%= htmlWebpackPlugin.options.CLARITY_PROJECT_ID %>';
|
'<%= htmlWebpackPlugin.options.CLARITY_PROJECT_ID %>';
|
||||||
|
|
||||||
|
@ -265,10 +265,15 @@ function findUnitObject(
|
|||||||
|
|
||||||
export function convertValue(
|
export function convertValue(
|
||||||
value: number,
|
value: number,
|
||||||
currentUnit: string,
|
currentUnit?: string,
|
||||||
targetUnit: string,
|
targetUnit?: string,
|
||||||
): number | null {
|
): number | null {
|
||||||
if (targetUnit === 'none') {
|
if (
|
||||||
|
targetUnit === 'none' ||
|
||||||
|
!currentUnit ||
|
||||||
|
!targetUnit ||
|
||||||
|
currentUnit === targetUnit
|
||||||
|
) {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
const currentUnitObj = findUnitObject(currentUnit);
|
const currentUnitObj = findUnitObject(currentUnit);
|
||||||
|
@ -15,6 +15,7 @@ import onClickPlugin, { OnClickPluginOpts } from './plugins/onClickPlugin';
|
|||||||
import tooltipPlugin from './plugins/tooltipPlugin';
|
import tooltipPlugin from './plugins/tooltipPlugin';
|
||||||
import getAxes from './utils/getAxes';
|
import getAxes from './utils/getAxes';
|
||||||
import getSeries from './utils/getSeriesData';
|
import getSeries from './utils/getSeriesData';
|
||||||
|
import { getYAxisScale } from './utils/getYAxisScale';
|
||||||
|
|
||||||
interface GetUPlotChartOptions {
|
interface GetUPlotChartOptions {
|
||||||
id?: string;
|
id?: string;
|
||||||
@ -79,7 +80,11 @@ export const getUPlotChartOptions = ({
|
|||||||
auto: true, // Automatically adjust scale range
|
auto: true, // Automatically adjust scale range
|
||||||
},
|
},
|
||||||
y: {
|
y: {
|
||||||
auto: true,
|
...getYAxisScale(
|
||||||
|
thresholds,
|
||||||
|
apiResponse?.data.newResult.data.result,
|
||||||
|
yAxisUnit,
|
||||||
|
),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
|
@ -29,6 +29,8 @@ const generateTooltipContent = (
|
|||||||
): HTMLElement => {
|
): HTMLElement => {
|
||||||
const container = document.createElement('div');
|
const container = document.createElement('div');
|
||||||
container.classList.add('tooltip-container');
|
container.classList.add('tooltip-container');
|
||||||
|
const overlay = document.getElementById('overlay');
|
||||||
|
let tooltipCount = 0;
|
||||||
|
|
||||||
let tooltipTitle = '';
|
let tooltipTitle = '';
|
||||||
const formattedData: Record<string, UplotTooltipDataProps> = {};
|
const formattedData: Record<string, UplotTooltipDataProps> = {};
|
||||||
@ -49,9 +51,10 @@ const generateTooltipContent = (
|
|||||||
const { metric = {}, queryName = '', legend = '' } =
|
const { metric = {}, queryName = '', legend = '' } =
|
||||||
seriesList[index - 1] || {};
|
seriesList[index - 1] || {};
|
||||||
|
|
||||||
|
const value = data[index][idx];
|
||||||
const label = getLabelName(metric, queryName || '', legend || '');
|
const label = getLabelName(metric, queryName || '', legend || '');
|
||||||
|
|
||||||
const value = data[index][idx] || 0;
|
if (value) {
|
||||||
const tooltipValue = getToolTipValue(value, yAxisUnit);
|
const tooltipValue = getToolTipValue(value, yAxisUnit);
|
||||||
|
|
||||||
const dataObj = {
|
const dataObj = {
|
||||||
@ -66,11 +69,22 @@ const generateTooltipContent = (
|
|||||||
textContent: `${label} : ${tooltipValue}`,
|
textContent: `${label} : ${tooltipValue}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
tooltipCount += 1;
|
||||||
formattedData[label] = dataObj;
|
formattedData[label] = dataObj;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show tooltip only if atleast only series has a value at the hovered timestamp
|
||||||
|
if (tooltipCount <= 0) {
|
||||||
|
if (overlay && overlay.style.display === 'block') {
|
||||||
|
overlay.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
const sortedData: Record<
|
const sortedData: Record<
|
||||||
string,
|
string,
|
||||||
UplotTooltipDataProps
|
UplotTooltipDataProps
|
||||||
@ -116,8 +130,6 @@ const generateTooltipContent = (
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const overlay = document.getElementById('overlay');
|
|
||||||
|
|
||||||
if (overlay && overlay.style.display === 'none') {
|
if (overlay && overlay.style.display === 'none') {
|
||||||
overlay.style.display = 'block';
|
overlay.style.display = 'block';
|
||||||
}
|
}
|
||||||
|
@ -1,44 +1,65 @@
|
|||||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||||
|
import { QueryData } from 'types/api/widgets/getQuery';
|
||||||
|
|
||||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
function getXAxisTimestamps(seriesList: QueryData[]): number[] {
|
||||||
function fillMissingTimestamps(
|
const timestamps = new Set();
|
||||||
sortedTimestamps: number[],
|
|
||||||
subsetArray: any[],
|
|
||||||
fillSpans: boolean | undefined,
|
|
||||||
): any[] {
|
|
||||||
const filledArray = [];
|
|
||||||
|
|
||||||
let subsetIndex = 0;
|
seriesList.forEach((series: { values: [number, string][] }) => {
|
||||||
// eslint-disable-next-line no-restricted-syntax
|
series.values.forEach((value) => {
|
||||||
for (const timestamp of sortedTimestamps) {
|
timestamps.add(value[0]);
|
||||||
if (
|
});
|
||||||
subsetIndex < subsetArray.length &&
|
});
|
||||||
timestamp === subsetArray[subsetIndex][0]
|
|
||||||
) {
|
|
||||||
// Timestamp is present in subsetArray
|
|
||||||
const seriesPointData = subsetArray[subsetIndex];
|
|
||||||
|
|
||||||
if (
|
const timestampsArr: number[] | unknown[] = Array.from(timestamps) || [];
|
||||||
seriesPointData &&
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
Array.isArray(seriesPointData) &&
|
// @ts-ignore
|
||||||
seriesPointData.length > 0 &&
|
return timestampsArr.sort((a, b) => a - b);
|
||||||
seriesPointData[1] !== 'NaN'
|
}
|
||||||
) {
|
|
||||||
filledArray.push(subsetArray[subsetIndex]);
|
function fillMissingXAxisTimestamps(
|
||||||
} else {
|
timestampArr: number[],
|
||||||
|
data: any[],
|
||||||
|
fillSpans: boolean,
|
||||||
|
): any {
|
||||||
|
// Generate a set of all timestamps in the range
|
||||||
|
const allTimestampsSet = new Set(timestampArr);
|
||||||
|
const processedData = JSON.parse(JSON.stringify(data));
|
||||||
|
|
||||||
|
// Fill missing timestamps with null values
|
||||||
|
processedData.forEach((entry: { values: (number | null)[][] }) => {
|
||||||
|
const existingTimestamps = new Set(entry.values.map((value) => value[0]));
|
||||||
|
|
||||||
|
const missingTimestamps = Array.from(allTimestampsSet).filter(
|
||||||
|
(timestamp) => !existingTimestamps.has(timestamp),
|
||||||
|
);
|
||||||
|
|
||||||
|
missingTimestamps.forEach((timestamp) => {
|
||||||
const value = fillSpans ? 0 : null;
|
const value = fillSpans ? 0 : null;
|
||||||
filledArray.push([seriesPointData[0], value]);
|
|
||||||
}
|
|
||||||
|
|
||||||
subsetIndex += 1;
|
entry.values.push([timestamp, value]);
|
||||||
} else {
|
});
|
||||||
// Timestamp is missing in subsetArray, fill with [timestamp, 0]
|
|
||||||
const value = fillSpans ? 0 : null;
|
|
||||||
filledArray.push([timestamp, value]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return filledArray;
|
entry.values.forEach((v) => {
|
||||||
|
if (Number.isNaN(v[1])) {
|
||||||
|
const replaceValue = fillSpans ? 0 : null;
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
v[1] = replaceValue;
|
||||||
|
} else if (v[1] !== null) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
v[1] = parseFloat(v[1]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
entry.values.sort((a, b) => a[0] - b[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
return processedData.map((entry: { values: [number, string][] }) =>
|
||||||
|
entry.values.map((value) => value[1]),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getUPlotChartData = (
|
export const getUPlotChartData = (
|
||||||
@ -46,43 +67,12 @@ export const getUPlotChartData = (
|
|||||||
fillSpans?: boolean,
|
fillSpans?: boolean,
|
||||||
): any[] => {
|
): any[] => {
|
||||||
const seriesList = apiResponse?.data?.result || [];
|
const seriesList = apiResponse?.data?.result || [];
|
||||||
const uPlotData = [];
|
const timestampArr = getXAxisTimestamps(seriesList);
|
||||||
|
const yAxisValuesArr = fillMissingXAxisTimestamps(
|
||||||
// this helps us identify the series with the max number of values and helps define the x axis - timestamps
|
|
||||||
const xSeries = seriesList.reduce(
|
|
||||||
(maxObj, currentObj) =>
|
|
||||||
currentObj.values.length > maxObj.values.length ? currentObj : maxObj,
|
|
||||||
seriesList[0],
|
|
||||||
);
|
|
||||||
|
|
||||||
// sort seriesList
|
|
||||||
for (let index = 0; index < seriesList.length; index += 1) {
|
|
||||||
seriesList[index]?.values?.sort((a, b) => a[0] - b[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const timestampArr = xSeries?.values?.map((v) => v[0]);
|
|
||||||
|
|
||||||
// timestamp
|
|
||||||
uPlotData.push(timestampArr);
|
|
||||||
|
|
||||||
// for each series, push the values
|
|
||||||
seriesList.forEach((series) => {
|
|
||||||
const updatedSeries = fillMissingTimestamps(
|
|
||||||
timestampArr,
|
timestampArr,
|
||||||
series?.values || [],
|
seriesList,
|
||||||
fillSpans,
|
fillSpans || false,
|
||||||
);
|
);
|
||||||
|
|
||||||
const seriesData =
|
return [timestampArr, ...yAxisValuesArr];
|
||||||
updatedSeries?.map((v) => {
|
|
||||||
if (v[1] === null) {
|
|
||||||
return v[1];
|
|
||||||
}
|
|
||||||
return parseFloat(v[1]);
|
|
||||||
}) || [];
|
|
||||||
|
|
||||||
uPlotData.push(seriesData);
|
|
||||||
});
|
|
||||||
|
|
||||||
return uPlotData;
|
|
||||||
};
|
};
|
||||||
|
83
frontend/src/lib/uPlotLib/utils/getYAxisScale.ts
Normal file
83
frontend/src/lib/uPlotLib/utils/getYAxisScale.ts
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
|
||||||
|
import { convertValue } from 'lib/getConvertedValue';
|
||||||
|
import { QueryDataV3 } from 'types/api/widgets/getQuery';
|
||||||
|
|
||||||
|
function findMinMaxValues(data: QueryDataV3[]): [number, number] {
|
||||||
|
let min = 0;
|
||||||
|
let max = 0;
|
||||||
|
data?.forEach((entry) => {
|
||||||
|
entry.series?.forEach((series) => {
|
||||||
|
series.values.forEach((valueObj) => {
|
||||||
|
const value = parseFloat(valueObj.value);
|
||||||
|
if (!value) return;
|
||||||
|
min = Math.min(min, value);
|
||||||
|
max = Math.max(max, value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return [min, max];
|
||||||
|
}
|
||||||
|
|
||||||
|
function findMinMaxThresholdValues(
|
||||||
|
thresholds: ThresholdProps[],
|
||||||
|
yAxisUnit?: string,
|
||||||
|
): [number, number] {
|
||||||
|
let minThresholdValue = 0;
|
||||||
|
let maxThresholdValue = 0;
|
||||||
|
|
||||||
|
thresholds.forEach((entry) => {
|
||||||
|
const { thresholdValue, thresholdUnit } = entry;
|
||||||
|
if (thresholdValue === undefined) return;
|
||||||
|
minThresholdValue = Math.min(
|
||||||
|
minThresholdValue,
|
||||||
|
convertValue(thresholdValue, thresholdUnit, yAxisUnit) || 0,
|
||||||
|
);
|
||||||
|
maxThresholdValue = Math.max(
|
||||||
|
maxThresholdValue,
|
||||||
|
convertValue(thresholdValue, thresholdUnit, yAxisUnit) || 0,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return [minThresholdValue, maxThresholdValue];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRange(
|
||||||
|
thresholds: ThresholdProps[],
|
||||||
|
series: QueryDataV3[],
|
||||||
|
yAxisUnit?: string,
|
||||||
|
): [number, number] {
|
||||||
|
const [minThresholdValue, maxThresholdValue] = findMinMaxThresholdValues(
|
||||||
|
thresholds,
|
||||||
|
yAxisUnit,
|
||||||
|
);
|
||||||
|
const [minSeriesValue, maxSeriesValue] = findMinMaxValues(series);
|
||||||
|
|
||||||
|
const min = Math.min(minThresholdValue, minSeriesValue);
|
||||||
|
const max = Math.max(maxThresholdValue, maxSeriesValue);
|
||||||
|
|
||||||
|
return [min, max];
|
||||||
|
}
|
||||||
|
|
||||||
|
function areAllSeriesEmpty(series: QueryDataV3[]): boolean {
|
||||||
|
return series.every((entry) => {
|
||||||
|
if (!entry.series) return true;
|
||||||
|
return entry.series.every((series) => series.values.length === 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getYAxisScale = (
|
||||||
|
thresholds?: ThresholdProps[],
|
||||||
|
series?: QueryDataV3[],
|
||||||
|
yAxisUnit?: string,
|
||||||
|
): {
|
||||||
|
auto: boolean;
|
||||||
|
range?: [number, number];
|
||||||
|
} => {
|
||||||
|
if (!thresholds || !series) return { auto: true };
|
||||||
|
|
||||||
|
if (areAllSeriesEmpty(series)) return { auto: true };
|
||||||
|
|
||||||
|
const [min, max] = getRange(thresholds, series, yAxisUnit);
|
||||||
|
return { auto: false, range: [min, max] };
|
||||||
|
};
|
@ -3,7 +3,7 @@ import ReleaseNote from 'components/ReleaseNote';
|
|||||||
import ListOfAllDashboard from 'container/ListOfDashboard';
|
import ListOfAllDashboard from 'container/ListOfDashboard';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
function Dashboard(): JSX.Element {
|
function DashboardsListPage(): JSX.Element {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -14,4 +14,4 @@ function Dashboard(): JSX.Element {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Dashboard;
|
export default DashboardsListPage;
|
3
frontend/src/pages/DashboardsListPage/index.tsx
Normal file
3
frontend/src/pages/DashboardsListPage/index.tsx
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import DashboardsListPage from './DashboardsListPage';
|
||||||
|
|
||||||
|
export default DashboardsListPage;
|
33
frontend/src/pages/NewDashboard/DashboardPage.tsx
Normal file
33
frontend/src/pages/NewDashboard/DashboardPage.tsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { Typography } from 'antd';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import NotFound from 'components/NotFound';
|
||||||
|
import Spinner from 'components/Spinner';
|
||||||
|
import NewDashboard from 'container/NewDashboard';
|
||||||
|
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||||
|
import { ErrorType } from 'types/common';
|
||||||
|
|
||||||
|
function DashboardPage(): JSX.Element {
|
||||||
|
const { dashboardResponse } = useDashboard();
|
||||||
|
|
||||||
|
const { isFetching, isError, isLoading } = dashboardResponse;
|
||||||
|
|
||||||
|
const errorMessage = isError
|
||||||
|
? (dashboardResponse?.error as AxiosError)?.response?.data.errorType
|
||||||
|
: 'Something went wrong';
|
||||||
|
|
||||||
|
if (isError && !isFetching && errorMessage === ErrorType.NotFound) {
|
||||||
|
return <NotFound />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isError && errorMessage) {
|
||||||
|
return <Typography>{errorMessage}</Typography>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <Spinner tip="Loading.." />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <NewDashboard />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DashboardPage;
|
@ -1,33 +1,3 @@
|
|||||||
import { Typography } from 'antd';
|
import DashboardPage from './DashboardPage';
|
||||||
import { AxiosError } from 'axios';
|
|
||||||
import NotFound from 'components/NotFound';
|
|
||||||
import Spinner from 'components/Spinner';
|
|
||||||
import NewDashboard from 'container/NewDashboard';
|
|
||||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
|
||||||
import { ErrorType } from 'types/common';
|
|
||||||
|
|
||||||
function NewDashboardPage(): JSX.Element {
|
export default DashboardPage;
|
||||||
const { dashboardResponse } = useDashboard();
|
|
||||||
|
|
||||||
const { isFetching, isError, isLoading } = dashboardResponse;
|
|
||||||
|
|
||||||
const errorMessage = isError
|
|
||||||
? (dashboardResponse?.error as AxiosError)?.response?.data.errorType
|
|
||||||
: 'Something went wrong';
|
|
||||||
|
|
||||||
if (isError && !isFetching && errorMessage === ErrorType.NotFound) {
|
|
||||||
return <NotFound />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isError && errorMessage) {
|
|
||||||
return <Typography>{errorMessage}</Typography>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return <Spinner tip="Loading.." />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <NewDashboard />;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default NewDashboardPage;
|
|
||||||
|
@ -5,7 +5,9 @@ import Spinner from 'components/Spinner';
|
|||||||
import ChangeHistory from 'container/PipelinePage/Layouts/ChangeHistory';
|
import ChangeHistory from 'container/PipelinePage/Layouts/ChangeHistory';
|
||||||
import PipelinePage from 'container/PipelinePage/Layouts/Pipeline';
|
import PipelinePage from 'container/PipelinePage/Layouts/Pipeline';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
|
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||||
import { useEffect, useMemo } from 'react';
|
import { useEffect, useMemo } from 'react';
|
||||||
|
import { ErrorBoundary } from 'react-error-boundary';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useQuery } from 'react-query';
|
import { useQuery } from 'react-query';
|
||||||
import { SuccessResponse } from 'types/api';
|
import { SuccessResponse } from 'types/api';
|
||||||
@ -77,7 +79,11 @@ function Pipelines(): JSX.Element {
|
|||||||
return <Spinner height="75vh" tip="Loading Pipelines..." />;
|
return <Spinner height="75vh" tip="Loading Pipelines..." />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Tabs defaultActiveKey="pipelines" items={tabItems} />;
|
return (
|
||||||
|
<ErrorBoundary FallbackComponent={ErrorBoundaryFallback}>
|
||||||
|
<Tabs defaultActiveKey="pipelines" items={tabItems} />;
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Pipelines;
|
export default Pipelines;
|
||||||
|
@ -494,6 +494,20 @@ export function QueryBuilderProvider({
|
|||||||
unit: query.unit || initialQueryState.unit,
|
unit: query.unit || initialQueryState.unit,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const pagination = urlQuery.get(QueryParams.pagination);
|
||||||
|
|
||||||
|
if (pagination) {
|
||||||
|
const parsedPagination = JSON.parse(pagination);
|
||||||
|
|
||||||
|
urlQuery.set(
|
||||||
|
QueryParams.pagination,
|
||||||
|
JSON.stringify({
|
||||||
|
limit: parsedPagination.limit,
|
||||||
|
offset: 0,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
urlQuery.set(
|
urlQuery.set(
|
||||||
QueryParams.compositeQuery,
|
QueryParams.compositeQuery,
|
||||||
encodeURIComponent(JSON.stringify(currentGeneratedQuery)),
|
encodeURIComponent(JSON.stringify(currentGeneratedQuery)),
|
||||||
|
@ -42,6 +42,11 @@ body {
|
|||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.u-off {
|
||||||
|
text-decoration: line-through;
|
||||||
|
text-decoration-thickness: 3px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,6 +10,6 @@ export type Props = {
|
|||||||
variables: PayloadVariables;
|
variables: PayloadVariables;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PayloadProps = {
|
export type VariableResponseProps = {
|
||||||
variableValues: string[] | number[];
|
variableValues: string[] | number[];
|
||||||
};
|
};
|
||||||
|
@ -27,6 +27,10 @@ export interface ProcessorData {
|
|||||||
trace_flags?: {
|
trace_flags?: {
|
||||||
parse_from: string;
|
parse_from: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// time parser fields
|
||||||
|
layout_type?: string;
|
||||||
|
layout?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PipelineData {
|
export interface PipelineData {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"sourceMap": true,
|
||||||
"outDir": "./dist/",
|
"outDir": "./dist/",
|
||||||
"noImplicitAny": true,
|
"noImplicitAny": true,
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
@ -20,11 +21,12 @@
|
|||||||
"baseUrl": "./src",
|
"baseUrl": "./src",
|
||||||
"downlevelIteration": true,
|
"downlevelIteration": true,
|
||||||
"plugins": [{ "name": "typescript-plugin-css-modules" }],
|
"plugins": [{ "name": "typescript-plugin-css-modules" }],
|
||||||
"types": ["node", "jest"]
|
"types": ["node", "jest"],
|
||||||
},
|
},
|
||||||
"exclude": ["node_modules"],
|
"exclude": ["node_modules"],
|
||||||
"include": [
|
"include": [
|
||||||
"./src",
|
"./src",
|
||||||
|
"./src/**/*.ts", "src/**/*.tsx", "src/**/*.d.ts",
|
||||||
"./babel.config.js",
|
"./babel.config.js",
|
||||||
"./jest.config.ts",
|
"./jest.config.ts",
|
||||||
"./.eslintrc.js",
|
"./.eslintrc.js",
|
||||||
|
@ -46,7 +46,7 @@ if (process.env.BUNDLE_ANALYSER === 'true') {
|
|||||||
*/
|
*/
|
||||||
const config = {
|
const config = {
|
||||||
mode: 'development',
|
mode: 'development',
|
||||||
devtool: 'source-map',
|
devtool: 'eval-source-map',
|
||||||
entry: resolve(__dirname, './src/index.tsx'),
|
entry: resolve(__dirname, './src/index.tsx'),
|
||||||
devServer: {
|
devServer: {
|
||||||
historyApiFallback: true,
|
historyApiFallback: true,
|
||||||
|
@ -34,9 +34,9 @@
|
|||||||
three-render-objects "1"
|
three-render-objects "1"
|
||||||
|
|
||||||
"@adobe/css-tools@^4.0.1":
|
"@adobe/css-tools@^4.0.1":
|
||||||
version "4.3.1"
|
version "4.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.3.1.tgz#abfccb8ca78075a2b6187345c26243c1a0842f28"
|
resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.3.2.tgz#a6abc715fb6884851fca9dad37fc34739a04fd11"
|
||||||
integrity sha512-/62yikz7NLScCGAAST5SHdnjaDJQBDq0M2muyRTpf2VQhw6StBg2ALiu73zSJQ4fMVLA+0uBhBHAle7Wg+2kSg==
|
integrity sha512-DA5a1C0gD/pLOvhv33YMrbf2FK3oUzwNl9oOJqE4XVjuEtt6XIakRcsd7eLiOSPkp1kTRQGICTA8cKra/vFbjw==
|
||||||
|
|
||||||
"@ampproject/remapping@^2.2.0":
|
"@ampproject/remapping@^2.2.0":
|
||||||
version "2.2.1"
|
version "2.2.1"
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# use a minimal alpine image
|
# use a minimal alpine image
|
||||||
FROM alpine:3.18.3
|
FROM alpine:3.18.5
|
||||||
|
|
||||||
# Add Maintainer Info
|
# Add Maintainer Info
|
||||||
LABEL maintainer="signoz"
|
LABEL maintainer="signoz"
|
||||||
|
@ -66,6 +66,10 @@ type PipelineOperator struct {
|
|||||||
// time_parser fields.
|
// time_parser fields.
|
||||||
Layout string `json:"layout,omitempty" yaml:"layout,omitempty"`
|
Layout string `json:"layout,omitempty" yaml:"layout,omitempty"`
|
||||||
LayoutType string `json:"layout_type,omitempty" yaml:"layout_type,omitempty"`
|
LayoutType string `json:"layout_type,omitempty" yaml:"layout_type,omitempty"`
|
||||||
|
|
||||||
|
// severity parser fields
|
||||||
|
SeverityMapping map[string][]string `json:"mapping,omitempty" yaml:"mapping,omitempty"`
|
||||||
|
OverwriteSeverityText bool `json:"overwrite_text,omitempty" yaml:"overwrite_text,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type TimestampParser struct {
|
type TimestampParser struct {
|
||||||
|
@ -138,6 +138,16 @@ func getOperators(ops []PipelineOperator) ([]PipelineOperator, error) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
// TODO(Raj): Maybe add support for gotime too eventually
|
// TODO(Raj): Maybe add support for gotime too eventually
|
||||||
|
|
||||||
|
} else if operator.Type == "severity_parser" {
|
||||||
|
parseFromParts := strings.Split(operator.ParseFrom, ".")
|
||||||
|
parseFromPath := strings.Join(parseFromParts, "?.")
|
||||||
|
|
||||||
|
operator.If = fmt.Sprintf(
|
||||||
|
`%s != nil && ( type(%s) == "string" || ( type(%s) in ["int", "float"] && %s == float(int(%s)) ) )`,
|
||||||
|
parseFromPath, parseFromPath, parseFromPath, parseFromPath, parseFromPath,
|
||||||
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
filteredOp = append(filteredOp, operator)
|
filteredOp = append(filteredOp, operator)
|
||||||
|
@ -198,6 +198,18 @@ func isValidOperator(op PipelineOperator) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case "severity_parser":
|
||||||
|
if op.ParseFrom == "" {
|
||||||
|
return fmt.Errorf("parse from of severity parsing processor %s cannot be empty", op.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
validMappingLevels := []string{"trace", "debug", "info", "warn", "error", "fatal"}
|
||||||
|
for k, _ := range op.SeverityMapping {
|
||||||
|
if !slices.Contains(validMappingLevels, strings.ToLower(k)) {
|
||||||
|
return fmt.Errorf("%s is not a valid severity in processor %s", k, op.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf(fmt.Sprintf("operator type %s not supported for %s, use one of (grok_parser, regex_parser, copy, move, add, remove, trace_parser, retain)", op.Type, op.ID))
|
return fmt.Errorf(fmt.Sprintf("operator type %s not supported for %s, use one of (grok_parser, regex_parser, copy, move, add, remove, trace_parser, retain)", op.Type, op.ID))
|
||||||
}
|
}
|
||||||
|
@ -326,6 +326,40 @@ var operatorTest = []struct {
|
|||||||
Layout: "%U",
|
Layout: "%U",
|
||||||
},
|
},
|
||||||
IsValid: false,
|
IsValid: false,
|
||||||
|
}, {
|
||||||
|
Name: "Severity Parser - valid",
|
||||||
|
Operator: PipelineOperator{
|
||||||
|
ID: "severity",
|
||||||
|
Type: "severity_parser",
|
||||||
|
ParseFrom: "attributes.test_severity",
|
||||||
|
SeverityMapping: map[string][]string{
|
||||||
|
"trace": {"test_trace"},
|
||||||
|
"fatal": {"test_fatal"},
|
||||||
|
},
|
||||||
|
OverwriteSeverityText: true,
|
||||||
|
},
|
||||||
|
IsValid: true,
|
||||||
|
}, {
|
||||||
|
Name: "Severity Parser - Parse from is required",
|
||||||
|
Operator: PipelineOperator{
|
||||||
|
ID: "severity",
|
||||||
|
Type: "severity_parser",
|
||||||
|
SeverityMapping: map[string][]string{},
|
||||||
|
OverwriteSeverityText: true,
|
||||||
|
},
|
||||||
|
IsValid: false,
|
||||||
|
}, {
|
||||||
|
Name: "Severity Parser - mapping level must be valid",
|
||||||
|
Operator: PipelineOperator{
|
||||||
|
ID: "severity",
|
||||||
|
Type: "severity_parser",
|
||||||
|
ParseFrom: "attributes.test",
|
||||||
|
SeverityMapping: map[string][]string{
|
||||||
|
"not-a-level": {"bad-level"},
|
||||||
|
},
|
||||||
|
OverwriteSeverityText: true,
|
||||||
|
},
|
||||||
|
IsValid: false,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ package logparsingpipeline
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@ -91,15 +92,15 @@ func TestPipelinePreview(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
matchingLog := makeTestLogEntry(
|
matchingLog := makeTestSignozLog(
|
||||||
"test log body",
|
"test log body",
|
||||||
map[string]string{
|
map[string]interface{}{
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
nonMatchingLog := makeTestLogEntry(
|
nonMatchingLog := makeTestSignozLog(
|
||||||
"test log body",
|
"test log body",
|
||||||
map[string]string{
|
map[string]interface{}{
|
||||||
"method": "POST",
|
"method": "POST",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -184,9 +185,9 @@ func TestGrokParsingProcessor(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
testLog := makeTestLogEntry(
|
testLog := makeTestSignozLog(
|
||||||
"2023-10-26T04:38:00.602Z INFO route/server.go:71 HTTP request received",
|
"2023-10-26T04:38:00.602Z INFO route/server.go:71 HTTP request received",
|
||||||
map[string]string{
|
map[string]interface{}{
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -314,18 +315,39 @@ func TestTraceParsingProcessor(t *testing.T) {
|
|||||||
require.Equal("", result[0].SpanID)
|
require.Equal("", result[0].SpanID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeTestLogEntry(
|
func makeTestSignozLog(
|
||||||
body string,
|
body string,
|
||||||
attributes map[string]string,
|
attributes map[string]interface{},
|
||||||
) model.SignozLog {
|
) model.SignozLog {
|
||||||
return model.SignozLog{
|
|
||||||
|
testLog := model.SignozLog{
|
||||||
Timestamp: uint64(time.Now().UnixNano()),
|
Timestamp: uint64(time.Now().UnixNano()),
|
||||||
Body: body,
|
Body: body,
|
||||||
Attributes_string: attributes,
|
Attributes_bool: map[string]bool{},
|
||||||
|
Attributes_string: map[string]string{},
|
||||||
|
Attributes_int64: map[string]int64{},
|
||||||
|
Attributes_float64: map[string]float64{},
|
||||||
Resources_string: map[string]string{},
|
Resources_string: map[string]string{},
|
||||||
SeverityText: entry.Info.String(),
|
SeverityText: entry.Info.String(),
|
||||||
SeverityNumber: uint8(entry.Info),
|
SeverityNumber: uint8(entry.Info),
|
||||||
SpanID: uuid.New().String(),
|
SpanID: uuid.New().String(),
|
||||||
TraceID: uuid.New().String(),
|
TraceID: uuid.New().String(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for k, v := range attributes {
|
||||||
|
switch v.(type) {
|
||||||
|
case bool:
|
||||||
|
testLog.Attributes_bool[k] = v.(bool)
|
||||||
|
case string:
|
||||||
|
testLog.Attributes_string[k] = v.(string)
|
||||||
|
case int:
|
||||||
|
testLog.Attributes_int64[k] = int64(v.(int))
|
||||||
|
case float64:
|
||||||
|
testLog.Attributes_float64[k] = v.(float64)
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("found attribute value of unsupported type %T in test log", v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return testLog
|
||||||
}
|
}
|
||||||
|
221
pkg/query-service/app/logparsingpipeline/severity_parser_test.go
Normal file
221
pkg/query-service/app/logparsingpipeline/severity_parser_test.go
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
package logparsingpipeline
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"go.signoz.io/signoz/pkg/query-service/model"
|
||||||
|
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSeverityParsingProcessor(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
testPipelines := []Pipeline{
|
||||||
|
{
|
||||||
|
OrderId: 1,
|
||||||
|
Name: "pipeline1",
|
||||||
|
Alias: "pipeline1",
|
||||||
|
Enabled: true,
|
||||||
|
Filter: &v3.FilterSet{
|
||||||
|
Operator: "AND",
|
||||||
|
Items: []v3.FilterItem{
|
||||||
|
{
|
||||||
|
Key: v3.AttributeKey{
|
||||||
|
Key: "method",
|
||||||
|
DataType: v3.AttributeKeyDataTypeString,
|
||||||
|
Type: v3.AttributeKeyTypeTag,
|
||||||
|
},
|
||||||
|
Operator: "=",
|
||||||
|
Value: "GET",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Config: []PipelineOperator{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var severityParserOp PipelineOperator
|
||||||
|
err := json.Unmarshal([]byte(`
|
||||||
|
{
|
||||||
|
"orderId": 1,
|
||||||
|
"enabled": true,
|
||||||
|
"type": "severity_parser",
|
||||||
|
"name": "Test severity parser",
|
||||||
|
"id": "test-severity-parser",
|
||||||
|
"parse_from": "attributes.test_severity",
|
||||||
|
"mapping": {
|
||||||
|
"trace": ["test_trace"],
|
||||||
|
"debug": ["test_debug", "2xx"],
|
||||||
|
"info": ["test_info", "3xx"],
|
||||||
|
"warn": ["test_warn", "4xx"],
|
||||||
|
"error": ["test_error", "5xx"],
|
||||||
|
"fatal": ["test_fatal"]
|
||||||
|
},
|
||||||
|
"overwrite_text": true
|
||||||
|
}
|
||||||
|
`), &severityParserOp)
|
||||||
|
require.Nil(err)
|
||||||
|
testPipelines[0].Config = append(testPipelines[0].Config, severityParserOp)
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
severityValues []interface{}
|
||||||
|
expectedSeverityText string
|
||||||
|
expectedSeverityNumber uint8
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
severityValues: []interface{}{
|
||||||
|
"test_trace", "TEST_TRACE", "trace", "Trace",
|
||||||
|
},
|
||||||
|
expectedSeverityText: "TRACE",
|
||||||
|
expectedSeverityNumber: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
severityValues: []interface{}{
|
||||||
|
"test_debug", "TEST_DEBUG", "debug", "DEBUG", 202.0,
|
||||||
|
},
|
||||||
|
expectedSeverityText: "DEBUG",
|
||||||
|
expectedSeverityNumber: 5,
|
||||||
|
}, {
|
||||||
|
severityValues: []interface{}{
|
||||||
|
"test_info", "TEST_INFO", "info", "INFO", 302.0,
|
||||||
|
},
|
||||||
|
expectedSeverityText: "INFO",
|
||||||
|
expectedSeverityNumber: 9,
|
||||||
|
}, {
|
||||||
|
severityValues: []interface{}{
|
||||||
|
"test_warn", "TEST_WARN", "warn", "WARN", 404.0,
|
||||||
|
},
|
||||||
|
expectedSeverityText: "WARN",
|
||||||
|
expectedSeverityNumber: 13,
|
||||||
|
}, {
|
||||||
|
severityValues: []interface{}{
|
||||||
|
"test_error", "TEST_ERROR", "error", "ERROR", 500.0,
|
||||||
|
},
|
||||||
|
expectedSeverityText: "ERROR",
|
||||||
|
expectedSeverityNumber: 17,
|
||||||
|
}, {
|
||||||
|
severityValues: []interface{}{
|
||||||
|
"test_fatal", "TEST_FATAL", "fatal", "FATAL",
|
||||||
|
},
|
||||||
|
expectedSeverityText: "FATAL",
|
||||||
|
expectedSeverityNumber: 21,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
inputLogs := []model.SignozLog{}
|
||||||
|
for _, severityAttribValue := range testCase.severityValues {
|
||||||
|
inputLogs = append(inputLogs, makeTestSignozLog(
|
||||||
|
"test log",
|
||||||
|
map[string]interface{}{
|
||||||
|
"method": "GET",
|
||||||
|
"test_severity": severityAttribValue,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
result, collectorWarnAndErrorLogs, err := SimulatePipelinesProcessing(
|
||||||
|
context.Background(),
|
||||||
|
testPipelines,
|
||||||
|
inputLogs,
|
||||||
|
)
|
||||||
|
|
||||||
|
require.Nil(err)
|
||||||
|
require.Equal(len(inputLogs), len(result))
|
||||||
|
require.Equal(0, len(collectorWarnAndErrorLogs), strings.Join(collectorWarnAndErrorLogs, "\n"))
|
||||||
|
processed := result[0]
|
||||||
|
|
||||||
|
require.Equal(testCase.expectedSeverityNumber, processed.SeverityNumber)
|
||||||
|
require.Equal(testCase.expectedSeverityText, processed.SeverityText)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNoCollectorErrorsFromSeverityParserForMismatchedLogs(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
testPipelineFilter := &v3.FilterSet{
|
||||||
|
Operator: "AND",
|
||||||
|
Items: []v3.FilterItem{
|
||||||
|
{
|
||||||
|
Key: v3.AttributeKey{
|
||||||
|
Key: "method",
|
||||||
|
DataType: v3.AttributeKeyDataTypeString,
|
||||||
|
Type: v3.AttributeKeyTypeTag,
|
||||||
|
},
|
||||||
|
Operator: "=",
|
||||||
|
Value: "GET",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
makeTestPipeline := func(config []PipelineOperator) Pipeline {
|
||||||
|
return Pipeline{
|
||||||
|
OrderId: 1,
|
||||||
|
Name: "pipeline1",
|
||||||
|
Alias: "pipeline1",
|
||||||
|
Enabled: true,
|
||||||
|
Filter: testPipelineFilter,
|
||||||
|
Config: config,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type pipelineTestCase struct {
|
||||||
|
Name string
|
||||||
|
Operator PipelineOperator
|
||||||
|
NonMatchingLog model.SignozLog
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []pipelineTestCase{
|
||||||
|
{
|
||||||
|
"severity parser should ignore logs with missing field",
|
||||||
|
PipelineOperator{
|
||||||
|
ID: "severity",
|
||||||
|
Type: "severity_parser",
|
||||||
|
Enabled: true,
|
||||||
|
Name: "severity parser",
|
||||||
|
ParseFrom: "attributes.test_severity",
|
||||||
|
SeverityMapping: map[string][]string{
|
||||||
|
"debug": {"debug"},
|
||||||
|
},
|
||||||
|
OverwriteSeverityText: true,
|
||||||
|
},
|
||||||
|
makeTestSignozLog("mismatching log", map[string]interface{}{
|
||||||
|
"method": "GET",
|
||||||
|
}),
|
||||||
|
}, {
|
||||||
|
"severity parser should ignore logs with invalid values.",
|
||||||
|
PipelineOperator{
|
||||||
|
ID: "severity",
|
||||||
|
Type: "severity_parser",
|
||||||
|
Enabled: true,
|
||||||
|
Name: "severity parser",
|
||||||
|
ParseFrom: "attributes.test_severity",
|
||||||
|
SeverityMapping: map[string][]string{
|
||||||
|
"debug": {"debug"},
|
||||||
|
},
|
||||||
|
OverwriteSeverityText: true,
|
||||||
|
},
|
||||||
|
makeTestSignozLog("mismatching log", map[string]interface{}{
|
||||||
|
"method": "GET",
|
||||||
|
"test_severity": 200.3,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
testPipelines := []Pipeline{makeTestPipeline([]PipelineOperator{testCase.Operator})}
|
||||||
|
|
||||||
|
result, collectorWarnAndErrorLogs, err := SimulatePipelinesProcessing(
|
||||||
|
context.Background(),
|
||||||
|
testPipelines,
|
||||||
|
[]model.SignozLog{testCase.NonMatchingLog},
|
||||||
|
)
|
||||||
|
require.Nil(err)
|
||||||
|
require.Equal(0, len(collectorWarnAndErrorLogs), strings.Join(collectorWarnAndErrorLogs, "\n"))
|
||||||
|
require.Equal(1, len(result))
|
||||||
|
}
|
||||||
|
}
|
@ -108,9 +108,9 @@ func TestTimestampParsingProcessor(t *testing.T) {
|
|||||||
testPipelines[0].Config = append(testPipelines[0].Config, timestampParserOp)
|
testPipelines[0].Config = append(testPipelines[0].Config, timestampParserOp)
|
||||||
|
|
||||||
testTimestampStr := "2023-11-27T12:03:28.239907+0530"
|
testTimestampStr := "2023-11-27T12:03:28.239907+0530"
|
||||||
testLog := makeTestLogEntry(
|
testLog := makeTestSignozLog(
|
||||||
"test log",
|
"test log",
|
||||||
map[string]string{
|
map[string]interface{}{
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"test_timestamp": testTimestampStr,
|
"test_timestamp": testTimestampStr,
|
||||||
},
|
},
|
||||||
|
Loading…
x
Reference in New Issue
Block a user