Merge pull request #2225 from SigNoz/release/v0.16.0

Release/v0.16.0
This commit is contained in:
Ankit Nayan 2023-02-11 23:52:58 +05:30 committed by GitHub
commit cb22aef36f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
130 changed files with 1913 additions and 14824 deletions

2
.github/config.yml vendored
View File

@ -17,7 +17,7 @@ newPRWelcomeComment: >
# Comment to be posted to on pull requests merged by a first time user
firstPRMergeComment: >
Congrats on merging your first pull request!
![minion-party](https://i.imgur.com/Xlg59lP.gif)
We here at SigNoz are proud of you! 🥳

View File

@ -57,7 +57,7 @@ jobs:
--set frontend.service.type=LoadBalancer \
--set queryService.image.tag=$DOCKER_TAG \
--set frontend.image.tag=$DOCKER_TAG
# get pods, services and the container images
kubectl get pods -n platform
kubectl get svc -n platform

View File

@ -17,4 +17,3 @@ jobs:
uses: hattan/verify-linked-issue-action@v1.1.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -24,4 +24,3 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

View File

@ -27,12 +27,6 @@ For x86 chip (amd):
docker-compose -f docker/clickhouse-setup/docker-compose.yaml up -d
```
For Mac with Apple chip (arm):
```sh
docker-compose -f docker/clickhouse-setup/docker-compose.arm.yaml up -d
```
Open http://localhost:3301 in your favourite browser. In couple of minutes, you should see
the data generated from hotrod in SigNoz UI.

View File

@ -137,7 +137,7 @@ services:
condition: on-failure
query-service:
image: signoz/query-service:0.15.0
image: signoz/query-service:0.16.0
command: ["-config=/root/config/prometheus.yml"]
# ports:
# - "6060:6060" # pprof port
@ -166,7 +166,7 @@ services:
<<: *clickhouse-depend
frontend:
image: signoz/frontend:0.15.0
image: signoz/frontend:0.16.0
deploy:
restart_policy:
condition: on-failure
@ -179,7 +179,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector:
image: signoz/signoz-otel-collector:0.66.3
image: signoz/signoz-otel-collector:0.66.4
command: ["--config=/etc/otel-collector-config.yaml"]
user: root # required for reading docker container logs
volumes:
@ -188,6 +188,7 @@ services:
environment:
- OTEL_RESOURCE_ATTRIBUTES=host.name={{.Node.Hostname}},os.type={{.Node.Platform.OS}},dockerswarm.service.name={{.Service.Name}},dockerswarm.task.name={{.Task.Name}}
- DOCKER_MULTI_NODE_CLUSTER=false
- LOW_CARDINAL_EXCEPTION_GROUPING=false
ports:
# - "1777:1777" # pprof extension
- "4317:4317" # OTLP gRPC receiver
@ -207,7 +208,7 @@ services:
<<: *clickhouse-depend
otel-collector-metrics:
image: signoz/signoz-otel-collector:0.66.3
image: signoz/signoz-otel-collector:0.66.4
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
volumes:
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml

View File

@ -110,6 +110,7 @@ exporters:
clickhousetraces:
datasource: tcp://clickhouse:9000/?database=signoz_traces
docker_multi_node_cluster: ${DOCKER_MULTI_NODE_CLUSTER}
low_cardinal_exception_grouping: ${LOW_CARDINAL_EXCEPTION_GROUPING}
clickhousemetricswrite:
endpoint: tcp://clickhouse:9000/?database=signoz_metrics
resource_to_telemetry_conversion:

View File

@ -41,7 +41,7 @@ services:
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
otel-collector:
container_name: otel-collector
image: signoz/signoz-otel-collector:0.66.3
image: signoz/signoz-otel-collector:0.66.4
command: ["--config=/etc/otel-collector-config.yaml"]
# user: root # required for reading docker container logs
volumes:
@ -67,7 +67,7 @@ services:
otel-collector-metrics:
container_name: otel-collector-metrics
image: signoz/signoz-otel-collector:0.66.3
image: signoz/signoz-otel-collector:0.66.4
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
volumes:
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml

View File

@ -153,7 +153,7 @@ services:
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
query-service:
image: signoz/query-service:${DOCKER_TAG:-0.15.0}
image: signoz/query-service:${DOCKER_TAG:-0.16.0}
container_name: query-service
command: ["-config=/root/config/prometheus.yml"]
# ports:
@ -181,7 +181,7 @@ services:
<<: *clickhouse-depend
frontend:
image: signoz/frontend:${DOCKER_TAG:-0.15.0}
image: signoz/frontend:${DOCKER_TAG:-0.16.0}
container_name: frontend
restart: on-failure
depends_on:
@ -193,7 +193,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector:
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.66.3}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.66.4}
command: ["--config=/etc/otel-collector-config.yaml"]
user: root # required for reading docker container logs
volumes:
@ -202,6 +202,7 @@ services:
environment:
- OTEL_RESOURCE_ATTRIBUTES=host.name=signoz-host,os.type=linux
- DOCKER_MULTI_NODE_CLUSTER=false
- LOW_CARDINAL_EXCEPTION_GROUPING=false
ports:
# - "1777:1777" # pprof extension
- "4317:4317" # OTLP gRPC receiver
@ -218,7 +219,7 @@ services:
<<: *clickhouse-depend
otel-collector-metrics:
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.66.3}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.66.4}
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
volumes:
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
@ -231,15 +232,15 @@ services:
<<: *clickhouse-depend
hotrod:
image: jaegertracing/example-hotrod:1.30
container_name: hotrod
logging:
options:
max-size: 50m
max-file: "3"
command: ["all"]
environment:
- JAEGER_ENDPOINT=http://otel-collector:14268/api/traces
image: jaegertracing/example-hotrod:1.30
container_name: hotrod
logging:
options:
max-size: 50m
max-file: "3"
command: ["all"]
environment:
- JAEGER_ENDPOINT=http://otel-collector:14268/api/traces
load-hotrod:
image: "grubykarol/locust:1.2.3-python3.9-alpine3.12"

View File

@ -119,6 +119,7 @@ exporters:
clickhousetraces:
datasource: tcp://clickhouse:9000/?database=signoz_traces
docker_multi_node_cluster: ${DOCKER_MULTI_NODE_CLUSTER}
low_cardinal_exception_grouping: ${LOW_CARDINAL_EXCEPTION_GROUPING}
clickhousemetricswrite:
endpoint: tcp://clickhouse:9000/?database=signoz_metrics
resource_to_telemetry_conversion:

View File

@ -81,6 +81,11 @@ check_os() {
os="centos"
package_manager="yum"
;;
Rocky*)
desired_os=1
os="centos"
package_manager="yum"
;;
SLES*)
desired_os=1
os="sles"

View File

@ -25,6 +25,7 @@ const config: Config.InitialOptions = {
setupFilesAfterEnv: ['<rootDir>jest.setup.ts'],
testPathIgnorePatterns: ['/node_modules/', '/public/'],
moduleDirectories: ['node_modules', 'src'],
testEnvironment: 'jest-environment-jsdom',
testEnvironmentOptions: {
'jest-playwright': {
browsers: ['chromium', 'firefox', 'webkit'],

View File

@ -31,10 +31,6 @@
"@ant-design/icons": "4.8.0",
"@grafana/data": "^8.4.3",
"@monaco-editor/react": "^4.3.1",
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
"@welldone-software/why-did-you-render": "^6.2.1",
"@xstate/react": "^3.0.0",
"antd": "5.0.5",
"axios": "^0.21.0",
@ -82,7 +78,6 @@
"react-router-dom": "^5.2.0",
"react-use": "^17.3.2",
"react-virtuoso": "4.0.3",
"react-vis": "^1.11.7",
"redux": "^4.0.5",
"redux-thunk": "^2.3.0",
"stream": "^0.0.2",
@ -122,7 +117,9 @@
"@commitlint/config-conventional": "^16.2.4",
"@jest/globals": "^27.5.1",
"@playwright/test": "^1.22.0",
"@testing-library/react-hooks": "^7.0.2",
"@testing-library/jest-dom": "5.16.5",
"@testing-library/react": "13.4.0",
"@testing-library/user-event": "14.4.3",
"@types/color": "^3.0.3",
"@types/compression-webpack-plugin": "^9.0.0",
"@types/copy-webpack-plugin": "^8.0.1",
@ -138,6 +135,7 @@
"@types/react-dom": "18.0.10",
"@types/react-grid-layout": "^1.1.2",
"@types/react-redux": "^7.1.11",
"@types/react-resizable": "3.0.3",
"@types/react-router-dom": "^5.1.6",
"@types/redux": "^3.6.0",
"@types/styled-components": "^5.1.4",
@ -147,6 +145,7 @@
"@types/webpack-dev-server": "^4.3.0",
"@typescript-eslint/eslint-plugin": "^4.28.2",
"@typescript-eslint/parser": "^4.28.2",
"@welldone-software/why-did-you-render": "6.2.1",
"autoprefixer": "^9.0.0",
"babel-plugin-styled-components": "^1.12.0",
"compression-webpack-plugin": "9.0.0",
@ -176,6 +175,7 @@
"portfinder-sync": "^0.0.2",
"prettier": "2.2.1",
"react-hot-loader": "^4.13.0",
"react-resizable": "3.0.4",
"ts-jest": "^27.1.4",
"ts-node": "^10.2.1",
"typescript-plugin-css-modules": "^3.4.0",

View File

@ -47,6 +47,8 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
const dispatch = useDispatch<Dispatch<AppActions>>();
const [notifications, NotificationElement] = notification.useNotification();
const currentRoute = mapRoutes.get('current');
const navigateToLoginIfNotLoggedIn = (isLoggedIn = isLoggedInState): void => {
@ -106,7 +108,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
} else {
Logout();
notification.error({
notifications.error({
message: response.error || t('something_went_wrong'),
});
}
@ -155,7 +157,12 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
// NOTE: disabling this rule as there is no need to have div
// eslint-disable-next-line react/jsx-no-useless-fragment
return <>{children}</>;
return (
<>
{NotificationElement}
{children}
</>
);
}
interface PrivateRouteProps {

View File

@ -37,7 +37,7 @@ const getSpans = async (
start: String(props.start),
end: String(props.end),
function: props.function,
groupBy: props.groupBy,
groupBy: props.groupBy === 'none' ? '' : props.groupBy,
step: props.step,
tags: updatedSelectedTags,
...nonDuration,

View File

@ -23,8 +23,10 @@ import {
} from 'chart.js';
import * as chartjsAdapter from 'chartjs-adapter-date-fns';
import annotationPlugin from 'chartjs-plugin-annotation';
import dayjs from 'dayjs';
import { useIsDarkMode } from 'hooks/useDarkMode';
import React, { useCallback, useEffect, useRef } from 'react';
import isEqual from 'lodash-es/isEqual';
import React, { memo, useCallback, useEffect, useRef } from 'react';
import { hasData } from './hasData';
import { getAxisLabelColor } from './helpers';
@ -80,6 +82,7 @@ function Graph({
onDragSelect,
dragSelectColor,
}: GraphProps): JSX.Element {
const nearestDatasetIndex = useRef<null | number>(null);
const chartRef = useRef<HTMLCanvasElement>(null);
const isDarkMode = useIsDarkMode();
@ -150,6 +153,10 @@ function Graph({
},
tooltip: {
callbacks: {
title(context) {
const date = dayjs(context[0].parsed.x);
return date.format('MMM DD, YYYY, HH:mm:ss');
},
label(context) {
let label = context.dataset.label || '';
@ -159,8 +166,16 @@ function Graph({
if (context.parsed.y !== null) {
label += getToolTipValue(context.parsed.y.toString(), yAxisUnit);
}
return label;
},
labelTextColor(labelData) {
if (labelData.datasetIndex === nearestDatasetIndex.current) {
return 'rgba(255, 255, 255, 1)';
}
return 'rgba(255, 255, 255, 0.75)';
},
},
},
[dragSelectPluginId]: createDragSelectPluginOptions(
@ -226,12 +241,38 @@ function Graph({
tension: 0,
cubicInterpolationMode: 'monotone',
},
point: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
hoverBackgroundColor: (ctx: any) => {
if (ctx?.element?.options?.borderColor) {
return ctx.element.options.borderColor;
}
return 'rgba(0,0,0,0.1)';
},
hoverRadius: 5,
},
},
onClick: (event, element, chart) => {
if (onClickHandler) {
onClickHandler(event, element, chart, data);
}
},
onHover: (event, _, chart) => {
if (event.native) {
const interactions = chart.getElementsAtEventForMode(
event.native,
'nearest',
{
intersect: false,
},
true,
);
if (interactions[0]) {
nearestDatasetIndex.current = interactions[0].datasetIndex;
}
}
},
};
const chartHasData = hasData(data);
@ -334,4 +375,7 @@ Graph.defaultProps = {
onDragSelect: undefined,
dragSelectColor: undefined,
};
export default Graph;
export default memo(Graph, (prevProps, nextProps) =>
isEqual(prevProps.data, nextProps.data),
);

View File

@ -2,7 +2,9 @@ import { Button, Popover } from 'antd';
import { generateFilterQuery } from 'lib/logs/generateFilterQuery';
import React, { memo, useCallback, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Dispatch } from 'redux';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import { SET_SEARCH_QUERY_STRING } from 'types/actions/logs';
import { ILogsReducer } from 'types/reducer/logs';
@ -14,7 +16,7 @@ function AddToQueryHOC({
const {
searchFilter: { queryString },
} = useSelector<AppState, ILogsReducer>((store) => store.logs);
const dispatch = useDispatch();
const dispatch = useDispatch<Dispatch<AppActions>>();
const generatedQuery = useMemo(
() => generateFilterQuery({ fieldKey, fieldValue, type: 'IN' }),
@ -31,7 +33,9 @@ function AddToQueryHOC({
}
dispatch({
type: SET_SEARCH_QUERY_STRING,
payload: updatedQueryString,
payload: {
searchQueryString: updatedQueryString,
},
});
}, [dispatch, generatedQuery, queryString]);

View File

@ -7,14 +7,14 @@ function CopyClipboardHOC({
children,
}: CopyClipboardHOCProps): JSX.Element {
const [value, setCopy] = useCopyToClipboard();
const [notifications, NotificationElement] = notification.useNotification();
useEffect(() => {
if (value.value) {
notification.success({
notifications.success({
message: 'Copied to clipboard',
});
}
}, [value]);
}, [value, notifications]);
const onClick = useCallback((): void => {
setCopy(textToCopy);
@ -22,6 +22,7 @@ function CopyClipboardHOC({
return (
<span onClick={onClick} onKeyDown={onClick} role="button" tabIndex={0}>
{NotificationElement}
<Popover
placement="top"
content={<span style={{ fontSize: '0.9rem' }}>Copy to clipboard</span>}

View File

@ -79,6 +79,7 @@ function LogItem({ logData }: LogItemProps): JSX.Element {
const dispatch = useDispatch();
const flattenLogData = useMemo(() => FlatLogData(logData), [logData]);
const [, setCopy] = useCopyToClipboard();
const [notifications, NotificationElement] = notification.useNotification();
const handleDetailedView = useCallback(() => {
dispatch({
@ -89,27 +90,22 @@ function LogItem({ logData }: LogItemProps): JSX.Element {
const handleCopyJSON = (): void => {
setCopy(JSON.stringify(logData, null, 2));
notification.success({
notifications.success({
message: 'Copied to clipboard',
});
};
return (
<Container>
{NotificationElement}
<div>
<div>
{'{'}
<LogContainer>
<>
<LogGeneralField
fieldKey="log"
fieldValue={flattenLogData.body as never}
/>
<LogGeneralField fieldKey="log" fieldValue={flattenLogData.body} />
{flattenLogData.stream && (
<LogGeneralField
fieldKey="stream"
fieldValue={flattenLogData.stream as never}
/>
<LogGeneralField fieldKey="stream" fieldValue={flattenLogData.stream} />
)}
<LogGeneralField
fieldKey="timestamp"

View File

@ -1,7 +1,3 @@
/**
* @jest-environment jsdom
*/
import { render } from '@testing-library/react';
import React from 'react';
import { Provider } from 'react-redux';

View File

@ -0,0 +1,51 @@
import React, { useMemo } from 'react';
import { Resizable, ResizeCallbackData } from 'react-resizable';
import { enableUserSelectHack } from './config';
import { SpanStyle } from './styles';
function ResizableHeader(props: ResizableHeaderProps): JSX.Element {
const { onResize, width, ...restProps } = props;
const handle = useMemo(
() => (
<SpanStyle
className="react-resizable-handle"
onClick={(e): void => e.stopPropagation()}
/>
),
[],
);
const draggableOpts = useMemo(
() => ({
enableUserSelectHack,
}),
[],
);
if (!width) {
// eslint-disable-next-line react/jsx-props-no-spreading
return <th {...restProps} />;
}
return (
<Resizable
width={width}
height={0}
handle={handle}
onResize={onResize}
draggableOpts={draggableOpts}
>
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
<th {...restProps} />
</Resizable>
);
}
interface ResizableHeaderProps {
onResize: (e: React.SyntheticEvent<Element>, data: ResizeCallbackData) => void;
width: number;
}
export default ResizableHeader;

View File

@ -0,0 +1,51 @@
import { Table } from 'antd';
import type { TableProps } from 'antd/es/table';
import { ColumnsType } from 'antd/lib/table';
import React, { useCallback, useMemo, useState } from 'react';
import { ResizeCallbackData } from 'react-resizable';
import ResizableHeader from './ResizableHeader';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function ResizeTable({ columns, ...restprops }: TableProps<any>): JSX.Element {
const [columnsData, setColumns] = useState<ColumnsType>(columns || []);
const handleResize = useCallback(
(index: number) => (
_e: React.SyntheticEvent<Element>,
{ size }: ResizeCallbackData,
): void => {
const newColumns = [...columnsData];
newColumns[index] = {
...newColumns[index],
width: size.width,
};
setColumns(newColumns);
},
[columnsData],
);
const mergeColumns = useMemo(
() =>
columnsData.map((col, index) => ({
...col,
onHeaderCell: (column: ColumnsType<unknown>[number]): unknown => ({
width: column.width,
onResize: handleResize(index),
}),
})),
[columnsData, handleResize],
);
return (
<Table
// eslint-disable-next-line react/jsx-props-no-spreading
{...restprops}
components={{ header: { cell: ResizableHeader } }}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
columns={mergeColumns as ColumnsType<any>}
/>
);
}
export default ResizeTable;

View File

@ -0,0 +1 @@
export const enableUserSelectHack = { enableUserSelectHack: false };

View File

@ -0,0 +1,4 @@
import ResizableHeader from './ResizableHeader';
import ResizeTable from './ResizeTable';
export { ResizableHeader, ResizeTable };

View File

@ -0,0 +1,11 @@
import styled from 'styled-components';
export const SpanStyle = styled.span`
position: absolute;
right: -5px;
bottom: 0;
z-index: 1;
width: 10px;
height: 100%;
cursor: col-resize;
`;

View File

@ -37,6 +37,7 @@ const themeColors = {
matterhornGrey: '#555555',
whiteCream: '#ffffffd5',
black: '#000000',
lightgrey: '#ddd',
};
export { themeColors };

View File

@ -1,6 +1,7 @@
/* eslint-disable react/display-name */
import { Button, notification, Table } from 'antd';
import { Button, notification } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import { ResizeTable } from 'components/ResizeTable';
import ROUTES from 'constants/routes';
import useComponentPermission from 'hooks/useComponentPermission';
import history from 'lib/history';
@ -34,11 +35,13 @@ function AlertChannels({ allChannels }: AlertChannelsProps): JSX.Element {
title: t('column_channel_name'),
dataIndex: 'name',
key: 'name',
width: 100,
},
{
title: t('column_channel_type'),
dataIndex: 'type',
key: 'type',
width: 80,
},
];
@ -48,6 +51,7 @@ function AlertChannels({ allChannels }: AlertChannelsProps): JSX.Element {
dataIndex: 'id',
key: 'action',
align: 'center',
width: 80,
render: (id: string): JSX.Element => (
<>
<Button onClick={(): void => onClickEditHandler(id)} type="link">
@ -62,8 +66,7 @@ function AlertChannels({ allChannels }: AlertChannelsProps): JSX.Element {
return (
<>
{Element}
<Table rowKey="id" dataSource={channels} columns={columns} />
<ResizeTable columns={columns} dataSource={channels} rowKey="id" />
</>
);
}

View File

@ -5,7 +5,6 @@ import {
Input,
notification,
Space,
Table,
TableProps,
Tooltip,
Typography,
@ -16,6 +15,7 @@ import { ColumnsType } from 'antd/lib/table';
import { FilterConfirmProps } from 'antd/lib/table/interface';
import getAll from 'api/errors/getAll';
import getErrorCounts from 'api/errors/getErrorCounts';
import { ResizeTable } from 'components/ResizeTable';
import ROUTES from 'constants/routes';
import dayjs from 'dayjs';
import useUrlQuery from 'hooks/useUrlQuery';
@ -127,14 +127,15 @@ function AllErrors(): JSX.Element {
enabled: !loading,
},
]);
const [notifications, NotificationElement] = notification.useNotification();
useEffect(() => {
if (data?.error) {
notification.error({
notifications.error({
message: data.error || t('something_went_wrong'),
});
}
}, [data?.error, data?.payload, t]);
}, [data?.error, data?.payload, t, notifications]);
const getDateValue = (value: string): JSX.Element => (
<Typography>{dayjs(value).format('DD/MM/YYYY HH:mm:ss A')}</Typography>
@ -258,6 +259,7 @@ function AllErrors(): JSX.Element {
const columns: ColumnsType<Exception> = [
{
title: 'Exception Type',
width: 100,
dataIndex: 'exceptionType',
key: 'exceptionType',
...getFilter(onExceptionTypeFilter, 'Search By Exception', 'exceptionType'),
@ -283,6 +285,7 @@ function AllErrors(): JSX.Element {
title: 'Error Message',
dataIndex: 'exceptionMessage',
key: 'exceptionMessage',
width: 100,
render: (value): JSX.Element => (
<Tooltip overlay={(): JSX.Element => value}>
<Typography.Paragraph
@ -297,6 +300,7 @@ function AllErrors(): JSX.Element {
},
{
title: 'Count',
width: 50,
dataIndex: 'exceptionCount',
key: 'exceptionCount',
sorter: true,
@ -309,6 +313,7 @@ function AllErrors(): JSX.Element {
{
title: 'Last Seen',
dataIndex: 'lastSeen',
width: 80,
key: 'lastSeen',
render: getDateValue,
sorter: true,
@ -321,6 +326,7 @@ function AllErrors(): JSX.Element {
{
title: 'First Seen',
dataIndex: 'firstSeen',
width: 80,
key: 'firstSeen',
render: getDateValue,
sorter: true,
@ -333,6 +339,7 @@ function AllErrors(): JSX.Element {
{
title: 'Application',
dataIndex: 'serviceName',
width: 100,
key: 'serviceName',
sorter: true,
defaultSortOrder: getDefaultOrder(
@ -379,21 +386,24 @@ function AllErrors(): JSX.Element {
);
return (
<Table
tableLayout="fixed"
dataSource={data?.payload as Exception[]}
columns={columns}
rowKey="firstSeen"
loading={isLoading || false || errorCountResponse.status === 'loading'}
pagination={{
pageSize: getUpdatedPageSize,
responsive: true,
current: getUpdatedOffset / 10 + 1,
position: ['bottomLeft'],
total: errorCountResponse.data?.payload || 0,
}}
onChange={onChangeHandler}
/>
<>
{NotificationElement}
<ResizeTable
columns={columns}
tableLayout="fixed"
dataSource={data?.payload as Exception[]}
rowKey="firstSeen"
loading={isLoading || false || errorCountResponse.status === 'loading'}
pagination={{
pageSize: getUpdatedPageSize,
responsive: true,
current: getUpdatedOffset / 10 + 1,
position: ['bottomLeft'],
total: errorCountResponse.data?.payload || 0,
}}
onChange={onChangeHandler}
/>
</>
);
}

View File

@ -91,6 +91,8 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
const latestVersionCounter = useRef(0);
const latestConfigCounter = useRef(0);
const [notifications, NotificationElement] = notification.useNotification();
useEffect(() => {
if (
getUserLatestVersionResponse.isFetched &&
@ -105,7 +107,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
isError: true,
},
});
notification.error({
notifications.error({
message: t('oops_something_went_wrong_version'),
});
}
@ -123,7 +125,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
isError: true,
},
});
notification.error({
notifications.error({
message: t('oops_something_went_wrong_version'),
});
}
@ -219,12 +221,14 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
getDynamicConfigsResponse.data,
getDynamicConfigsResponse.isFetched,
getDynamicConfigsResponse.isSuccess,
notifications,
]);
const isToDisplayLayout = isLoggedIn;
return (
<Layout>
{NotificationElement}
{isToDisplayLayout && <Header />}
<Layout>
{isToDisplayLayout && <SideNav />}

View File

@ -6,6 +6,16 @@ import {
defaultMatchType,
} from 'types/api/alerts/def';
const defaultAlertDescription =
'This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})';
const defaultAlertSummary =
'The rule threshold is set to {{$threshold}}, and the observed metric value is {{$value}}';
const defaultAnnotations = {
description: defaultAlertDescription,
summary: defaultAlertSummary,
};
export const alertDefaults: AlertDef = {
alertType: AlertTypes.METRICS_BASED_ALERT,
condition: {
@ -38,9 +48,7 @@ export const alertDefaults: AlertDef = {
labels: {
severity: 'warning',
},
annotations: {
description: 'A new alert',
},
annotations: defaultAnnotations,
evalWindow: defaultEvalWindow,
};
@ -85,9 +93,7 @@ export const logAlertDefaults: AlertDef = {
severity: 'warning',
details: `${window.location.protocol}//${window.location.host}/logs`,
},
annotations: {
description: 'A new log-based alert',
},
annotations: defaultAnnotations,
evalWindow: defaultEvalWindow,
};
@ -132,9 +138,7 @@ export const traceAlertDefaults: AlertDef = {
severity: 'warning',
details: `${window.location.protocol}//${window.location.host}/traces`,
},
annotations: {
description: 'A new trace-based alert',
},
annotations: defaultAnnotations,
evalWindow: defaultEvalWindow,
};
@ -179,8 +183,6 @@ export const exceptionAlertDefaults: AlertDef = {
severity: 'warning',
details: `${window.location.protocol}//${window.location.host}/exceptions`,
},
annotations: {
description: 'A new exceptions-based alert',
},
annotations: defaultAnnotations,
evalWindow: defaultEvalWindow,
};

View File

@ -1,6 +1,7 @@
import { Button, Divider, notification, Space, Table, Typography } from 'antd';
import { Button, Divider, notification, Space, Typography } from 'antd';
import getNextPrevId from 'api/errors/getNextPrevId';
import Editor from 'components/Editor';
import { ResizeTable } from 'components/ResizeTable';
import { getNanoSeconds } from 'container/AllError/utils';
import dayjs from 'dayjs';
import history from 'lib/history';
@ -53,12 +54,14 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
() => [
{
title: 'Key',
width: 100,
dataIndex: 'key',
key: 'key',
},
{
title: 'Value',
dataIndex: 'value',
width: 100,
key: 'value',
},
],
@ -77,13 +80,15 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
[],
);
const [notifications, NotificationElement] = notification.useNotification();
const onClickErrorIdHandler = async (
id: string,
timestamp: string,
): Promise<void> => {
try {
if (id.length === 0) {
notification.error({
notifications.error({
message: 'Error Id cannot be empty',
});
return;
@ -95,7 +100,7 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
}&timestamp=${getNanoSeconds(timestamp)}&errorId=${id}`,
);
} catch (error) {
notification.error({
notifications.error({
message: t('something_went_wrong'),
});
}
@ -116,6 +121,7 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
return (
<>
{NotificationElement}
<Typography>{errorDetail.exceptionType}</Typography>
<Typography>{errorDetail.exceptionMessage}</Typography>
<Divider />
@ -167,7 +173,7 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
<EditorContainer>
<Space direction="vertical">
<Table tableLayout="fixed" columns={columns} dataSource={data} />
<ResizeTable columns={columns} tableLayout="fixed" dataSource={data} />
</Space>
</EditorContainer>
</>

View File

@ -1,5 +1,4 @@
import { Input, Select } from 'antd';
import FormItem from 'antd/lib/form/FormItem';
import { Form, Input, Select } from 'antd';
import { LabelFilterStatement } from 'container/CreateAlertChannels/config';
import React from 'react';
@ -10,7 +9,7 @@ const { Option } = Select;
// point
function LabelFilterForm({ setFilter }: LabelFilterProps): JSX.Element {
return (
<FormItem name="label_filter" label="Notify When (Optional)">
<Form.Item name="label_filter" label="Notify When (Optional)">
<Input.Group compact>
<Select
defaultValue="Severity"
@ -51,7 +50,7 @@ function LabelFilterForm({ setFilter }: LabelFilterProps): JSX.Element {
}}
/>
</Input.Group>
</FormItem>
</Form.Item>
);
}

View File

@ -1,5 +1,4 @@
import { Input } from 'antd';
import FormItem from 'antd/lib/form/FormItem';
import { Form, Input } from 'antd';
import React from 'react';
import { useTranslation } from 'react-i18next';
@ -11,7 +10,7 @@ function PagerForm({ setSelectedConfig }: PagerFormProps): JSX.Element {
const { t } = useTranslation('channels');
return (
<>
<FormItem name="routing_key" label={t('field_pager_routing_key')} required>
<Form.Item name="routing_key" label={t('field_pager_routing_key')} required>
<Input
onChange={(event): void => {
setSelectedConfig((value) => ({
@ -20,9 +19,9 @@ function PagerForm({ setSelectedConfig }: PagerFormProps): JSX.Element {
}));
}}
/>
</FormItem>
</Form.Item>
<FormItem
<Form.Item
name="description"
help={t('help_pager_description')}
label={t('field_pager_description')}
@ -38,9 +37,9 @@ function PagerForm({ setSelectedConfig }: PagerFormProps): JSX.Element {
}
placeholder={t('placeholder_pager_description')}
/>
</FormItem>
</Form.Item>
<FormItem
<Form.Item
name="severity"
help={t('help_pager_severity')}
label={t('field_pager_severity')}
@ -53,9 +52,9 @@ function PagerForm({ setSelectedConfig }: PagerFormProps): JSX.Element {
}))
}
/>
</FormItem>
</Form.Item>
<FormItem
<Form.Item
name="details"
help={t('help_pager_details')}
label={t('field_pager_details')}
@ -69,9 +68,9 @@ function PagerForm({ setSelectedConfig }: PagerFormProps): JSX.Element {
}))
}
/>
</FormItem>
</Form.Item>
<FormItem
<Form.Item
name="component"
help={t('help_pager_component')}
label={t('field_pager_component')}
@ -84,9 +83,9 @@ function PagerForm({ setSelectedConfig }: PagerFormProps): JSX.Element {
}))
}
/>
</FormItem>
</Form.Item>
<FormItem
<Form.Item
name="group"
help={t('help_pager_group')}
label={t('field_pager_group')}
@ -99,9 +98,9 @@ function PagerForm({ setSelectedConfig }: PagerFormProps): JSX.Element {
}))
}
/>
</FormItem>
</Form.Item>
<FormItem
<Form.Item
name="class"
help={t('help_pager_class')}
label={t('field_pager_class')}
@ -114,8 +113,8 @@ function PagerForm({ setSelectedConfig }: PagerFormProps): JSX.Element {
}))
}
/>
</FormItem>
<FormItem
</Form.Item>
<Form.Item
name="client"
help={t('help_pager_client')}
label={t('field_pager_client')}
@ -128,9 +127,9 @@ function PagerForm({ setSelectedConfig }: PagerFormProps): JSX.Element {
}))
}
/>
</FormItem>
</Form.Item>
<FormItem
<Form.Item
name="client_url"
help={t('help_pager_client_url')}
label={t('field_pager_client_url')}
@ -143,7 +142,7 @@ function PagerForm({ setSelectedConfig }: PagerFormProps): JSX.Element {
}))
}
/>
</FormItem>
</Form.Item>
</>
);
}

View File

@ -1,5 +1,4 @@
import { Input } from 'antd';
import FormItem from 'antd/lib/form/FormItem';
import { Form, Input } from 'antd';
import React from 'react';
import { useTranslation } from 'react-i18next';
@ -12,7 +11,7 @@ function Slack({ setSelectedConfig }: SlackProps): JSX.Element {
return (
<>
<FormItem name="api_url" label={t('field_webhook_url')}>
<Form.Item name="api_url" label={t('field_webhook_url')}>
<Input
onChange={(event): void => {
setSelectedConfig((value) => ({
@ -21,9 +20,9 @@ function Slack({ setSelectedConfig }: SlackProps): JSX.Element {
}));
}}
/>
</FormItem>
</Form.Item>
<FormItem
<Form.Item
name="channel"
help={t('slack_channel_help')}
label={t('field_slack_recipient')}
@ -36,9 +35,9 @@ function Slack({ setSelectedConfig }: SlackProps): JSX.Element {
}))
}
/>
</FormItem>
</Form.Item>
<FormItem name="title" label={t('field_slack_title')}>
<Form.Item name="title" label={t('field_slack_title')}>
<TextArea
rows={4}
// value={`[{{ .Status | toUpper }}{{ if eq .Status \"firing\" }}:{{ .Alerts.Firing | len }}{{ end }}] {{ .CommonLabels.alertname }} for {{ .CommonLabels.job }}\n{{- if gt (len .CommonLabels) (len .GroupLabels) -}}\n{{\" \"}}(\n{{- with .CommonLabels.Remove .GroupLabels.Names }}\n {{- range $index, $label := .SortedPairs -}}\n {{ if $index }}, {{ end }}\n {{- $label.Name }}=\"{{ $label.Value -}}\"\n {{- end }}\n{{- end -}}\n)\n{{- end }}`}
@ -49,9 +48,9 @@ function Slack({ setSelectedConfig }: SlackProps): JSX.Element {
}))
}
/>
</FormItem>
</Form.Item>
<FormItem name="text" label={t('field_slack_description')}>
<Form.Item name="text" label={t('field_slack_description')}>
<TextArea
onChange={(event): void =>
setSelectedConfig((value) => ({
@ -61,7 +60,7 @@ function Slack({ setSelectedConfig }: SlackProps): JSX.Element {
}
placeholder={t('placeholder_slack_description')}
/>
</FormItem>
</Form.Item>
</>
);
}

View File

@ -1,5 +1,4 @@
import { Input } from 'antd';
import FormItem from 'antd/lib/form/FormItem';
import { Form, Input } from 'antd';
import React from 'react';
import { useTranslation } from 'react-i18next';
@ -10,7 +9,7 @@ function WebhookSettings({ setSelectedConfig }: WebhookProps): JSX.Element {
return (
<>
<FormItem name="api_url" label={t('field_webhook_url')}>
<Form.Item name="api_url" label={t('field_webhook_url')}>
<Input
onChange={(event): void => {
setSelectedConfig((value) => ({
@ -19,8 +18,8 @@ function WebhookSettings({ setSelectedConfig }: WebhookProps): JSX.Element {
}));
}}
/>
</FormItem>
<FormItem
</Form.Item>
<Form.Item
name="username"
label={t('field_webhook_username')}
help={t('help_webhook_username')}
@ -33,8 +32,8 @@ function WebhookSettings({ setSelectedConfig }: WebhookProps): JSX.Element {
}));
}}
/>
</FormItem>
<FormItem
</Form.Item>
<Form.Item
name="password"
label="Password (optional)"
help={t('help_webhook_password')}
@ -48,7 +47,7 @@ function WebhookSettings({ setSelectedConfig }: WebhookProps): JSX.Element {
}));
}}
/>
</FormItem>
</Form.Item>
</>
);
}

View File

@ -1,5 +1,4 @@
import { Form, FormInstance, Input, Select, Typography } from 'antd';
import FormItem from 'antd/lib/form/FormItem';
import { Store } from 'antd/lib/form/interface';
import ROUTES from 'constants/routes';
import {
@ -59,7 +58,7 @@ function FormAlertChannels({
<Title level={3}>{title}</Title>
<Form initialValues={initialValue} layout="vertical" form={formInstance}>
<FormItem label={t('field_channel_name')} labelAlign="left" name="name">
<Form.Item label={t('field_channel_name')} labelAlign="left" name="name">
<Input
disabled={editing}
onChange={(event): void => {
@ -69,9 +68,9 @@ function FormAlertChannels({
}));
}}
/>
</FormItem>
</Form.Item>
<FormItem label={t('field_channel_type')} labelAlign="left" name="type">
<Form.Item label={t('field_channel_type')} labelAlign="left" name="type">
<Select disabled={editing} onChange={onTypeChangeHandler} value={type}>
<Option value="slack" key="slack">
Slack
@ -83,11 +82,11 @@ function FormAlertChannels({
Pagerduty
</Option>
</Select>
</FormItem>
</Form.Item>
<FormItem>{renderSettings()}</FormItem>
<Form.Item>{renderSettings()}</Form.Item>
<FormItem>
<Form.Item>
<Button
disabled={savingState}
loading={savingState}
@ -110,7 +109,7 @@ function FormAlertChannels({
>
{t('button_return')}
</Button>
</FormItem>
</Form.Item>
</Form>
</>
);

View File

@ -20,12 +20,14 @@ function ChannelSelect({
const { loading, payload, error, errorMessage } = useFetch(getChannels);
const [notifications, NotificationElement] = notification.useNotification();
const handleChange = (value: string[]): void => {
onSelectChannels(value);
};
if (error && errorMessage !== '') {
notification.error({
notifications.error({
message: 'Error',
description: errorMessage,
});
@ -48,19 +50,22 @@ function ChannelSelect({
return children;
};
return (
<StyledSelect
status={error ? 'error' : ''}
mode="multiple"
style={{ width: '100%' }}
placeholder={t('placeholder_channel_select')}
value={currentValue}
onChange={(value): void => {
handleChange(value as string[]);
}}
optionLabelProp="label"
>
{renderOptions()}
</StyledSelect>
<>
{NotificationElement}
<StyledSelect
status={error ? 'error' : ''}
mode="multiple"
style={{ width: '100%' }}
placeholder={t('placeholder_channel_select')}
value={currentValue}
onChange={(value): void => {
handleChange(value as string[]);
}}
optionLabelProp="label"
>
{renderOptions()}
</StyledSelect>
</>
);
}

View File

@ -163,10 +163,11 @@ function QuerySection({
...allQueries,
});
};
const [notifications, NotificationElement] = notification.useNotification();
const addMetricQuery = useCallback(() => {
if (Object.keys(metricQueries).length > 5) {
notification.error({
notifications.error({
message: t('metric_query_max_limit'),
});
return;
@ -191,7 +192,7 @@ function QuerySection({
expression: queryLabel,
};
setMetricQueries({ ...queries });
}, [t, getNextQueryLabel, metricQueries, setMetricQueries]);
}, [t, getNextQueryLabel, metricQueries, setMetricQueries, notifications]);
const addFormula = useCallback(() => {
// defaulting to F1 as only one formula is supported
@ -350,6 +351,7 @@ function QuerySection({
};
return (
<>
{NotificationElement}
<StepHeading> {t('alert_form_step1')}</StepHeading>
<FormContainer>
<div style={{ display: 'flex' }}>{renderTabs(alertType)}</div>

View File

@ -1,5 +1,4 @@
import { Select, Typography } from 'antd';
import FormItem from 'antd/lib/form/FormItem';
import { Form, Select, Typography } from 'antd';
import React from 'react';
import { useTranslation } from 'react-i18next';
import {
@ -18,6 +17,7 @@ import {
} from './styles';
const { Option } = Select;
const FormItem = Form.Item;
function RuleOptions({
alertDef,

View File

@ -190,12 +190,14 @@ function FormAlertRules({
});
}
};
const [notifications, NotificationElement] = notification.useNotification();
const validatePromParams = useCallback((): boolean => {
let retval = true;
if (queryCategory !== EQueryType.PROM) return retval;
if (!promQueries || Object.keys(promQueries).length === 0) {
notification.error({
notifications.error({
message: 'Error',
description: t('promql_required'),
});
@ -204,7 +206,7 @@ function FormAlertRules({
Object.keys(promQueries).forEach((key) => {
if (promQueries[key].query === '') {
notification.error({
notifications.error({
message: 'Error',
description: t('promql_required'),
});
@ -213,14 +215,14 @@ function FormAlertRules({
});
return retval;
}, [t, promQueries, queryCategory]);
}, [t, promQueries, queryCategory, notifications]);
const validateChQueryParams = useCallback((): boolean => {
let retval = true;
if (queryCategory !== EQueryType.CLICKHOUSE) return retval;
if (!chQueries || Object.keys(chQueries).length === 0) {
notification.error({
notifications.error({
message: 'Error',
description: t('chquery_required'),
});
@ -229,7 +231,7 @@ function FormAlertRules({
Object.keys(chQueries).forEach((key) => {
if (chQueries[key].rawQuery === '') {
notification.error({
notifications.error({
message: 'Error',
description: t('chquery_required'),
});
@ -238,14 +240,14 @@ function FormAlertRules({
});
return retval;
}, [t, chQueries, queryCategory]);
}, [t, chQueries, queryCategory, notifications]);
const validateQBParams = useCallback((): boolean => {
let retval = true;
if (queryCategory !== EQueryType.QUERY_BUILDER) return true;
if (!metricQueries || Object.keys(metricQueries).length === 0) {
notification.error({
notifications.error({
message: 'Error',
description: t('condition_required'),
});
@ -253,7 +255,7 @@ function FormAlertRules({
}
if (!alertDef.condition?.target) {
notification.error({
notifications.error({
message: 'Error',
description: t('target_missing'),
});
@ -262,7 +264,7 @@ function FormAlertRules({
Object.keys(metricQueries).forEach((key) => {
if (metricQueries[key].metricName === '') {
notification.error({
notifications.error({
message: 'Error',
description: t('metricname_missing', { where: metricQueries[key].name }),
});
@ -272,7 +274,7 @@ function FormAlertRules({
Object.keys(formulaQueries).forEach((key) => {
if (formulaQueries[key].expression === '') {
notification.error({
notifications.error({
message: 'Error',
description: t('expression_missing', formulaQueries[key].name),
});
@ -280,11 +282,11 @@ function FormAlertRules({
}
});
return retval;
}, [t, alertDef, queryCategory, metricQueries, formulaQueries]);
}, [t, alertDef, queryCategory, metricQueries, formulaQueries, notifications]);
const isFormValid = useCallback((): boolean => {
if (!alertDef.alert || alertDef.alert === '') {
notification.error({
notifications.error({
message: 'Error',
description: t('alertname_required'),
});
@ -300,7 +302,14 @@ function FormAlertRules({
}
return validateQBParams();
}, [t, validateQBParams, validateChQueryParams, alertDef, validatePromParams]);
}, [
t,
validateQBParams,
validateChQueryParams,
alertDef,
validatePromParams,
notifications,
]);
const preparePostData = (): AlertDef => {
const postableAlert: AlertDef = {
@ -348,7 +357,7 @@ function FormAlertRules({
const response = await saveAlertApi(apiReq);
if (response.statusCode === 200) {
notification.success({
notifications.success({
message: 'Success',
description:
!ruleId || ruleId === 0 ? t('rule_created') : t('rule_edited'),
@ -361,19 +370,26 @@ function FormAlertRules({
history.replace(ROUTES.LIST_ALL_ALERT);
}, 2000);
} else {
notification.error({
notifications.error({
message: 'Error',
description: response.error || t('unexpected_error'),
});
}
} catch (e) {
notification.error({
notifications.error({
message: 'Error',
description: t('unexpected_error'),
});
}
setLoading(false);
}, [t, isFormValid, ruleId, ruleCache, memoizedPreparePostData]);
}, [
t,
isFormValid,
ruleId,
ruleCache,
memoizedPreparePostData,
notifications,
]);
const onSaveHandler = useCallback(async () => {
const content = (
@ -407,30 +423,30 @@ function FormAlertRules({
if (response.statusCode === 200) {
const { payload } = response;
if (payload?.alertCount === 0) {
notification.error({
notifications.error({
message: 'Error',
description: t('no_alerts_found'),
});
} else {
notification.success({
notifications.success({
message: 'Success',
description: t('rule_test_fired'),
});
}
} else {
notification.error({
notifications.error({
message: 'Error',
description: response.error || t('unexpected_error'),
});
}
} catch (e) {
notification.error({
notifications.error({
message: 'Error',
description: t('unexpected_error'),
});
}
setLoading(false);
}, [t, isFormValid, memoizedPreparePostData]);
}, [t, isFormValid, memoizedPreparePostData, notifications]);
const renderBasicInfo = (): JSX.Element => (
<BasicInfo alertDef={alertDef} setAlertDef={setAlertDef} />
@ -467,6 +483,7 @@ function FormAlertRules({
);
return (
<>
{NotificationElement}
{Element}
<PanelContainer>
<StyledLeftContainer flex="5 1 600px">

View File

@ -95,6 +95,8 @@ export const ThresholdInput = styled(InputNumber)`
align-items: center;
& > .ant-input-number-group-addon {
width: 130px;
border: 0;
background: transparent;
}
& > .ant-input-number {
width: 50%;

View File

@ -172,6 +172,8 @@ function GeneralSettings({
logsTtlValuesPayload.status === 'pending' ? 1000 : null,
);
const [notifications, NotificationElement] = notification.useNotification();
const onModalToggleHandler = (type: TTTLType): void => {
if (type === 'metrics') setModalMetrics((modal) => !modal);
if (type === 'traces') setModalTraces((modal) => !modal);
@ -186,14 +188,14 @@ function GeneralSettings({
const onClickSaveHandler = useCallback(
(type: TTTLType) => {
if (!setRetentionPermission) {
notification.error({
notifications.error({
message: `Sorry you don't have permission to make these changes`,
});
return;
}
onModalToggleHandler(type);
},
[setRetentionPermission],
[setRetentionPermission, notifications],
);
const s3Enabled = useMemo(
@ -352,7 +354,7 @@ function GeneralSettings({
let hasSetTTLFailed = false;
if (setTTLResponse.statusCode === 409) {
hasSetTTLFailed = true;
notification.error({
notifications.error({
message: 'Error',
description: t('retention_request_race_condition'),
placement: 'topRight',
@ -390,7 +392,7 @@ function GeneralSettings({
});
}
} catch (error) {
notification.error({
notifications.error({
message: 'Error',
description: t('retention_failed_message'),
placement: 'topRight',
@ -591,6 +593,7 @@ function GeneralSettings({
return (
<>
{NotificationElement}
{Element}
<Col xs={24} md={22} xl={20} xxl={18} style={{ margin: 'auto' }}>
<ErrorTextContainer>

View File

@ -43,6 +43,7 @@ function GridCardGraph({
const { ref: graphRef, inView: isGraphVisible } = useInView({
threshold: 0,
triggerOnce: true,
initialInView: true,
});
const [errorMessage, setErrorMessage] = useState<string | undefined>('');

View File

@ -204,6 +204,8 @@ function GridGraph(props: Props): JSX.Element {
[widgets, onDragSelect],
);
const [notifications, NotificationElement] = notification.useNotification();
const onEmptyWidgetHandler = useCallback(async () => {
try {
const id = 'empty';
@ -219,22 +221,25 @@ function GridGraph(props: Props): JSX.Element {
...(data.layout || []),
];
await UpdateDashboard({
data,
generateWidgetId: id,
graphType: 'EMPTY_WIDGET',
selectedDashboard,
layout,
isRedirected: false,
});
await UpdateDashboard(
{
data,
generateWidgetId: id,
graphType: 'EMPTY_WIDGET',
selectedDashboard,
layout,
isRedirected: false,
},
notifications,
);
setLayoutFunction(layout);
} catch (error) {
notification.error({
notifications.error({
message: error instanceof Error ? error.toString() : 'Something went wrong',
});
}
}, [data, selectedDashboard, setLayoutFunction]);
}, [data, selectedDashboard, setLayoutFunction, notifications]);
const onLayoutChangeHandler = async (layout: Layout[]): Promise<void> => {
setLayoutFunction(layout);
@ -255,7 +260,7 @@ function GridGraph(props: Props): JSX.Element {
toggleAddWidget(true);
})
.catch(() => {
notification.error(t('something_went_wrong'));
notifications.error(t('something_went_wrong'));
});
} else {
toggleAddWidget(true);
@ -263,26 +268,29 @@ function GridGraph(props: Props): JSX.Element {
}
} catch (error) {
if (typeof error === 'string') {
notification.error({
notifications.error({
message: error || t('something_went_wrong'),
});
}
}
}, [layouts, onEmptyWidgetHandler, t, toggleAddWidget]);
}, [layouts, onEmptyWidgetHandler, t, toggleAddWidget, notifications]);
return (
<GraphLayoutContainer
{...{
addPanelLoading,
layouts,
onAddPanelHandler,
onLayoutChangeHandler,
onLayoutSaveHandler,
saveLayoutState,
widgets,
setLayout,
}}
/>
<>
{NotificationElement}
<GraphLayoutContainer
{...{
addPanelLoading,
layouts,
onAddPanelHandler,
onLayoutChangeHandler,
onLayoutSaveHandler,
saveLayoutState,
widgets,
setLayout,
}}
/>
</>
);
}

View File

@ -1,4 +1,4 @@
import { notification } from 'antd';
import { NotificationInstance } from 'antd/es/notification/interface';
import updateDashboardApi from 'api/dashboard/update';
import {
ClickHouseQueryTemplate,
@ -12,14 +12,17 @@ import store from 'store';
import { Dashboard } from 'types/api/dashboard/getAll';
import { EQueryType } from 'types/common/dashboard';
export const UpdateDashboard = async ({
data,
graphType,
generateWidgetId,
layout,
selectedDashboard,
isRedirected,
}: UpdateDashboardProps): Promise<Dashboard | undefined> => {
export const UpdateDashboard = async (
{
data,
graphType,
generateWidgetId,
layout,
selectedDashboard,
isRedirected,
}: UpdateDashboardProps,
notify: NotificationInstance,
): Promise<Dashboard | undefined> => {
const updatedSelectedDashboard: Dashboard = {
...selectedDashboard,
data: {
@ -89,7 +92,7 @@ export const UpdateDashboard = async ({
if (response.statusCode === 200) {
return response.payload;
}
notification.error({
notify.error({
message: response.error || 'Something went wrong',
});
return undefined;

View File

@ -1,5 +1,4 @@
import { Button, Input, notification } from 'antd';
import FormItem from 'antd/lib/form/FormItem';
import { Button, Form, Input, notification } from 'antd';
import getFeaturesFlags from 'api/features/getFeatureFlags';
import apply from 'api/licenses/apply';
import React, { useState } from 'react';
@ -13,6 +12,8 @@ import { PayloadProps } from 'types/api/licenses/getAll';
import { ApplyForm, ApplyFormContainer, LicenseInput } from './styles';
const FormItem = Form.Item;
function ApplyLicenseForm({
licenseRefetch,
}: ApplyLicenseFormProps): JSX.Element {
@ -26,10 +27,12 @@ function ApplyLicenseForm({
enabled: false,
});
const [notifications, NotificationElement] = notification.useNotification();
const onFinish = async (values: unknown | { key: string }): Promise<void> => {
const params = values as { key: string };
if (params.key === '' || !params.key) {
notification.error({
notifications.error({
message: 'Error',
description: t('enter_license_key'),
});
@ -53,18 +56,18 @@ function ApplyLicenseForm({
payload: featureFlagsResponse.data.payload,
});
}
notification.success({
notifications.success({
message: 'Success',
description: t('license_applied'),
});
} else {
notification.error({
notifications.error({
message: 'Error',
description: response.error || t('unexpected_error'),
});
}
} catch (e) {
notification.error({
notifications.error({
message: 'Error',
description: t('unexpected_error'),
});
@ -74,6 +77,7 @@ function ApplyLicenseForm({
return (
<ApplyFormContainer>
{NotificationElement}
<ApplyForm layout="inline" onFinish={onFinish}>
<LicenseInput labelAlign="left" name="key">
<Input

View File

@ -1,5 +1,5 @@
import { Table } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import { ResizeTable } from 'components/ResizeTable';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { License } from 'types/api/licenses/def';
@ -13,25 +13,29 @@ function ListLicenses({ licenses }: ListLicensesProps): JSX.Element {
title: t('column_license_status'),
dataIndex: 'status',
key: 'status',
width: 100,
},
{
title: t('column_license_key'),
dataIndex: 'key',
key: 'key',
width: 80,
},
{
title: t('column_valid_from'),
dataIndex: 'ValidFrom',
key: 'valid from',
width: 80,
},
{
title: t('column_valid_until'),
dataIndex: 'ValidUntil',
key: 'valid until',
width: 80,
},
];
return <Table rowKey="id" dataSource={licenses} columns={columns} />;
return <ResizeTable columns={columns} rowKey="id" dataSource={licenses} />;
}
interface ListLicensesProps {

View File

@ -1,5 +1,4 @@
import { Form } from 'antd';
import FormItem from 'antd/lib/form/FormItem';
import styled from 'styled-components';
export const ApplyFormContainer = styled.div`
@ -15,7 +14,7 @@ export const ApplyForm = styled(Form)`
}
`;
export const LicenseInput = styled(FormItem)`
export const LicenseInput = styled(Form.Item)`
width: 200px;
&:focus {
width: 350px;

View File

@ -1,7 +1,8 @@
/* eslint-disable react/display-name */
import { PlusOutlined } from '@ant-design/icons';
import { notification, Table, Typography } from 'antd';
import { notification, Typography } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import { ResizeTable } from 'components/ResizeTable';
import TextToolTip from 'components/TextToolTip';
import ROUTES from 'constants/routes';
import useComponentPermission from 'hooks/useComponentPermission';
@ -30,6 +31,8 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
role,
);
const [notificationsApi, NotificationElement] = notification.useNotification();
useInterval(() => {
(async (): Promise<void> => {
const { data: refetchData, status } = await refetch();
@ -37,7 +40,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
setData(refetchData?.payload || []);
}
if (status === 'error') {
notification.error({
notificationsApi.error({
message: t('something_went_wrong'),
});
}
@ -58,6 +61,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
{
title: 'Status',
dataIndex: 'state',
width: 80,
key: 'state',
sorter: (a, b): number =>
(b.state ? b.state.charCodeAt(0) : 1000) -
@ -67,6 +71,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
{
title: 'Alert Name',
dataIndex: 'alert',
width: 100,
key: 'name',
sorter: (a, b): number =>
(a.alert ? a.alert.charCodeAt(0) : 1000) -
@ -82,6 +87,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
{
title: 'Severity',
dataIndex: 'labels',
width: 80,
key: 'severity',
sorter: (a, b): number =>
(a.labels ? a.labels.severity.length : 0) -
@ -99,7 +105,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
dataIndex: 'labels',
key: 'tags',
align: 'center',
width: 350,
width: 100,
render: (value): JSX.Element => {
const objectKeys = Object.keys(value);
const withOutSeverityKeys = objectKeys.filter((e) => e !== 'severity');
@ -126,6 +132,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
title: 'Action',
dataIndex: 'id',
key: 'action',
width: 120,
render: (id: GettableAlert['id'], record): JSX.Element => (
<>
<ToggleAlertState disabled={record.disabled} setData={setData} id={id} />
@ -145,6 +152,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
return (
<>
{NotificationElement}
{Element}
<ButtonContainer>
@ -161,8 +169,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
</Button>
)}
</ButtonContainer>
<Table rowKey="id" columns={columns} dataSource={data} />
<ResizeTable columns={columns} rowKey="id" dataSource={data} />
</>
);
}

View File

@ -20,6 +20,8 @@ function ToggleAlertState({
payload: undefined,
});
const [notifications, NotificationElement] = notification.useNotification();
const defaultErrorMessage = 'Something went wrong';
const onToggleHandler = async (
@ -58,7 +60,7 @@ function ToggleAlertState({
loading: false,
payload: response.payload,
}));
notification.success({
notifications.success({
message: 'Success',
});
} else {
@ -69,7 +71,7 @@ function ToggleAlertState({
errorMessage: response.error || defaultErrorMessage,
}));
notification.error({
notifications.error({
message: response.error || defaultErrorMessage,
});
}
@ -81,21 +83,24 @@ function ToggleAlertState({
errorMessage: defaultErrorMessage,
}));
notification.error({
notifications.error({
message: defaultErrorMessage,
});
}
};
return (
<ColumnButton
disabled={apiStatus.loading || false}
loading={apiStatus.loading || false}
onClick={(): Promise<void> => onToggleHandler(id, !disabled)}
type="link"
>
{disabled ? 'Enable' : 'Disable'}
</ColumnButton>
<>
{NotificationElement}
<ColumnButton
disabled={apiStatus.loading || false}
loading={apiStatus.loading || false}
onClick={(): Promise<void> => onToggleHandler(id, !disabled)}
type="link"
>
{disabled ? 'Enable' : 'Disable'}
</ColumnButton>
</>
);
}

View File

@ -17,28 +17,38 @@ function ListAlertRules(): JSX.Element {
cacheTime: 0,
});
const [notifications, NotificationElement] = notification.useNotification();
useEffect(() => {
if (status === 'error' || (status === 'success' && data.statusCode >= 400)) {
notification.error({
notifications.error({
message: data?.error || t('something_went_wrong'),
});
}
}, [data?.error, data?.statusCode, status, t]);
}, [data?.error, data?.statusCode, status, t, notifications]);
// api failed to load the data
if (isError) {
return <div>{data?.error || t('something_went_wrong')}</div>;
return (
<div>
{NotificationElement}
{data?.error || t('something_went_wrong')}
</div>
);
}
// api is successful but error is present
if (status === 'success' && data.statusCode >= 400) {
return (
<ListAlert
{...{
allAlertRules: [],
refetch,
}}
/>
<>
{NotificationElement}
<ListAlert
{...{
allAlertRules: [],
refetch,
}}
/>
</>
);
}
@ -49,6 +59,7 @@ function ListAlertRules(): JSX.Element {
return (
<Space direction="vertical" size="large" style={{ width: '100%' }}>
{NotificationElement}
<ReleaseNote path={location.pathname} />
<ListAlert
{...{

View File

@ -41,6 +41,8 @@ function ImportJSON({
const [editorValue, setEditorValue] = useState<string>('');
const [notifications, NotificationElement] = notification.useNotification();
const onChangeHandler: UploadProps['onChange'] = (info) => {
const { fileList } = info;
const reader = new FileReader();
@ -106,7 +108,7 @@ function ImportJSON({
}, 10);
} else {
setIsCreateDashboardError(true);
notification.error({
notifications.error({
message:
response.error ||
t('something_went_wrong', {
@ -130,58 +132,61 @@ function ImportJSON({
);
return (
<Modal
open={isImportJSONModalVisible}
centered
maskClosable
destroyOnClose
width="70vw"
onCancel={onModalHandler}
title={
<>
<Typography.Title level={4}>{t('import_json')}</Typography.Title>
<Typography>{t('import_dashboard_by_pasting')}</Typography>
</>
}
footer={
<FooterContainer>
<Button
disabled={editorValue.length === 0}
onClick={onClickLoadJsonHandler}
loading={dashboardCreating}
>
{t('load_json')}
</Button>
{isCreateDashboardError && getErrorNode(t('error_loading_json'))}
</FooterContainer>
}
>
<div>
<Space direction="horizontal">
<Upload
accept=".json"
showUploadList={false}
multiple={false}
onChange={onChangeHandler}
beforeUpload={(): boolean => false}
action="none"
data={jsonData}
>
<Button type="primary">{t('upload_json_file')}</Button>
</Upload>
{isUploadJSONError && <>{getErrorNode(t('error_upload_json'))}</>}
</Space>
<>
{NotificationElement}
<Modal
open={isImportJSONModalVisible}
centered
maskClosable
destroyOnClose
width="70vw"
onCancel={onModalHandler}
title={
<>
<Typography.Title level={4}>{t('import_json')}</Typography.Title>
<Typography>{t('import_dashboard_by_pasting')}</Typography>
</>
}
footer={
<FooterContainer>
<Button
disabled={editorValue.length === 0}
onClick={onClickLoadJsonHandler}
loading={dashboardCreating}
>
{t('load_json')}
</Button>
{isCreateDashboardError && getErrorNode(t('error_loading_json'))}
</FooterContainer>
}
>
<div>
<Space direction="horizontal">
<Upload
accept=".json"
showUploadList={false}
multiple={false}
onChange={onChangeHandler}
beforeUpload={(): boolean => false}
action="none"
data={jsonData}
>
<Button type="primary">{t('upload_json_file')}</Button>
</Upload>
{isUploadJSONError && <>{getErrorNode(t('error_upload_json'))}</>}
</Space>
<EditorContainer>
<Typography.Paragraph>{t('paste_json_below')}</Typography.Paragraph>
<Editor
onChange={(newValue): void => setEditorValue(newValue)}
value={editorValue}
language="json"
/>
</EditorContainer>
</div>
</Modal>
<EditorContainer>
<Typography.Paragraph>{t('paste_json_below')}</Typography.Paragraph>
<Editor
onChange={(newValue): void => setEditorValue(newValue)}
value={editorValue}
language="json"
/>
</EditorContainer>
</div>
</Modal>
</>
);
}

View File

@ -1,15 +1,8 @@
import { PlusOutlined } from '@ant-design/icons';
import {
Card,
Dropdown,
Menu,
Row,
Table,
TableColumnProps,
Typography,
} from 'antd';
import { Card, Dropdown, Menu, Row, TableColumnProps, Typography } from 'antd';
import createDashboard from 'api/dashboard/create';
import { AxiosError } from 'axios';
import { ResizeTable } from 'components/ResizeTable';
import TextToolTip from 'components/TextToolTip';
import ROUTES from 'constants/routes';
import SearchFilter from 'container/ListOfDashboard/SearchFilter';
@ -74,20 +67,24 @@ function ListOfAllDashboard(): JSX.Element {
{
title: 'Name',
dataIndex: 'name',
width: 100,
render: Name,
},
{
title: 'Description',
width: 100,
dataIndex: 'description',
},
{
title: 'Tags (can be multiple)',
dataIndex: 'tags',
width: 80,
render: Tags,
},
{
title: 'Created At',
dataIndex: 'createdBy',
width: 80,
sorter: (a: Data, b: Data): number => {
const prev = new Date(a.createdBy).getTime();
const next = new Date(b.createdBy).getTime();
@ -98,6 +95,7 @@ function ListOfAllDashboard(): JSX.Element {
},
{
title: 'Last Updated Time',
width: 90,
dataIndex: 'lastUpdatedTime',
sorter: (a: Data, b: Data): number => {
const prev = new Date(a.lastUpdatedTime).getTime();
@ -114,6 +112,7 @@ function ListOfAllDashboard(): JSX.Element {
title: 'Action',
dataIndex: '',
key: 'x',
width: 40,
render: DeleteButton,
});
}
@ -271,7 +270,8 @@ function ListOfAllDashboard(): JSX.Element {
uploadedGrafana={uploadedGrafana}
onModalHandler={(): void => onModalHandler(false)}
/>
<Table
<ResizeTable
columns={columns}
pagination={{
pageSize: 9,
defaultPageSize: 9,
@ -280,7 +280,6 @@ function ListOfAllDashboard(): JSX.Element {
bordered
sticky
loading={loading}
columns={columns}
dataSource={data}
showSorterTooltip
/>

View File

@ -4,6 +4,8 @@ import {
RightOutlined,
} from '@ant-design/icons';
import { Button, Divider, Select } from 'antd';
import { getGlobalTime } from 'container/LogsSearchFilter/utils';
import { getMinMax } from 'container/TopNav/AutoRefresh/config';
import React, { memo, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
@ -13,6 +15,7 @@ import {
RESET_ID_START_AND_END,
SET_LOG_LINES_PER_PAGE,
} from 'types/actions/logs';
import { GlobalReducer } from 'types/reducer/globalTime';
import { ILogsReducer } from 'types/reducer/logs';
import { ITEMS_PER_PAGE_OPTIONS } from './config';
@ -28,18 +31,33 @@ function LogControls(): JSX.Element | null {
isLoadingAggregate,
logs,
} = useSelector<AppState, ILogsReducer>((state) => state.logs);
const globalTime = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const dispatch = useDispatch();
const handleLogLinesPerPageChange = (e: number): void => {
dispatch({
type: SET_LOG_LINES_PER_PAGE,
payload: e,
payload: {
logLinesPerPage: e,
},
});
};
const handleGoToLatest = (): void => {
const { maxTime, minTime } = getMinMax(
globalTime.selectedTime,
globalTime.minTime,
globalTime.maxTime,
);
dispatch({
type: RESET_ID_START_AND_END,
payload: getGlobalTime(globalTime.selectedTime, {
maxTime,
minTime,
}),
});
};

View File

@ -4,7 +4,7 @@ import getStep from 'lib/getStep';
import { generateFilterQuery } from 'lib/logs/generateFilterQuery';
import React, { memo, useMemo } from 'react';
import { connect, useDispatch, useSelector } from 'react-redux';
import { bindActionCreators } from 'redux';
import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import { getLogs } from 'store/actions/logs/getLogs';
import { getLogsAggregate } from 'store/actions/logs/getLogsAggregate';
@ -46,7 +46,7 @@ function ActionItem({
liveTail,
idEnd,
} = useSelector<AppState, ILogsReducer>((store) => store.logs);
const dispatch = useDispatch();
const dispatch = useDispatch<Dispatch<AppActions>>();
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
@ -62,7 +62,9 @@ function ActionItem({
}
dispatch({
type: SET_SEARCH_QUERY_STRING,
payload: updatedQueryString,
payload: {
searchQueryString: updatedQueryString,
},
});
if (liveTail === 'STOPPED') {

View File

@ -1,7 +1,8 @@
import { blue, orange } from '@ant-design/colors';
import { Input, Table } from 'antd';
import { Input } from 'antd';
import AddToQueryHOC from 'components/Logs/AddToQueryHOC';
import CopyClipboardHOC from 'components/Logs/CopyClipboardHOC';
import { ResizeTable } from 'components/ResizeTable';
import flatten from 'flat';
import { fieldSearchFilter } from 'lib/logs/fieldSearch';
import React, { useMemo, useState } from 'react';
@ -56,7 +57,7 @@ function TableView({ logData }: TableViewProps): JSX.Element | null {
title: 'Field',
dataIndex: 'field',
key: 'field',
width: '35%',
width: 100,
render: (field: string): JSX.Element => {
const fieldKey = field.split('.').slice(-1);
const renderedField = <span style={{ color: blue[4] }}>{field}</span>;
@ -64,7 +65,6 @@ function TableView({ logData }: TableViewProps): JSX.Element | null {
if (!RESTRICTED_FIELDS.includes(fieldKey[0])) {
return (
<AddToQueryHOC fieldKey={fieldKey[0]} fieldValue={flattenLogData[field]}>
{' '}
{renderedField}
</AddToQueryHOC>
);
@ -76,15 +76,16 @@ function TableView({ logData }: TableViewProps): JSX.Element | null {
title: 'Value',
dataIndex: 'value',
key: 'value',
width: 80,
ellipsis: false,
render: (field: never): JSX.Element => (
<CopyClipboardHOC textToCopy={field}>
<span style={{ color: orange[6] }}>{field}</span>
</CopyClipboardHOC>
),
width: '60%',
},
];
return (
<div style={{ position: 'relative' }}>
<Input
@ -93,11 +94,10 @@ function TableView({ logData }: TableViewProps): JSX.Element | null {
value={fieldSearchInput}
onChange={(e): void => setFieldSearchInput(e.target.value)}
/>
<Table
// scroll={{ x: true }}
<ResizeTable
columns={columns as never}
tableLayout="fixed"
dataSource={dataSource}
columns={columns as never}
pagination={false}
/>
</div>

View File

@ -42,6 +42,8 @@ function Login({
const [precheckInProcess, setPrecheckInProcess] = useState(false);
const [precheckComplete, setPrecheckComplete] = useState(false);
const [notifications, NotificationElement] = notification.useNotification();
useEffect(() => {
if (withPassword === 'Y') {
setPrecheckComplete(true);
@ -62,15 +64,15 @@ function Login({
useEffect(() => {
if (ssoerror !== '') {
notification.error({
notifications.error({
message: t('failed_to_login'),
});
}
}, [ssoerror, t]);
}, [ssoerror, t, notifications]);
const onNextHandler = async (): Promise<void> => {
if (!email) {
notification.error({
notifications.error({
message: t('invalid_email'),
});
return;
@ -88,18 +90,18 @@ function Login({
if (isUser) {
setPrecheckComplete(true);
} else {
notification.error({
notifications.error({
message: t('invalid_account'),
});
}
} else {
notification.error({
notifications.error({
message: t('invalid_config'),
});
}
} catch (e) {
console.log('failed to call precheck Api', e);
notification.error({ message: t('unexpected_error') });
notifications.error({ message: t('unexpected_error') });
}
setPrecheckInProcess(false);
};
@ -144,14 +146,14 @@ function Login({
);
history.push(ROUTES.APPLICATION);
} else {
notification.error({
notifications.error({
message: response.error || t('unexpected_error'),
});
}
setIsLoading(false);
} catch (error) {
setIsLoading(false);
notification.error({
notifications.error({
message: t('unexpected_error'),
});
}
@ -183,6 +185,7 @@ function Login({
return (
<FormWrapper>
{NotificationElement}
<FormContainer onSubmit={onSubmitHandler}>
<Title level={4}>{t('login_page_title')}</Title>
<ParentContainer>

View File

@ -1,20 +1,17 @@
import { LoadingOutlined } from '@ant-design/icons';
import { Button, Popover, Spin } from 'antd';
import { Button, Popover, Spin, Typography } from 'antd';
import { useIsDarkMode } from 'hooks/useDarkMode';
import React, { useState } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import {
IField,
IInterestingFields,
ISelectedFields,
} from 'types/api/logs/fields';
import { ICON_STYLE } from './config';
import { Field } from './styles';
interface FieldItemProps {
name: string;
buttonIcon: React.ReactNode;
buttonOnClick: (arg0: Record<string, unknown>) => void;
fieldData: Record<string, never>;
fieldIndex: number;
isLoading: boolean;
iconHoverText: string;
}
export function FieldItem({
function FieldItem({
name,
buttonIcon,
buttonOnClick,
@ -23,33 +20,65 @@ export function FieldItem({
isLoading,
iconHoverText,
}: FieldItemProps): JSX.Element {
const [isHovered, setIsHovered] = useState(false);
const [isHovered, setIsHovered] = useState<boolean>(false);
const isDarkMode = useIsDarkMode();
const onClickHandler = useCallback(() => {
if (!isLoading && buttonOnClick) buttonOnClick({ fieldData, fieldIndex });
}, [buttonOnClick, fieldData, fieldIndex, isLoading]);
const renderContent = useMemo(() => {
if (isLoading) {
return <Spin spinning size="small" indicator={<LoadingOutlined spin />} />;
}
if (isHovered) {
return (
<Popover content={<Typography>{iconHoverText}</Typography>}>
<Button
size="small"
type="text"
icon={buttonIcon}
onClick={onClickHandler}
/>
</Popover>
);
}
return null;
}, [buttonIcon, iconHoverText, isHovered, isLoading, onClickHandler]);
const onMouseHoverHandler = useCallback(
(value: boolean) => (): void => {
setIsHovered(value);
},
[],
);
return (
<Field
onMouseEnter={(): void => {
setIsHovered(true);
}}
onMouseLeave={(): void => setIsHovered(false)}
onMouseEnter={onMouseHoverHandler(true)}
onMouseLeave={onMouseHoverHandler(false)}
isDarkMode={isDarkMode}
>
<span>{name}</span>
{isLoading ? (
<Spin spinning size="small" indicator={<LoadingOutlined spin />} />
) : (
isHovered &&
buttonOnClick && (
<Popover content={<span>{iconHoverText}</span>}>
<Button
type="text"
size="small"
icon={buttonIcon}
onClick={(): void => buttonOnClick({ fieldData, fieldIndex })}
style={{ color: 'inherit', padding: 0, height: '1rem', width: '1rem' }}
/>
</Popover>
)
)}
<Typography style={ICON_STYLE.PLUS}>{name}</Typography>
{renderContent}
</Field>
);
}
interface FieldItemProps {
name: string;
buttonIcon: React.ReactNode;
buttonOnClick: (props: {
fieldData: IInterestingFields | ISelectedFields;
fieldIndex: number;
}) => void;
fieldData: IField;
fieldIndex: number;
isLoading: boolean;
iconHoverText: string;
}
export default FieldItem;

View File

@ -0,0 +1,8 @@
import { blue, red } from '@ant-design/colors';
export const RESTRICTED_SELECTED_FIELDS = ['timestamp', 'id'];
export const ICON_STYLE = {
PLUS: { color: blue[5] },
CLOSE: { color: red[5] },
};

View File

@ -1,27 +1,19 @@
/* eslint-disable react/no-array-index-key */
import { red } from '@ant-design/colors';
import { CloseOutlined, PlusCircleFilled } from '@ant-design/icons';
import { Input } from 'antd';
import AddToSelectedFields from 'api/logs/AddToSelectedField';
import RemoveSelectedField from 'api/logs/RemoveFromSelectedField';
import CategoryHeading from 'components/Logs/CategoryHeading';
import { fieldSearchFilter } from 'lib/logs/fieldSearch';
import React, { memo, useState } from 'react';
import { connect, useSelector } from 'react-redux';
import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import { GetLogsFields } from 'store/actions/logs/getFields';
import React, { useCallback, useState } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import { IInterestingFields, ISelectedFields } from 'types/api/logs/fields';
import { ILogsReducer } from 'types/reducer/logs';
import { FieldItem } from './FieldItem';
import { ICON_STYLE } from './config';
import FieldItem from './FieldItem';
import { CategoryContainer, Container, FieldContainer } from './styles';
import { IHandleInterestProps, IHandleRemoveInterestProps } from './types';
import { onHandleAddInterest, onHandleRemoveInterest } from './utils';
const RESTRICTED_SELECTED_FIELDS = ['timestamp', 'id'];
function LogsFilters({ getLogsFields }: LogsFiltersProps): JSX.Element {
function LogsFilters(): JSX.Element {
const {
fields: { interesting, selected },
} = useSelector<AppState, ILogsReducer>((state) => state.logs);
@ -36,57 +28,40 @@ function LogsFilters({ getLogsFields }: LogsFiltersProps): JSX.Element {
setFilterValuesInput((e.target as HTMLInputElement).value);
};
const handleAddInterestingToSelected = async ({
fieldData,
fieldIndex,
}: {
fieldData: IInterestingFields;
fieldIndex: number;
}): Promise<void> => {
setInterestingFieldLoading((prevState: number[]) => {
prevState.push(fieldIndex);
return [...prevState];
});
const onHandleAddSelectedToInteresting = useCallback(
({ fieldData, fieldIndex }: IHandleInterestProps) => (): Promise<void> =>
onHandleAddInterest({
fieldData,
fieldIndex,
interesting,
interestingFieldLoading,
setInterestingFieldLoading,
selected,
}),
[interesting, interestingFieldLoading, selected],
);
await AddToSelectedFields({
...fieldData,
selected: true,
});
getLogsFields();
const onHandleRemoveSelected = useCallback(
({
fieldData,
fieldIndex,
}: IHandleRemoveInterestProps) => (): Promise<void> =>
onHandleRemoveInterest({
fieldData,
fieldIndex,
interesting,
interestingFieldLoading,
selected,
setSelectedFieldLoading,
}),
[interesting, interestingFieldLoading, selected, setSelectedFieldLoading],
);
setInterestingFieldLoading(
interestingFieldLoading.filter((e) => e !== fieldIndex),
);
};
const handleRemoveSelectedField = async ({
fieldData,
fieldIndex,
}: {
fieldData: ISelectedFields;
fieldIndex: number;
}): Promise<void> => {
setSelectedFieldLoading((prevState) => {
prevState.push(fieldIndex);
return [...prevState];
});
await RemoveSelectedField({
...fieldData,
selected: false,
});
getLogsFields();
setSelectedFieldLoading(
interestingFieldLoading.filter((e) => e !== fieldIndex),
);
};
return (
<Container flex="450px">
<Input
placeholder="Filter Values"
onInput={handleSearch}
style={{ width: '100%' }}
value={filterValuesInput}
onChange={handleSearch}
/>
@ -98,15 +73,15 @@ function LogsFilters({ getLogsFields }: LogsFiltersProps): JSX.Element {
.filter((field) => fieldSearchFilter(field.name, filterValuesInput))
.map((field, idx) => (
<FieldItem
key={`${JSON.stringify(field)}-${idx}`}
key={`${JSON.stringify(field)}`}
name={field.name}
fieldData={field as never}
fieldData={field}
fieldIndex={idx}
buttonIcon={<CloseOutlined style={{ color: red[5] }} />}
buttonOnClick={
(!RESTRICTED_SELECTED_FIELDS.includes(field.name) &&
handleRemoveSelectedField) as never
}
buttonIcon={<CloseOutlined style={ICON_STYLE.CLOSE} />}
buttonOnClick={onHandleRemoveSelected({
fieldData: field,
fieldIndex: idx,
})}
isLoading={selectedFieldLoading.includes(idx)}
iconHoverText="Remove from Selected Fields"
/>
@ -120,33 +95,23 @@ function LogsFilters({ getLogsFields }: LogsFiltersProps): JSX.Element {
.filter((field) => fieldSearchFilter(field.name, filterValuesInput))
.map((field, idx) => (
<FieldItem
key={`${JSON.stringify(field)}-${idx}`}
key={`${JSON.stringify(field)}`}
name={field.name}
fieldData={field as never}
fieldData={field}
fieldIndex={idx}
buttonIcon={<PlusCircleFilled />}
buttonOnClick={handleAddInterestingToSelected as never}
buttonIcon={<PlusCircleFilled style={ICON_STYLE.PLUS} />}
buttonOnClick={onHandleAddSelectedToInteresting({
fieldData: field,
fieldIndex: idx,
})}
isLoading={interestingFieldLoading.includes(idx)}
iconHoverText="Add to Selected Fields"
/>
))}
</FieldContainer>
</CategoryContainer>
{/* <ExtractField>Extract Fields</ExtractField> */}
</Container>
);
}
interface DispatchProps {
getLogsFields: () => (dispatch: Dispatch<AppActions>) => void;
}
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
getLogsFields: bindActionCreators(GetLogsFields, dispatch),
});
type LogsFiltersProps = DispatchProps;
export default connect(null, mapDispatchToProps)(memo(LogsFilters));
export default LogsFilters;

View File

@ -4,8 +4,8 @@ import styled from 'styled-components';
export const Container = styled(Col)`
padding-top: 0.3rem;
min-width: 250px;
max-width: 350px;
min-width: 15.625rem;
max-width: 21.875rem;
`;
export const CategoryContainer = styled.div`
@ -21,6 +21,7 @@ export const FieldContainer = styled(Typography.Text)`
export const Field = styled.div<{ isDarkMode: boolean }>`
border-radius: 0.5rem;
padding: 0.3rem 0.5rem;
height: 2rem;
display: flex;
justify-content: space-between;
align-items: center;

View File

@ -0,0 +1,35 @@
import {
IField,
IInterestingFields,
ISelectedFields,
} from 'types/api/logs/fields';
type SetLoading = (value: React.SetStateAction<number[]>) => void;
export type IHandleInterestProps = {
fieldData: IInterestingFields;
fieldIndex: number;
};
export type IHandleRemoveInterestProps = {
fieldData: ISelectedFields;
fieldIndex: number;
};
export interface OnHandleAddInterestProps {
setInterestingFieldLoading: SetLoading;
fieldIndex: number;
fieldData: ISelectedFields;
interesting: IField[];
interestingFieldLoading: number[];
selected: IField[];
}
export interface OnHandleRemoveInterestProps {
setSelectedFieldLoading: SetLoading;
selected: IField[];
interesting: IField[];
interestingFieldLoading: number[];
fieldData: IInterestingFields;
fieldIndex: number;
}

View File

@ -0,0 +1,94 @@
import AddToSelectedFields from 'api/logs/AddToSelectedField';
import RemoveSelectedField from 'api/logs/RemoveFromSelectedField';
import store from 'store';
import {
UPDATE_INTERESTING_FIELDS,
UPDATE_SELECTED_FIELDS,
} from 'types/actions/logs';
import { RESTRICTED_SELECTED_FIELDS } from './config';
import { OnHandleAddInterestProps, OnHandleRemoveInterestProps } from './types';
export const onHandleAddInterest = async ({
setInterestingFieldLoading,
fieldIndex,
fieldData,
interesting,
interestingFieldLoading,
selected,
}: OnHandleAddInterestProps): Promise<void> => {
const { dispatch } = store;
setInterestingFieldLoading((prevState: number[]) => {
prevState.push(fieldIndex);
return [...prevState];
});
await AddToSelectedFields({
...fieldData,
selected: true,
});
dispatch({
type: UPDATE_INTERESTING_FIELDS,
payload: {
field: interesting.filter((e) => e.name !== fieldData.name),
type: 'selected',
},
});
dispatch({
type: UPDATE_SELECTED_FIELDS,
payload: {
field: [...selected, fieldData],
type: 'selected',
},
});
setInterestingFieldLoading(
interestingFieldLoading.filter((e) => e !== fieldIndex),
);
};
export const onHandleRemoveInterest = async ({
setSelectedFieldLoading,
selected,
interesting,
interestingFieldLoading,
fieldData,
fieldIndex,
}: OnHandleRemoveInterestProps): Promise<void> => {
if (RESTRICTED_SELECTED_FIELDS.includes(fieldData.name)) return;
const { dispatch } = store;
setSelectedFieldLoading((prevState) => {
prevState.push(fieldIndex);
return [...prevState];
});
await RemoveSelectedField({
...fieldData,
selected: false,
});
dispatch({
type: UPDATE_SELECTED_FIELDS,
payload: {
field: selected.filter((e) => e.name !== fieldData.name),
type: 'selected',
},
});
dispatch({
type: UPDATE_INTERESTING_FIELDS,
payload: {
field: [...interesting, fieldData],
type: 'interesting',
},
});
setSelectedFieldLoading(
interestingFieldLoading.filter((e) => e !== fieldIndex),
);
};

View File

@ -1,9 +1,3 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable no-bitwise */
/* eslint-disable sonarjs/no-identical-functions */
/* eslint-disable no-param-reassign */
/* eslint-disable react/no-array-index-key */
/* eslint-disable react-hooks/exhaustive-deps */
import { CloseOutlined, CloseSquareOutlined } from '@ant-design/icons';
import { Button, Input, Select } from 'antd';
import CategoryHeading from 'components/Logs/CategoryHeading';
@ -12,7 +6,7 @@ import {
QueryOperatorsMultiVal,
QueryOperatorsSingleVal,
} from 'lib/logql/tokens';
import React, { useMemo } from 'react';
import React, { useCallback, useMemo } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { ILogsReducer } from 'types/reducer/logs';
@ -52,7 +46,7 @@ function QueryConditionField({
interface QueryFieldProps {
query: Query;
queryIndex: number;
onUpdate: (query: unknown, queryIndex: number) => void;
onUpdate: (query: Query, queryIndex: number) => void;
onDelete: (queryIndex: number) => void;
}
function QueryField({
@ -64,34 +58,39 @@ function QueryField({
const {
fields: { selected },
} = useSelector<AppState, ILogsReducer>((store) => store.logs);
const getFieldType = (inputKey: string): string => {
// eslint-disable-next-line no-restricted-syntax
for (const selectedField of selected) {
if (inputKey === selectedField.name) {
const getFieldType = useCallback(
(inputKey: string): string => {
const selectedField = selected.find((field) => inputKey === field.name);
if (selectedField) {
return selectedField.type;
}
}
return '';
};
return '';
},
[selected],
);
const fieldType = useMemo(() => getFieldType(query[0].value as string), [
getFieldType,
query,
]);
const handleChange = (qIdx: number, value: string): void => {
query[qIdx].value = value || '';
const updatedQuery = [...query];
updatedQuery[qIdx].value = value || '';
if (qIdx === 1) {
if (Object.values(QueryOperatorsMultiVal).includes(value)) {
if (!Array.isArray(query[2].value)) {
query[2].value = [];
if (!Array.isArray(updatedQuery[2].value)) {
updatedQuery[2].value = [];
}
} else if (
Object.values(QueryOperatorsSingleVal).includes(value) &&
Array.isArray(query[2].value)
Array.isArray(updatedQuery[2].value)
) {
query[2].value = '';
updatedQuery[2].value = '';
}
}
onUpdate(query, queryIndex);
onUpdate(updatedQuery, queryIndex);
};
const handleClear = (): void => {
@ -210,16 +209,16 @@ function QueryBuilder({
if (Array.isArray(query) && query.length > 1) {
result.push(
<QueryField
key={keyPrefix + idx}
query={query as never}
key={keyPrefix}
query={query}
queryIndex={idx}
onUpdate={handleUpdate as never}
onUpdate={handleUpdate}
onDelete={handleDelete}
/>,
);
} else {
result.push(
<div key={keyPrefix + idx}>
<div key={keyPrefix}>
<QueryConditionField
query={Array.isArray(query) ? query[0] : query}
queryIndex={idx}

View File

@ -36,6 +36,8 @@ function SearchFields({
const keyPrefixRef = useRef(hashCode(JSON.stringify(fieldsQuery)));
const [notifications, NotificationElement] = notification.useNotification();
useEffect(() => {
const updatedFieldsQuery = createParsedQueryStructure([
...parsedQuery,
@ -81,7 +83,7 @@ function SearchFields({
const flatParsedQuery = flatten(fieldsQuery);
if (!fieldsQueryIsvalid(flatParsedQuery)) {
notification.error({
notifications.error({
message: 'Please enter a valid criteria for each of the selected fields',
});
return;
@ -90,7 +92,7 @@ function SearchFields({
keyPrefixRef.current = hashCode(JSON.stringify(flatParsedQuery));
updateParsedQuery(flatParsedQuery);
onDropDownToggleHandler(false)();
}, [onDropDownToggleHandler, fieldsQuery, updateParsedQuery]);
}, [onDropDownToggleHandler, fieldsQuery, updateParsedQuery, notifications]);
const clearFilters = useCallback((): void => {
keyPrefixRef.current = hashCode(JSON.stringify([]));
@ -100,6 +102,7 @@ function SearchFields({
return (
<>
{NotificationElement}
<QueryBuilder
key={keyPrefixRef.current}
keyPrefix={keyPrefixRef.current}

View File

@ -1,8 +1,9 @@
import { Input, InputRef, Popover } from 'antd';
import useUrlQuery from 'hooks/useUrlQuery';
import getStep from 'lib/getStep';
import { debounce } from 'lodash-es';
import debounce from 'lodash-es/debounce';
import React, {
memo,
useCallback,
useEffect,
useMemo,
@ -12,6 +13,7 @@ import React, {
import { connect, useDispatch, useSelector } from 'react-redux';
import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import { GetLogsFields } from 'store/actions/logs/getFields';
import { getLogs } from 'store/actions/logs/getLogs';
import { getLogsAggregate } from 'store/actions/logs/getLogsAggregate';
import { AppState } from 'store/reducers';
@ -32,6 +34,7 @@ import { useSearchParser } from './useSearchParser';
function SearchFilter({
getLogs,
getLogsAggregate,
getLogsFields,
}: SearchFilterProps): JSX.Element {
const {
updateParsedQuery,
@ -45,7 +48,7 @@ function SearchFilter({
AppState,
ILogsReducer
>((state) => state.logs);
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
const globalTime = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const dispatch = useDispatch<Dispatch<AppActions>>();
@ -69,6 +72,8 @@ function SearchFilter({
const handleSearch = useCallback(
(customQuery: string) => {
getLogsFields();
if (liveTail === 'PLAYING') {
dispatch({
type: TOGGLE_LIVE_TAIL,
@ -77,15 +82,13 @@ function SearchFilter({
dispatch({
type: FLUSH_LOGS,
});
setTimeout(
() =>
dispatch({
type: TOGGLE_LIVE_TAIL,
payload: liveTail,
}),
0,
);
dispatch({
type: TOGGLE_LIVE_TAIL,
payload: liveTail,
});
} else {
const { maxTime, minTime } = globalTime;
getLogs({
q: customQuery,
limit: logLinesPerPage,
@ -117,8 +120,8 @@ function SearchFilter({
idStart,
liveTail,
logLinesPerPage,
maxTime,
minTime,
globalTime,
getLogsFields,
],
);
@ -145,12 +148,12 @@ function SearchFilter({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
urlQueryString,
maxTime,
minTime,
idEnd,
idStart,
logLinesPerPage,
dispatch,
globalTime.maxTime,
globalTime.minTime,
]);
return (
@ -194,6 +197,7 @@ function SearchFilter({
interface DispatchProps {
getLogs: typeof getLogs;
getLogsAggregate: typeof getLogsAggregate;
getLogsFields: typeof GetLogsFields;
}
type SearchFilterProps = DispatchProps;
@ -203,6 +207,7 @@ const mapDispatchToProps = (
): DispatchProps => ({
getLogs: bindActionCreators(getLogs, dispatch),
getLogsAggregate: bindActionCreators(getLogsAggregate, dispatch),
getLogsFields: bindActionCreators(GetLogsFields, dispatch),
});
export default connect(null, mapDispatchToProps)(SearchFilter);
export default connect(null, mapDispatchToProps)(memo(SearchFilter));

View File

@ -1,38 +1,59 @@
import { getMinMax } from 'container/TopNav/AutoRefresh/config';
import useUrlQuery from 'hooks/useUrlQuery';
import history from 'lib/history';
import { parseQuery, reverseParser } from 'lib/logql';
import { ILogQLParsedQueryItem } from 'lib/logql/types';
import isEqual from 'lodash-es/isEqual';
import { useCallback, useEffect } from 'react';
import { useCallback, useEffect, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Dispatch } from 'redux';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import {
SET_SEARCH_QUERY_PARSED_PAYLOAD,
SET_SEARCH_QUERY_STRING,
} from 'types/actions/logs';
import { GlobalReducer } from 'types/reducer/globalTime';
import { ILogsReducer } from 'types/reducer/logs';
import { getGlobalTime } from './utils';
export function useSearchParser(): {
queryString: string;
parsedQuery: unknown;
updateParsedQuery: (arg0: ILogQLParsedQueryItem[]) => void;
updateQueryString: (arg0: string) => void;
} {
const dispatch = useDispatch();
const dispatch = useDispatch<Dispatch<AppActions>>();
const {
searchFilter: { parsedQuery, queryString },
} = useSelector<AppState, ILogsReducer>((store) => store.logs);
const urlQuery = useUrlQuery();
const parsedFilters = useMemo(() => urlQuery.get('q'), [urlQuery]);
const { minTime, maxTime, selectedTime } = useSelector<
AppState,
GlobalReducer
>((store) => store.globalTime);
const updateQueryString = useCallback(
(updatedQueryString: string) => {
history.replace({
pathname: history.location.pathname,
search: updatedQueryString ? `?q=${updatedQueryString}` : '',
search: `?q=${updatedQueryString}`,
});
const globalTime = getMinMax(selectedTime, minTime, maxTime);
dispatch({
type: SET_SEARCH_QUERY_STRING,
payload: updatedQueryString,
payload: {
searchQueryString: updatedQueryString,
globalTime: getGlobalTime(selectedTime, globalTime),
},
});
const parsedQueryFromString = parseQuery(updatedQueryString);
if (!isEqual(parsedQuery, parsedQueryFromString)) {
dispatch({
@ -41,12 +62,18 @@ export function useSearchParser(): {
});
}
},
// need to hide this warning as we don't want to update the query string on every change
// eslint-disable-next-line react-hooks/exhaustive-deps
[dispatch, parsedQuery],
);
useEffect(() => {
if (queryString !== null) updateQueryString(queryString);
}, [queryString, updateQueryString]);
if (!queryString && parsedFilters) {
updateQueryString(parsedFilters);
} else if (queryString) {
updateQueryString(queryString);
}
}, [queryString, updateQueryString, parsedFilters]);
const updateParsedQuery = useCallback(
(updatedParsedPayload: ILogQLParsedQueryItem[]) => {
@ -61,7 +88,9 @@ export function useSearchParser(): {
) {
dispatch({
type: SET_SEARCH_QUERY_STRING,
payload: reversedParsedQuery,
payload: {
searchQueryString: reversedParsedQuery,
},
});
}
},

View File

@ -0,0 +1,12 @@
import { Time } from 'container/TopNav/DateTimeSelection/config';
import { GetMinMaxPayload } from 'lib/getMinMax';
export const getGlobalTime = (
selectedTime: Time,
globalTime: GetMinMaxPayload,
): GetMinMaxPayload | undefined => {
if (selectedTime === 'custom') {
return undefined;
}
return globalTime;
};

View File

@ -47,7 +47,7 @@ export const databaseCallsAvgDuration = ({
const metricNameA = 'signoz_db_latency_sum';
const metricNameB = 'signoz_db_latency_count';
const expression = 'A/B';
const legendFormula = '';
const legendFormula = 'Average Duration';
const legend = '';
const disabled = true;
const additionalItemsA = [

View File

@ -4,8 +4,11 @@ import {
databaseCallsAvgDuration,
databaseCallsRPS,
} from 'container/MetricsApplication/MetricsPageQueries/DBCallQueries';
import { resourceAttributesToTagFilterItems } from 'lib/resourceAttributes';
import React, { useMemo } from 'react';
import {
convertRawQueriesToTraceSelectedTags,
resourceAttributesToTagFilterItems,
} from 'lib/resourceAttributes';
import React, { useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';
import { AppState } from 'store/reducers';
@ -13,9 +16,16 @@ import { Widgets } from 'types/api/dashboard/getAll';
import MetricReducer from 'types/reducer/metrics';
import { Card, GraphContainer, GraphTitle, Row } from '../styles';
import { Button } from './styles';
import {
dbSystemTags,
onGraphClickHandler,
onViewTracePopupClick,
} from './util';
function DBCall({ getWidgetQueryBuilder }: DBCallProps): JSX.Element {
const { servicename } = useParams<{ servicename?: string }>();
const [selectedTimeStamp, setSelectedTimeStamp] = useState<number>(0);
const { resourceAttributeQueries } = useSelector<AppState, MetricReducer>(
(state) => state.metrics,
);
@ -23,6 +33,15 @@ function DBCall({ getWidgetQueryBuilder }: DBCallProps): JSX.Element {
() => resourceAttributesToTagFilterItems(resourceAttributeQueries) || [],
[resourceAttributeQueries],
);
const selectedTraceTags: string = useMemo(
() =>
JSON.stringify(
convertRawQueriesToTraceSelectedTags(resourceAttributeQueries).concat(
...dbSystemTags,
) || [],
),
[resourceAttributeQueries],
);
const legend = '{{db_system}}';
const databaseCallsRPSWidget = useMemo(
@ -39,7 +58,6 @@ function DBCall({ getWidgetQueryBuilder }: DBCallProps): JSX.Element {
}),
[getWidgetQueryBuilder, servicename, tagFilterItems],
);
const databaseCallsAverageDurationWidget = useMemo(
() =>
getWidgetQueryBuilder({
@ -57,6 +75,18 @@ function DBCall({ getWidgetQueryBuilder }: DBCallProps): JSX.Element {
return (
<Row gutter={24}>
<Col span={12}>
<Button
type="default"
size="small"
id="database_call_rps_button"
onClick={onViewTracePopupClick(
servicename,
selectedTraceTags,
selectedTimeStamp,
)}
>
View Traces
</Button>
<Card>
<GraphTitle>Database Calls RPS</GraphTitle>
<GraphContainer>
@ -65,12 +95,33 @@ function DBCall({ getWidgetQueryBuilder }: DBCallProps): JSX.Element {
fullViewOptions={false}
widget={databaseCallsRPSWidget}
yAxisUnit="reqps"
onClickHandler={(ChartEvent, activeElements, chart, data): void => {
onGraphClickHandler(setSelectedTimeStamp)(
ChartEvent,
activeElements,
chart,
data,
'database_call_rps',
);
}}
/>
</GraphContainer>
</Card>
</Col>
<Col span={12}>
<Button
type="default"
size="small"
id="database_call_avg_duration_button"
onClick={onViewTracePopupClick(
servicename,
selectedTraceTags,
selectedTimeStamp,
)}
>
View Traces
</Button>
<Card>
<GraphTitle>Database Calls Avg Duration</GraphTitle>
<GraphContainer>
@ -79,6 +130,15 @@ function DBCall({ getWidgetQueryBuilder }: DBCallProps): JSX.Element {
fullViewOptions={false}
widget={databaseCallsAverageDurationWidget}
yAxisUnit="ms"
onClickHandler={(ChartEvent, activeElements, chart, data): void => {
onGraphClickHandler(setSelectedTimeStamp)(
ChartEvent,
activeElements,
chart,
data,
'database_call_avg_duration',
);
}}
/>
</GraphContainer>
</Card>

View File

@ -1,4 +1,3 @@
import { ActiveElement, Chart, ChartData, ChartEvent } from 'chart.js';
import Graph from 'components/Graph';
import { METRICS_PAGE_QUERY_PARAM } from 'constants/query';
import ROUTES from 'constants/routes';
@ -10,7 +9,7 @@ import {
convertRawQueriesToTraceSelectedTags,
resourceAttributesToTagFilterItems,
} from 'lib/resourceAttributes';
import React, { useCallback, useMemo, useRef } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';
import { UpdateTimeInterval } from 'store/actions';
@ -25,10 +24,11 @@ import {
import { Card, Col, GraphContainer, GraphTitle, Row } from '../styles';
import TopOperationsTable from '../TopOperationsTable';
import { Button } from './styles';
import { onGraphClickHandler, onViewTracePopupClick } from './util';
function Application({ getWidgetQueryBuilder }: DashboardProps): JSX.Element {
const { servicename } = useParams<{ servicename?: string }>();
const selectedTimeStamp = useRef(0);
const [selectedTimeStamp, setSelectedTimeStamp] = useState<number>(0);
const dispatch = useDispatch();
const {
@ -39,7 +39,7 @@ function Application({ getWidgetQueryBuilder }: DashboardProps): JSX.Element {
} = useSelector<AppState, MetricReducer>((state) => state.metrics);
const selectedTraceTags: string = JSON.stringify(
convertRawQueriesToTraceSelectedTags(resourceAttributeQueries, 'array') || [],
convertRawQueriesToTraceSelectedTags(resourceAttributeQueries) || [],
);
const tagFilterItems = useMemo(
@ -77,58 +77,6 @@ function Application({ getWidgetQueryBuilder }: DashboardProps): JSX.Element {
[servicename, topLevelOperations, tagFilterItems, getWidgetQueryBuilder],
);
const onTracePopupClick = (timestamp: number): void => {
const currentTime = timestamp;
const tPlusOne = timestamp + 1 * 60 * 1000;
const urlParams = new URLSearchParams();
urlParams.set(METRICS_PAGE_QUERY_PARAM.startTime, currentTime.toString());
urlParams.set(METRICS_PAGE_QUERY_PARAM.endTime, tPlusOne.toString());
history.replace(
`${
ROUTES.TRACE
}?${urlParams.toString()}&selected={"serviceName":["${servicename}"]}&filterToFetchData=["duration","status","serviceName"]&spanAggregateCurrentPage=1&selectedTags=${selectedTraceTags}&&isFilterExclude={"serviceName":false}&userSelectedFilter={"status":["error","ok"],"serviceName":["${servicename}"]}&spanAggregateCurrentPage=1`,
);
};
const onClickHandler = async (
event: ChartEvent,
elements: ActiveElement[],
chart: Chart,
data: ChartData,
from: string,
): Promise<void> => {
if (event.native) {
const points = chart.getElementsAtEventForMode(
event.native,
'nearest',
{ intersect: true },
true,
);
const id = `${from}_button`;
const buttonElement = document.getElementById(id);
if (points.length !== 0) {
const firstPoint = points[0];
if (data.labels) {
const time = data?.labels[firstPoint.index] as Date;
if (buttonElement) {
buttonElement.style.display = 'block';
buttonElement.style.left = `${firstPoint.element.x}px`;
buttonElement.style.top = `${firstPoint.element.y}px`;
selectedTimeStamp.current = time.getTime();
}
}
} else if (buttonElement && buttonElement.style.display === 'block') {
buttonElement.style.display = 'none';
}
}
};
const onDragSelect = useCallback(
(start: number, end: number) => {
const startTimestamp = Math.trunc(start);
@ -162,9 +110,11 @@ function Application({ getWidgetQueryBuilder }: DashboardProps): JSX.Element {
type="default"
size="small"
id="Service_button"
onClick={(): void => {
onTracePopupClick(selectedTimeStamp.current);
}}
onClick={onViewTracePopupClick(
servicename,
selectedTraceTags,
selectedTimeStamp,
)}
>
View Traces
</Button>
@ -173,7 +123,13 @@ function Application({ getWidgetQueryBuilder }: DashboardProps): JSX.Element {
<GraphContainer>
<Graph
onClickHandler={(ChartEvent, activeElements, chart, data): void => {
onClickHandler(ChartEvent, activeElements, chart, data, 'Service');
onGraphClickHandler(setSelectedTimeStamp)(
ChartEvent,
activeElements,
chart,
data,
'Service',
);
}}
name="service_latency"
type="line"
@ -230,9 +186,11 @@ function Application({ getWidgetQueryBuilder }: DashboardProps): JSX.Element {
type="default"
size="small"
id="Rate_button"
onClick={(): void => {
onTracePopupClick(selectedTimeStamp.current);
}}
onClick={onViewTracePopupClick(
servicename,
selectedTraceTags,
selectedTimeStamp,
)}
>
View Traces
</Button>
@ -243,7 +201,13 @@ function Application({ getWidgetQueryBuilder }: DashboardProps): JSX.Element {
name="operations_per_sec"
fullViewOptions={false}
onClickHandler={(event, element, chart, data): void => {
onClickHandler(event, element, chart, data, 'Rate');
onGraphClickHandler(setSelectedTimeStamp)(
event,
element,
chart,
data,
'Rate',
);
}}
widget={operationPerSecWidget}
yAxisUnit="ops"
@ -260,7 +224,7 @@ function Application({ getWidgetQueryBuilder }: DashboardProps): JSX.Element {
size="small"
id="Error_button"
onClick={(): void => {
onErrorTrackHandler(selectedTimeStamp.current);
onErrorTrackHandler(selectedTimeStamp);
}}
>
View Traces
@ -273,7 +237,13 @@ function Application({ getWidgetQueryBuilder }: DashboardProps): JSX.Element {
name="error_percentage_%"
fullViewOptions={false}
onClickHandler={(ChartEvent, activeElements, chart, data): void => {
onClickHandler(ChartEvent, activeElements, chart, data, 'Error');
onGraphClickHandler(setSelectedTimeStamp)(
ChartEvent,
activeElements,
chart,
data,
'Error',
);
}}
widget={errorPercentageWidget}
yAxisUnit="%"

View File

@ -0,0 +1,75 @@
import { ActiveElement, Chart, ChartData, ChartEvent } from 'chart.js';
import { METRICS_PAGE_QUERY_PARAM } from 'constants/query';
import ROUTES from 'constants/routes';
import history from 'lib/history';
import { Tags } from 'types/reducer/trace';
export const dbSystemTags: Tags[] = [
{
Key: ['db.system.(string)'],
StringValues: [''],
NumberValues: [],
BoolValues: [],
Operator: 'Exists',
},
];
export function onViewTracePopupClick(
servicename: string | undefined,
selectedTraceTags: string,
timestamp: number,
): VoidFunction {
return (): void => {
const currentTime = timestamp;
const tPlusOne = timestamp + 1 * 60 * 1000;
const urlParams = new URLSearchParams();
urlParams.set(METRICS_PAGE_QUERY_PARAM.startTime, currentTime.toString());
urlParams.set(METRICS_PAGE_QUERY_PARAM.endTime, tPlusOne.toString());
history.replace(
`${
ROUTES.TRACE
}?${urlParams.toString()}&selected={"serviceName":["${servicename}"]}&filterToFetchData=["duration","status","serviceName"]&spanAggregateCurrentPage=1&selectedTags=${selectedTraceTags}&&isFilterExclude={"serviceName":false}&userSelectedFilter={"status":["error","ok"],"serviceName":["${servicename}"]}&spanAggregateCurrentPage=1`,
);
};
}
export function onGraphClickHandler(
setSelectedTimeStamp: React.Dispatch<React.SetStateAction<number>>,
) {
return async (
event: ChartEvent,
elements: ActiveElement[],
chart: Chart,
data: ChartData,
from: string,
): Promise<void> => {
if (event.native) {
const points = chart.getElementsAtEventForMode(
event.native,
'nearest',
{ intersect: true },
true,
);
const id = `${from}_button`;
const buttonElement = document.getElementById(id);
if (points.length !== 0) {
const firstPoint = points[0];
if (data.labels) {
const time = data?.labels[firstPoint.index] as Date;
if (buttonElement) {
buttonElement.style.display = 'block';
buttonElement.style.left = `${firstPoint.element.x}px`;
buttonElement.style.top = `${firstPoint.element.y}px`;
setSelectedTimeStamp(time.getTime());
}
}
} else if (buttonElement && buttonElement.style.display === 'block') {
buttonElement.style.display = 'none';
}
}
};
}

View File

@ -1,5 +1,6 @@
import { Table, Tooltip, Typography } from 'antd';
import { Tooltip, Typography } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import { ResizeTable } from 'components/ResizeTable';
import { METRICS_PAGE_QUERY_PARAM } from 'constants/query';
import ROUTES from 'constants/routes';
import history from 'lib/history';
@ -51,7 +52,7 @@ function TopOperationsTable(props: TopOperationsTableProps): JSX.Element {
title: 'Name',
dataIndex: 'name',
key: 'name',
ellipsis: true,
width: 100,
render: (text: string): JSX.Element => (
<Tooltip placement="topLeft" title={text}>
<Typography.Link onClick={(): void => handleOnClick(text)}>
@ -64,6 +65,7 @@ function TopOperationsTable(props: TopOperationsTableProps): JSX.Element {
title: 'P50 (in ms)',
dataIndex: 'p50',
key: 'p50',
width: 50,
sorter: (a: DataProps, b: DataProps): number => a.p50 - b.p50,
render: (value: number): string => (value / 1000000).toFixed(2),
},
@ -71,6 +73,7 @@ function TopOperationsTable(props: TopOperationsTableProps): JSX.Element {
title: 'P95 (in ms)',
dataIndex: 'p95',
key: 'p95',
width: 50,
sorter: (a: DataProps, b: DataProps): number => a.p95 - b.p95,
render: (value: number): string => (value / 1000000).toFixed(2),
},
@ -78,6 +81,7 @@ function TopOperationsTable(props: TopOperationsTableProps): JSX.Element {
title: 'P99 (in ms)',
dataIndex: 'p99',
key: 'p99',
width: 50,
sorter: (a: DataProps, b: DataProps): number => a.p99 - b.p99,
render: (value: number): string => (value / 1000000).toFixed(2),
},
@ -85,18 +89,19 @@ function TopOperationsTable(props: TopOperationsTableProps): JSX.Element {
title: 'Number of Calls',
dataIndex: 'numCalls',
key: 'numCalls',
width: 50,
sorter: (a: TopOperationListItem, b: TopOperationListItem): number =>
a.numCalls - b.numCalls,
},
];
return (
<Table
<ResizeTable
columns={columns}
showHeader
title={(): string => 'Key Operations'}
tableLayout="fixed"
dataSource={data}
columns={columns}
rowKey="name"
/>
);

View File

@ -1,6 +1,6 @@
import { blue } from '@ant-design/colors';
import { SearchOutlined } from '@ant-design/icons';
import { Button, Card, Input, Space, Table } from 'antd';
import { Button, Card, Input, Space } from 'antd';
import type { ColumnsType, ColumnType } from 'antd/es/table';
import type {
FilterConfirmProps,
@ -8,6 +8,7 @@ import type {
} from 'antd/es/table/interface';
import localStorageGet from 'api/browser/localstorage/get';
import localStorageSet from 'api/browser/localstorage/set';
import { ResizeTable } from 'components/ResizeTable';
import { SKIP_ONBOARDING } from 'constants/onboarding';
import ROUTES from 'constants/routes';
import React, { useCallback, useMemo, useState } from 'react';
@ -101,6 +102,7 @@ function Metrics(): JSX.Element {
{
title: 'Application',
dataIndex: 'serviceName',
width: 200,
key: 'serviceName',
...getColumnSearchProps('serviceName'),
},
@ -108,6 +110,7 @@ function Metrics(): JSX.Element {
title: 'P99 latency (in ms)',
dataIndex: 'p99',
key: 'p99',
width: 150,
defaultSortOrder: 'descend',
sorter: (a: DataProps, b: DataProps): number => a.p99 - b.p99,
render: (value: number): string => (value / 1000000).toFixed(2),
@ -116,6 +119,7 @@ function Metrics(): JSX.Element {
title: 'Error Rate (% of total)',
dataIndex: 'errorRate',
key: 'errorRate',
width: 150,
sorter: (a: DataProps, b: DataProps): number => a.errorRate - b.errorRate,
render: (value: number): string => value.toFixed(2),
},
@ -123,6 +127,7 @@ function Metrics(): JSX.Element {
title: 'Operations Per Second',
dataIndex: 'callRate',
key: 'callRate',
width: 150,
sorter: (a: DataProps, b: DataProps): number => a.callRate - b.callRate,
render: (value: number): string => value.toFixed(2),
},
@ -141,10 +146,10 @@ function Metrics(): JSX.Element {
return (
<Container>
<Table
<ResizeTable
columns={columns}
loading={loading}
dataSource={services}
columns={columns}
rowKey="serviceName"
/>
</Container>

View File

@ -23,6 +23,8 @@ function PasswordContainer(): JSX.Element {
ns: 'settings',
});
const [notifications, NotificationElement] = notification.useNotification();
useEffect(() => {
if (currentPassword && !isPasswordValid(currentPassword)) {
setIsPasswordPolicyError(true);
@ -52,13 +54,13 @@ function PasswordContainer(): JSX.Element {
});
if (statusCode === 200) {
notification.success({
notifications.success({
message: t('success', {
ns: 'common',
}),
});
} else {
notification.error({
notifications.error({
message:
error ||
t('something_went_wrong', {
@ -70,7 +72,7 @@ function PasswordContainer(): JSX.Element {
} catch (error) {
setIsLoading(false);
notification.error({
notifications.error({
message: t('something_went_wrong', {
ns: 'common',
}),
@ -80,6 +82,7 @@ function PasswordContainer(): JSX.Element {
return (
<Space direction="vertical" size="large">
{NotificationElement}
<Typography.Title level={3}>
{t('change_password', {
ns: 'settings',

View File

@ -21,6 +21,8 @@ function UpdateName(): JSX.Element {
const [changedName, setChangedName] = useState<string>(user?.name || '');
const [loading, setLoading] = useState<boolean>(false);
const [notifications, NotificationElement] = notification.useNotification();
if (!user || !org) {
return <div />;
}
@ -34,7 +36,7 @@ function UpdateName(): JSX.Element {
});
if (statusCode === 200) {
notification.success({
notifications.success({
message: t('success', {
ns: 'common',
}),
@ -51,7 +53,7 @@ function UpdateName(): JSX.Element {
},
});
} else {
notification.error({
notifications.error({
message: t('something_went_wrong', {
ns: 'common',
}),
@ -59,7 +61,7 @@ function UpdateName(): JSX.Element {
}
setLoading(false);
} catch (error) {
notification.error({
notifications.error({
message: t('something_went_wrong', {
ns: 'common',
}),
@ -70,6 +72,7 @@ function UpdateName(): JSX.Element {
return (
<div>
{NotificationElement}
<Space direction="vertical" size="middle">
<Typography>Name</Typography>
<NameInput

View File

@ -22,6 +22,8 @@ function DashboardGraphSlider({ toggleAddWidget }: Props): JSX.Element {
(state) => state.dashboards,
);
const [notifications, NotificationElement] = notification.useNotification();
const [selectedDashboard] = dashboards;
const { data } = selectedDashboard;
@ -31,7 +33,7 @@ function DashboardGraphSlider({ toggleAddWidget }: Props): JSX.Element {
const emptyLayout = data.layout?.find((e) => e.i === 'empty');
if (emptyLayout === undefined) {
notification.error({
notifications.error({
message: 'Please click on Add Panel Button',
});
return;
@ -43,18 +45,19 @@ function DashboardGraphSlider({ toggleAddWidget }: Props): JSX.Element {
`${history.location.pathname}/new?graphType=${name}&widgetId=${emptyLayout.i}`,
);
} catch (error) {
notification.error({
notifications.error({
message: 'Something went wrong',
});
}
},
[data, toggleAddWidget],
[data, toggleAddWidget, notifications],
);
const isDarkMode = useIsDarkMode();
const fillColor: React.CSSProperties['color'] = isDarkMode ? 'white' : 'black';
return (
<Container>
{NotificationElement}
{menuItems.map(({ name, Icon, display }) => (
<Card
onClick={(event): void => {

View File

@ -1,6 +1,8 @@
import { blue, red } from '@ant-design/colors';
import { PlusOutlined } from '@ant-design/icons';
import { Button, Modal, Row, Space, Table, Tag } from 'antd';
import { Button, Modal, notification, Row, Space, Tag } from 'antd';
import { NotificationInstance } from 'antd/es/notification/interface';
import { ResizeTable } from 'components/ResizeTable';
import React, { useRef, useState } from 'react';
import { connect, useSelector } from 'react-redux';
import { bindActionCreators, Dispatch } from 'redux';
@ -24,6 +26,8 @@ function VariablesSetting({
(state) => state.dashboards,
);
const [notifications, NotificationElement] = notification.useNotification();
const [selectedDashboard] = dashboards;
const {
@ -74,8 +78,7 @@ function VariablesSetting({
if (oldName) {
delete newVariables[oldName];
}
updateDashboardVariables(newVariables);
updateDashboardVariables(newVariables, notifications);
onDoneVariableViewMode();
};
@ -87,7 +90,7 @@ function VariablesSetting({
const handleDeleteConfirm = (): void => {
const newVariables = { ...variables };
if (variableToDelete?.current) delete newVariables[variableToDelete?.current];
updateDashboardVariables(newVariables);
updateDashboardVariables(newVariables, notifications);
variableToDelete.current = null;
setDeleteVariableModal(false);
};
@ -102,15 +105,18 @@ function VariablesSetting({
{
title: 'Variable',
dataIndex: 'name',
width: 100,
key: 'name',
},
{
title: 'Definition',
dataIndex: 'description',
width: 100,
key: 'description',
},
{
title: 'Actions',
width: 50,
key: 'action',
render: (_: IDashboardVariable): JSX.Element => (
<Space>
@ -137,6 +143,7 @@ function VariablesSetting({
return (
<>
{NotificationElement}
{variableViewMode ? (
<VariableItem
variableData={{ ...variableEditData } as IDashboardVariable}
@ -158,7 +165,7 @@ function VariablesSetting({
<PlusOutlined /> New Variables
</Button>
</Row>
<Table columns={columns} dataSource={variablesTableData} />
<ResizeTable columns={columns} dataSource={variablesTableData} />
</>
)}
<Modal
@ -178,6 +185,7 @@ function VariablesSetting({
interface DispatchProps {
updateDashboardVariables: (
props: Record<string, IDashboardVariable>,
notify: NotificationInstance,
) => (dispatch: Dispatch<AppActions>) => void;
}

View File

@ -1,4 +1,5 @@
import { Row } from 'antd';
import { notification, Row } from 'antd';
import { NotificationInstance } from 'antd/es/notification/interface';
import { map, sortBy } from 'lodash-es';
import React, { useState } from 'react';
import { connect, useSelector } from 'react-redux';
@ -25,6 +26,7 @@ function DashboardVariableSelection({
const [update, setUpdate] = useState<boolean>(false);
const [lastUpdatedVar, setLastUpdatedVar] = useState<string>('');
const [notifications, NotificationElement] = notification.useNotification();
const onVarChanged = (name: string): void => {
setLastUpdatedVar(name);
@ -45,7 +47,7 @@ function DashboardVariableSelection({
): void => {
const updatedVariablesData = { ...variables };
updatedVariablesData[name].selectedValue = value;
updateDashboardVariables(updatedVariablesData);
updateDashboardVariables(updatedVariablesData, notifications);
onVarChanged(name);
};
const onAllSelectedUpdate = (
@ -54,12 +56,13 @@ function DashboardVariableSelection({
): void => {
const updatedVariablesData = { ...variables };
updatedVariablesData[name].allSelected = value;
updateDashboardVariables(updatedVariablesData);
updateDashboardVariables(updatedVariablesData, notifications);
onVarChanged(name);
};
return (
<Row style={{ gap: '1rem' }}>
{NotificationElement}
{map(sortBy(Object.keys(variables)), (variableName) => (
<VariableItem
key={`${variableName}${variables[variableName].modificationUUID}`}
@ -81,6 +84,7 @@ function DashboardVariableSelection({
interface DispatchProps {
updateDashboardVariables: (
props: Parameters<typeof UpdateDashboardVariables>[0],
notify: NotificationInstance,
) => (dispatch: Dispatch<AppActions>) => void;
}

View File

@ -32,10 +32,11 @@ function ShareModal({
const [isViewJSON, setIsViewJSON] = useState<boolean>(false);
const { t } = useTranslation(['dashboard', 'common']);
const [state, setCopy] = useCopyToClipboard();
const [notifications, NotificationElement] = notification.useNotification();
useEffect(() => {
if (state.error) {
notification.error({
notifications.error({
message: t('something_went_wrong', {
ns: 'common',
}),
@ -43,13 +44,13 @@ function ShareModal({
}
if (state.value) {
notification.success({
notifications.success({
message: t('success', {
ns: 'common',
}),
});
}
}, [state.error, state.value, t]);
}, [state.error, state.value, t, notifications]);
const selectedDataCleaned = cleardQueryData(selectedData);
const GetFooterComponent = useMemo(() => {
@ -83,28 +84,34 @@ function ShareModal({
}, [isViewJSON, jsonValue, selectedData, selectedDataCleaned, setCopy, t]);
return (
<Modal
open={isJSONModalVisible}
onCancel={(): void => {
onToggleHandler();
setIsViewJSON(false);
}}
width="70vw"
centered
title={t('share', {
ns: 'common',
})}
okText={t('download_json')}
cancelText={t('cancel')}
destroyOnClose
footer={GetFooterComponent}
>
{!isViewJSON ? (
<Typography>{t('export_dashboard')}</Typography>
) : (
<Editor onChange={(value): void => setJSONValue(value)} value={jsonValue} />
)}
</Modal>
<>
{NotificationElement}
<Modal
open={isJSONModalVisible}
onCancel={(): void => {
onToggleHandler();
setIsViewJSON(false);
}}
width="70vw"
centered
title={t('share', {
ns: 'common',
})}
okText={t('download_json')}
cancelText={t('cancel')}
destroyOnClose
footer={GetFooterComponent}
>
{!isViewJSON ? (
<Typography>{t('export_dashboard')}</Typography>
) : (
<Editor
onChange={(value): void => setJSONValue(value)}
value={jsonValue}
/>
)}
</Modal>
</>
);
}

View File

@ -37,6 +37,7 @@ function QueryBuilderQueryContainer({
metricsBuilderQueries,
selectedGraph,
}: IQueryBuilderQueryContainerProps): JSX.Element | null {
const [notifications, NotificationElement] = notification.useNotification();
const handleQueryBuilderQueryChange = ({
queryIndex,
aggregateFunction,
@ -113,7 +114,7 @@ function QueryBuilderQueryContainer({
};
const addQueryHandler = (): void => {
if (!canCreateQueryAndFormula(queryData)) {
notification.error({
notifications.error({
message:
'Unable to create query. You can create at max 10 queries and formulae.',
});
@ -130,7 +131,7 @@ function QueryBuilderQueryContainer({
const addFormulaHandler = (): void => {
if (!canCreateQueryAndFormula(queryData)) {
notification.error({
notifications.error({
message:
'Unable to create formula. You can create at max 10 queries and formulae.',
});
@ -155,6 +156,7 @@ function QueryBuilderQueryContainer({
}
return (
<>
{NotificationElement}
{metricsBuilderQueries.queryBuilder.map((q, idx) => (
<MetricsBuilder
key={q.name}

View File

@ -74,7 +74,6 @@ function QuerySection({
setLocalQueryChanges(cloneDeep(query) as Query);
}, [query]);
const queryDiff = (
queryA: Query,
queryB: Query,
@ -142,7 +141,7 @@ function QuerySection({
const handleLocalQueryUpdate = ({
updatedQuery,
}: IHandleUpdatedQuery): void => {
setLocalQueryChanges(updatedQuery);
setLocalQueryChanges(cloneDeep(updatedQuery));
};
return (

View File

@ -21,6 +21,8 @@ function AddDomain({ refetch }: Props): JSX.Element {
const { org } = useSelector<AppState, AppReducer>((state) => state.app);
const [notifications, NotificationElement] = notification.useNotification();
const onCreateHandler = async (): Promise<void> => {
try {
const response = await createDomainApi({
@ -29,19 +31,19 @@ function AddDomain({ refetch }: Props): JSX.Element {
});
if (response.statusCode === 200) {
notification.success({
notifications.success({
message: 'Your domain has been added successfully.',
duration: 15,
});
setIsDomain(false);
refetch();
} else {
notification.error({
notifications.error({
message: t('common:something_went_wrong'),
});
}
} catch (error) {
notification.error({
notifications.error({
message: t('common:something_went_wrong'),
});
}
@ -49,6 +51,7 @@ function AddDomain({ refetch }: Props): JSX.Element {
return (
<>
{NotificationElement}
<Container>
<Typography.Title level={3}>
{t('authenticated_domains', {

View File

@ -31,6 +31,8 @@ function EditSSO({
const { t } = useTranslation(['common']);
const [notifications, NotificationElement] = notification.useNotification();
const onFinishHandler = useCallback(() => {
form
.validateFields()
@ -44,11 +46,11 @@ function EditSSO({
});
})
.catch(() => {
notification.error({
notifications.error({
message: t('something_went_wrong', { ns: 'common' }),
});
});
}, [form, onRecordUpdateHandler, record, t]);
}, [form, onRecordUpdateHandler, record, t, notifications]);
const onResetHandler = useCallback(() => {
form.resetFields();
@ -61,7 +63,7 @@ function EditSSO({
initialValues={record}
onFinishFailed={(error): void => {
error.errorFields.forEach(({ errors }) => {
notification.error({
notifications.error({
message:
errors[0].toString() || t('something_went_wrong', { ns: 'common' }),
});
@ -73,6 +75,7 @@ function EditSSO({
autoComplete="off"
form={form}
>
{NotificationElement}
{renderFormInputs(record)}
<Space
style={{ width: '100%', justifyContent: 'flex-end' }}

View File

@ -1,9 +1,10 @@
import { LockTwoTone } from '@ant-design/icons';
import { Button, Modal, notification, Space, Table, Typography } from 'antd';
import { Button, Modal, notification, Space, Typography } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import deleteDomain from 'api/SAML/deleteDomain';
import listAllDomain from 'api/SAML/listAllDomain';
import updateDomain from 'api/SAML/updateDomain';
import { ResizeTable } from 'components/ResizeTable';
import TextToolTip from 'components/TextToolTip';
import { SIGNOZ_UPGRADE_PLAN_URL } from 'constants/app';
import { FeatureKeys } from 'constants/featureKeys';
@ -56,6 +57,8 @@ function AuthDomains(): JSX.Element {
enabled: org !== null,
});
const [notifications, NotificationElement] = notification.useNotification();
const assignSsoMethod = useCallback(
(typ: AuthDomain['ssoType']): void => {
setCurrentDomain({ ...currentDomain, ssoType: typ } as AuthDomain);
@ -76,7 +79,7 @@ function AuthDomains(): JSX.Element {
const response = await updateDomain(record);
if (response.statusCode === 200) {
notification.success({
notifications.success({
message: t('saml_settings', {
ns: 'organizationsettings',
}),
@ -87,7 +90,7 @@ function AuthDomains(): JSX.Element {
return true;
}
notification.error({
notifications.error({
message: t('something_went_wrong', {
ns: 'common',
}),
@ -95,7 +98,7 @@ function AuthDomains(): JSX.Element {
return false;
} catch (error) {
notification.error({
notifications.error({
message: t('something_went_wrong', {
ns: 'common',
}),
@ -103,7 +106,7 @@ function AuthDomains(): JSX.Element {
return false;
}
},
[refetch, t, onCloseHandler],
[refetch, t, onCloseHandler, notifications],
);
const onOpenHandler = useCallback(
@ -142,19 +145,19 @@ function AuthDomains(): JSX.Element {
});
if (response.statusCode === 200) {
notification.success({
notifications.success({
message: t('common:success'),
});
refetch();
} else {
notification.error({
notifications.error({
message: t('common:something_went_wrong'),
});
}
},
});
},
[refetch, t],
[refetch, t, notifications],
);
const onClickLicenseHandler = useCallback(() => {
@ -166,6 +169,7 @@ function AuthDomains(): JSX.Element {
title: 'Domain',
dataIndex: 'name',
key: 'name',
width: 100,
},
{
title: (
@ -181,6 +185,7 @@ function AuthDomains(): JSX.Element {
),
dataIndex: 'ssoEnabled',
key: 'ssoEnabled',
width: 80,
render: (value: boolean, record: AuthDomain): JSX.Element => {
if (!SSOFlag) {
return (
@ -207,6 +212,7 @@ function AuthDomains(): JSX.Element {
title: '',
dataIndex: 'description',
key: 'description',
width: 100,
render: (_, record: AuthDomain): JSX.Element => {
if (!SSOFlag) {
return (
@ -231,6 +237,7 @@ function AuthDomains(): JSX.Element {
title: 'Action',
dataIndex: 'action',
key: 'action',
width: 50,
render: (_, record): JSX.Element => (
<Button
disabled={!SSOFlag}
@ -247,6 +254,7 @@ function AuthDomains(): JSX.Element {
if (!isLoading && data?.payload?.length === 0) {
return (
<Space direction="vertical" size="middle">
{NotificationElement}
<AddDomain refetch={refetch} />
<Modal
@ -264,10 +272,10 @@ function AuthDomains(): JSX.Element {
setIsSettingsOpen={setIsSettingsOpen}
/>
</Modal>
<Table
<ResizeTable
columns={columns}
rowKey={(record: AuthDomain): string => record.name + v4()}
dataSource={!SSOFlag ? notEntripriseData : []}
columns={columns}
tableLayout="fixed"
/>
</Space>
@ -278,6 +286,7 @@ function AuthDomains(): JSX.Element {
return (
<>
{NotificationElement}
<Modal
centered
title="Configure Authentication Method"
@ -313,10 +322,10 @@ function AuthDomains(): JSX.Element {
<Space direction="vertical" size="middle">
<AddDomain refetch={refetch} />
<Table
<ResizeTable
columns={columns}
dataSource={tableData}
loading={isLoading}
columns={columns}
tableLayout="fixed"
rowKey={(record: AuthDomain): string => record.name + v4()}
/>

View File

@ -21,6 +21,7 @@ function DisplayName({
const { name } = (org || [])[index];
const [isLoading, setIsLoading] = useState<boolean>(false);
const dispatch = useDispatch<Dispatch<AppActions>>();
const [notifications, NotificationElement] = notification.useNotification();
const onSubmit = async ({ name: orgName }: OnSubmitProps): Promise<void> => {
try {
@ -31,7 +32,7 @@ function DisplayName({
orgId,
});
if (statusCode === 200) {
notification.success({
notifications.success({
message: t('success', {
ns: 'common',
}),
@ -44,7 +45,7 @@ function DisplayName({
},
});
} else {
notification.error({
notifications.error({
message:
error ||
t('something_went_wrong', {
@ -55,7 +56,7 @@ function DisplayName({
setIsLoading(false);
} catch (error) {
setIsLoading(false);
notification.error({
notifications.error({
message: t('something_went_wrong', {
ns: 'common',
}),
@ -75,6 +76,7 @@ function DisplayName({
onFinish={onSubmit}
autoComplete="off"
>
{NotificationElement}
<Form.Item name="name" label="Display name" rules={[{ required: true }]}>
<Input size="large" placeholder={t('signoz')} disabled={isLoading} />
</Form.Item>

View File

@ -36,19 +36,21 @@ function EditMembersDetails({
[],
);
const [notifications, NotificationElement] = notification.useNotification();
useEffect(() => {
if (state.error) {
notification.error({
notifications.error({
message: t('something_went_wrong'),
});
}
if (state.value) {
notification.success({
notifications.success({
message: t('success'),
});
}
}, [state.error, state.value, t]);
}, [state.error, state.value, t, notifications]);
const onPasswordChangeHandler: React.ChangeEventHandler<HTMLInputElement> = useCallback(
(event) => {
@ -67,7 +69,7 @@ function EditMembersDetails({
if (response.statusCode === 200) {
setPasswordLink(getPasswordLink(response.payload.token));
} else {
notification.error({
notifications.error({
message:
response.error ||
t('something_went_wrong', {
@ -79,7 +81,7 @@ function EditMembersDetails({
} catch (error) {
setIsLoading(false);
notification.error({
notifications.error({
message: t('something_went_wrong', {
ns: 'common',
}),
@ -89,6 +91,7 @@ function EditMembersDetails({
return (
<Space direction="vertical" size="large">
{NotificationElement}
<Space direction="horizontal">
<Title>Email address</Title>
<Input

View File

@ -1,9 +1,10 @@
import { Button, Modal, notification, Space, Table, Typography } from 'antd';
import { Button, Modal, notification, Space, Typography } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import deleteUser from 'api/user/deleteUser';
import editUserApi from 'api/user/editUser';
import getOrgUser from 'api/user/getOrgUser';
import updateRole from 'api/user/updateRole';
import { ResizeTable } from 'components/ResizeTable';
import dayjs from 'dayjs';
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
@ -39,6 +40,7 @@ function UserFunction({
const { t } = useTranslation(['common']);
const [isDeleteLoading, setIsDeleteLoading] = useState<boolean>(false);
const [isUpdateLoading, setIsUpdateLoading] = useState<boolean>(false);
const [notifications, NotificationElement] = notification.useNotification();
const onUpdateDetailsHandler = (): void => {
setDataSource((data) => {
@ -88,14 +90,14 @@ function UserFunction({
if (response.statusCode === 200) {
onDelete();
notification.success({
notifications.success({
message: t('success', {
ns: 'common',
}),
});
setIsDeleteModalVisible(false);
} else {
notification.error({
notifications.error({
message:
response.error ||
t('something_went_wrong', {
@ -107,7 +109,7 @@ function UserFunction({
} catch (error) {
setIsDeleteLoading(false);
notification.error({
notifications.error({
message: t('something_went_wrong', {
ns: 'common',
}),
@ -134,13 +136,13 @@ function UserFunction({
updateRoleResponse.statusCode === 200
) {
onUpdateDetailsHandler();
notification.success({
notifications.success({
message: t('success', {
ns: 'common',
}),
});
} else {
notification.error({
notifications.error({
message:
editUserResponse.error ||
updateRoleResponse.error ||
@ -151,7 +153,7 @@ function UserFunction({
}
setIsUpdateLoading(false);
} catch (error) {
notification.error({
notifications.error({
message: t('something_went_wrong', {
ns: 'common',
}),
@ -162,6 +164,7 @@ function UserFunction({
return (
<>
{NotificationElement}
<Space direction="horizontal">
<Typography.Link
onClick={(): void => onModalToggleHandler(setIsModalVisible, true)}
@ -256,21 +259,25 @@ function Members(): JSX.Element {
title: 'Name',
dataIndex: 'name',
key: 'name',
width: 100,
},
{
title: 'Emails',
dataIndex: 'email',
key: 'email',
width: 100,
},
{
title: 'Access Level',
dataIndex: 'accessLevel',
key: 'accessLevel',
width: 50,
},
{
title: 'Joined On',
dataIndex: 'joinedOn',
key: 'joinedOn',
width: 60,
render: (_, record): JSX.Element => {
const { joinedOn } = record;
return (
@ -283,6 +290,7 @@ function Members(): JSX.Element {
{
title: 'Action',
dataIndex: 'action',
width: 80,
render: (_, record): JSX.Element => (
<UserFunction
{...{
@ -301,10 +309,10 @@ function Members(): JSX.Element {
return (
<Space direction="vertical" size="middle">
<Typography.Title level={3}>Members</Typography.Title>
<Table
<ResizeTable
columns={columns}
tableLayout="fixed"
dataSource={dataSource}
columns={columns}
pagination={false}
loading={status === 'loading'}
/>

View File

@ -1,9 +1,10 @@
import { PlusOutlined } from '@ant-design/icons';
import { Button, Modal, notification, Space, Table, Typography } from 'antd';
import { Button, Modal, notification, Space, Typography } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import deleteInvite from 'api/user/deleteInvite';
import getPendingInvites from 'api/user/getPendingInvites';
import sendInvite from 'api/user/sendInvite';
import { ResizeTable } from 'components/ResizeTable';
import { INVITE_MEMBERS_HASH } from 'constants/app';
import ROUTES from 'constants/routes';
import React, { useCallback, useEffect, useState } from 'react';
@ -25,22 +26,23 @@ function PendingInvitesContainer(): JSX.Element {
const [isInvitingMembers, setIsInvitingMembers] = useState<boolean>(false);
const { t } = useTranslation(['organizationsettings', 'common']);
const [state, setText] = useCopyToClipboard();
const [notifications, NotificationElement] = notification.useNotification();
useEffect(() => {
if (state.error) {
notification.error({
notifications.error({
message: state.error.message,
});
}
if (state.value) {
notification.success({
notifications.success({
message: t('success', {
ns: 'common',
}),
});
}
}, [state.error, state.value, t]);
}, [state.error, state.value, t, notifications]);
const getPendingInvitesResponse = useQuery({
queryFn: () => getPendingInvites(),
@ -112,13 +114,13 @@ function PendingInvitesContainer(): JSX.Element {
...dataSource.slice(index + 1, dataSource.length),
]);
}
notification.success({
notifications.success({
message: t('success', {
ns: 'common',
}),
});
} else {
notification.error({
notifications.error({
message:
response.error ||
t('something_went_wrong', {
@ -127,7 +129,7 @@ function PendingInvitesContainer(): JSX.Element {
});
}
} catch (error) {
notification.error({
notifications.error({
message: t('something_went_wrong', {
ns: 'common',
}),
@ -140,26 +142,31 @@ function PendingInvitesContainer(): JSX.Element {
title: 'Name',
dataIndex: 'name',
key: 'name',
width: 100,
},
{
title: 'Emails',
dataIndex: 'email',
key: 'email',
width: 80,
},
{
title: 'Access Level',
dataIndex: 'accessLevel',
key: 'accessLevel',
width: 50,
},
{
title: 'Invite Link',
dataIndex: 'inviteLink',
key: 'Invite Link',
ellipsis: true,
width: 100,
},
{
title: 'Action',
dataIndex: 'action',
width: 80,
key: 'Action',
render: (_, record): JSX.Element => (
<Space direction="horizontal">
@ -192,7 +199,7 @@ function PendingInvitesContainer(): JSX.Element {
});
if (statusCode !== 200) {
notification.error({
notifications.error({
message:
error ||
t('something_went_wrong', {
@ -212,7 +219,7 @@ function PendingInvitesContainer(): JSX.Element {
toggleModal(false);
}, 2000);
} catch (error) {
notification.error({
notifications.error({
message: t('something_went_wrong', {
ns: 'common',
}),
@ -222,6 +229,7 @@ function PendingInvitesContainer(): JSX.Element {
return (
<div>
{NotificationElement}
<Modal
title={t('invite_team_members')}
open={isInviteTeamMemberModalOpen}
@ -261,10 +269,10 @@ function PendingInvitesContainer(): JSX.Element {
{t('invite_members')}
</Button>
</TitleWrapper>
<Table
<ResizeTable
columns={columns}
tableLayout="fixed"
dataSource={dataSource}
columns={columns}
pagination={false}
loading={getPendingInvitesResponse.status === 'loading'}
/>

View File

@ -24,6 +24,7 @@ function ResetPassword({ version }: ResetPasswordProps): JSX.Element {
const { search } = useLocation();
const params = new URLSearchParams(search);
const token = params.get('token');
const [notifications, NotificationElement] = notification.useNotification();
useEffect(() => {
if (!token) {
@ -53,14 +54,14 @@ function ResetPassword({ version }: ResetPasswordProps): JSX.Element {
});
if (response.statusCode === 200) {
notification.success({
notifications.success({
message: t('success', {
ns: 'common',
}),
});
history.push(ROUTES.LOGIN);
} else {
notification.error({
notifications.error({
message:
response.error ||
t('something_went_wrong', {
@ -72,7 +73,7 @@ function ResetPassword({ version }: ResetPasswordProps): JSX.Element {
setLoading(false);
} catch (error) {
setLoading(false);
notification.error({
notifications.error({
message: t('something_went_wrong', {
ns: 'common',
}),
@ -82,70 +83,73 @@ function ResetPassword({ version }: ResetPasswordProps): JSX.Element {
return (
<WelcomeLeftContainer version={version}>
<FormWrapper>
<form onSubmit={handleSubmit}>
<Title level={4}>Reset Your Password</Title>
<>
{NotificationElement}
<FormWrapper>
<form onSubmit={handleSubmit}>
<Title level={4}>Reset Your Password</Title>
<div>
<Label htmlFor="Password">Password</Label>
<Input.Password
value={password}
onChange={(e): void => {
setState(e.target.value, setPassword);
}}
required
id="currentPassword"
/>
</div>
<div>
<Label htmlFor="ConfirmPassword">Confirm Password</Label>
<Input.Password
value={confirmPassword}
onChange={(e): void => {
const updateValue = e.target.value;
setState(updateValue, setConfirmPassword);
if (password !== updateValue) {
setConfirmPasswordError(true);
} else {
setConfirmPasswordError(false);
}
}}
required
id="UpdatePassword"
/>
{confirmPasswordError && (
<Typography.Paragraph
italic
style={{
color: '#D89614',
marginTop: '0.50rem',
<div>
<Label htmlFor="Password">Password</Label>
<Input.Password
value={password}
onChange={(e): void => {
setState(e.target.value, setPassword);
}}
>
Passwords dont match. Please try again
</Typography.Paragraph>
)}
</div>
required
id="currentPassword"
/>
</div>
<div>
<Label htmlFor="ConfirmPassword">Confirm Password</Label>
<Input.Password
value={confirmPassword}
onChange={(e): void => {
const updateValue = e.target.value;
setState(updateValue, setConfirmPassword);
if (password !== updateValue) {
setConfirmPasswordError(true);
} else {
setConfirmPasswordError(false);
}
}}
required
id="UpdatePassword"
/>
<ButtonContainer>
<Button
type="primary"
htmlType="submit"
data-attr="signup"
loading={loading}
disabled={
loading ||
!password ||
!confirmPassword ||
confirmPasswordError ||
token === null
}
>
Get Started
</Button>
</ButtonContainer>
</form>
</FormWrapper>
{confirmPasswordError && (
<Typography.Paragraph
italic
style={{
color: '#D89614',
marginTop: '0.50rem',
}}
>
Passwords dont match. Please try again
</Typography.Paragraph>
)}
</div>
<ButtonContainer>
<Button
type="primary"
htmlType="submit"
data-attr="signup"
loading={loading}
disabled={
loading ||
!password ||
!confirmPassword ||
confirmPasswordError ||
token === null
}
>
Get Started
</Button>
</ButtonContainer>
</form>
</FormWrapper>
</>
</WelcomeLeftContainer>
);
}

View File

@ -38,6 +38,8 @@ function CheckBoxComponent(props: CheckBoxProps): JSX.Element {
(userSelectedFilter.get(name) || []).find((e) => e === keyValue) !==
undefined;
const [notifications, NotificationElement] = notification.useNotification();
// eslint-disable-next-line sonarjs/cognitive-complexity
const onCheckHandler = async (): Promise<void> => {
try {
@ -141,12 +143,12 @@ function CheckBoxComponent(props: CheckBoxProps): JSX.Element {
} else {
setIsLoading(false);
notification.error({
notifications.error({
message: response.error || 'Something went wrong',
});
}
} catch (error) {
notification.error({
notifications.error({
message: (error as AxiosError).toString() || 'Something went wrong',
});
setIsLoading(false);
@ -161,6 +163,7 @@ function CheckBoxComponent(props: CheckBoxProps): JSX.Element {
return (
<CheckBoxContainer>
{NotificationElement}
<Checkbox
disabled={isLoading || filterLoading}
onClick={onCheckHandler}

View File

@ -28,6 +28,7 @@ function TraceID(): JSX.Element {
);
const [isLoading, setIsLoading] = useState(false);
const [userEnteredValue, setUserEnteredValue] = useState<string>('');
const [notifications, NotificationElement] = notification.useNotification();
useEffect(() => {
setUserEnteredValue(selectedFilter.get('traceID')?.[0] || '');
}, [selectedFilter]);
@ -91,7 +92,7 @@ function TraceID(): JSX.Element {
);
}
} catch (error) {
notification.error({
notifications.error({
message: (error as AxiosError).toString() || 'Something went wrong',
});
} finally {
@ -108,6 +109,7 @@ function TraceID(): JSX.Element {
};
return (
<div>
{NotificationElement}
<Search
placeholder="Filter by Trace ID"
onSearch={onSearch}

View File

@ -53,6 +53,8 @@ function PanelHeading(props: PanelHeadingProps): JSX.Element {
const defaultErrorMessage = 'Something went wrong';
const [notifications, NotificationElement] = notification.useNotification();
// eslint-disable-next-line sonarjs/cognitive-complexity
const onExpandHandler: React.MouseEventHandler<HTMLDivElement> = async (e) => {
try {
@ -119,14 +121,14 @@ function PanelHeading(props: PanelHeadingProps): JSX.Element {
spansAggregate.orderParam,
);
} else {
notification.error({
notifications.error({
message: response.error || defaultErrorMessage,
});
}
setIsLoading(false);
} catch (error) {
notification.error({
notifications.error({
message: (error as AxiosError).toString() || defaultErrorMessage,
});
}
@ -225,13 +227,13 @@ function PanelHeading(props: PanelHeadingProps): JSX.Element {
spansAggregate.orderParam,
);
} else {
notification.error({
notifications.error({
message: response.error || 'Something went wrong',
});
}
setIsLoading(false);
} catch (error) {
notification.error({
notifications.error({
message: (error as AxiosError).toString(),
});
setIsLoading(false);
@ -295,6 +297,7 @@ function PanelHeading(props: PanelHeadingProps): JSX.Element {
return (
<>
{NotificationElement}
{PanelName !== 'duration' && <Divider plain style={{ margin: 0 }} />}
<Card bordered={false}>

View File

@ -22,7 +22,7 @@ function TraceGraph(): JSX.Element {
const ChartData = useMemo(
() =>
selectedGroupBy.length === 0
selectedGroupBy.length === 0 || selectedGroupBy === 'none'
? getChartData(payload)
: getChartDataforGroupBy(payload),
[payload, selectedGroupBy],

View File

@ -9,13 +9,12 @@ interface Dropdown {
export const groupBy: DefaultOptionType[] = [
{
label: 'None',
value: '',
value: 'none',
},
{
label: 'Service Name',
value: 'serviceName',
},
{
label: 'Operation',
value: 'name',

View File

@ -1,6 +1,6 @@
import { AutoComplete, Input, Space } from 'antd';
import getTagFilters from 'api/trace/getTagFilter';
import React, { useMemo } from 'react';
import React, { useMemo, useState } from 'react';
import { useQuery } from 'react-query';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
@ -10,6 +10,7 @@ import { TraceReducer } from 'types/reducer/trace';
import { functions } from './config';
import { SelectComponent } from './styles';
import {
filterGroupBy,
getSelectedValue,
initOptions,
onClickSelectedFunctionHandler,
@ -24,6 +25,9 @@ function TraceGraphFilter(): JSX.Element {
AppState,
TraceReducer
>((state) => state.traces);
const [selectedGroupByLocal, setSelectedGroupByLocal] = useState<string>(
selectedGroupBy,
);
const globalTime = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
@ -75,8 +79,12 @@ function TraceGraphFilter(): JSX.Element {
id="selectedGroupBy"
data-testid="selectedGroupBy"
options={options}
value={selectedGroupByValue(selectedGroupBy, options)}
onChange={onClickSelectedGroupByHandler(options)}
value={selectedGroupByValue(selectedGroupByLocal, options)}
onChange={(e): void => setSelectedGroupByLocal(e.toString())}
onSelect={onClickSelectedGroupByHandler(options)}
filterOption={(inputValue, option): boolean =>
filterGroupBy(inputValue, option)
}
>
<Input disabled={isLoading} placeholder="Please select" />
</AutoComplete>

View File

@ -0,0 +1,27 @@
import { selectedGroupByValue } from './utils';
const options = [
{
value: '1',
label: '1',
},
{
value: '2',
label: '2',
},
];
describe('TraceGraphFilter/utils', () => {
it('should return the correct value', () => {
const selectedGroupBy = '1';
const result = selectedGroupByValue(selectedGroupBy, options);
expect(result).toEqual(selectedGroupBy);
});
it('should return the correct value when selectedOption not found', () => {
const selectedGroupBy = '3';
const result = selectedGroupByValue(selectedGroupBy, options);
expect(result).toEqual(selectedGroupBy);
});
});

View File

@ -61,13 +61,28 @@ export function onClickSelectedFunctionHandler(value: unknown): void {
}
}
}
export function selectedGroupByValue(
selectedGroupBy: string,
options: DefaultOptionType[],
): ReactNode {
return options.find((e) => selectedGroupBy === e.value)?.label;
const optionValue = options.find((e) => selectedGroupBy === e.value)?.label;
if (optionValue) {
return optionValue;
}
return selectedGroupBy;
}
export function getSelectedValue(selectedFunction: string): unknown {
return functions.find((e) => selectedFunction === e.key)?.displayValue;
}
export function filterGroupBy(
inputValue: string,
option: DefaultOptionType | undefined,
): boolean {
return (
option?.label?.toString().toUpperCase().indexOf(inputValue.toUpperCase()) !==
-1
);
}

View File

@ -1,5 +1,6 @@
import { Table, TableProps, Tag, Typography } from 'antd';
import { TableProps, Tag, Typography } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import { ResizeTable } from 'components/ResizeTable';
import ROUTES from 'constants/routes';
import {
getSpanOrder,
@ -73,6 +74,7 @@ function TraceTable(): JSX.Element {
title: 'Date',
dataIndex: 'timestamp',
key: 'timestamp',
width: 120,
sorter: true,
render: (value: TableType['timestamp']): JSX.Element => {
const day = dayjs(value);
@ -83,18 +85,21 @@ function TraceTable(): JSX.Element {
title: 'Service',
dataIndex: 'serviceName',
key: 'serviceName',
width: 50,
render: getValue,
},
{
title: 'Operation',
dataIndex: 'operation',
key: 'operation',
width: 110,
render: getValue,
},
{
title: 'Duration',
dataIndex: 'durationNano',
key: 'durationNano',
width: 50,
sorter: true,
render: (value: TableType['durationNano']): JSX.Element => (
<Typography>
@ -109,12 +114,14 @@ function TraceTable(): JSX.Element {
title: 'Method',
dataIndex: 'method',
key: 'method',
width: 50,
render: getHttpMethodOrStatus,
},
{
title: 'Status Code',
dataIndex: 'statusCode',
key: 'statusCode',
width: 50,
render: getHttpMethodOrStatus,
},
];
@ -180,16 +187,18 @@ function TraceTable(): JSX.Element {
) as number;
return (
<Table
<ResizeTable
columns={columns}
onChange={onChangeHandler}
dataSource={spansAggregate.data}
loading={loading || filterLoading}
columns={columns}
rowKey={(record): string => `${record.traceID}-${record.spanID}-${v4()}`}
rowKey={(record: { traceID: string; spanID: string }): string =>
`${record.traceID}-${record.spanID}-${v4()}`
}
style={{
cursor: 'pointer',
}}
onRow={(record): React.HTMLAttributes<TableType> => ({
onRow={(record: TableType): React.HTMLAttributes<TableType> => ({
onClick: (event): void => {
event.preventDefault();
event.stopPropagation();

View File

@ -1,9 +1,4 @@
/**
* @jest-environment jsdom
*/
import { render } from '@testing-library/react';
import { renderHook } from '@testing-library/react-hooks';
import { render, renderHook } from '@testing-library/react';
import TraceFlameGraph from 'container/TraceFlameGraph';
import React, { useState } from 'react';
import { Provider } from 'react-redux';

Some files were not shown because too many files have changed in this diff Show More