mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-12 04:29:04 +08:00
commit
cb22aef36f
2
.github/config.yml
vendored
2
.github/config.yml
vendored
@ -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!
|
||||
|
||||
|
||||

|
||||
|
||||
We here at SigNoz are proud of you! 🥳
|
||||
|
2
.github/workflows/e2e-k3s.yaml
vendored
2
.github/workflows/e2e-k3s.yaml
vendored
@ -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
|
||||
|
1
.github/workflows/pr_verify_linked_issue.yml
vendored
1
.github/workflows/pr_verify_linked_issue.yml
vendored
@ -17,4 +17,3 @@ jobs:
|
||||
uses: hattan/verify-linked-issue-action@v1.1.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
|
1
.github/workflows/sonar.yml
vendored
1
.github/workflows/sonar.yml
vendored
@ -24,4 +24,3 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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:
|
||||
|
@ -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"
|
||||
|
@ -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'],
|
||||
|
@ -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",
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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),
|
||||
);
|
||||
|
@ -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]);
|
||||
|
||||
|
@ -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>}
|
||||
|
@ -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"
|
||||
|
@ -1,7 +1,3 @@
|
||||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
|
||||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
|
51
frontend/src/components/ResizeTable/ResizableHeader.tsx
Normal file
51
frontend/src/components/ResizeTable/ResizableHeader.tsx
Normal 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;
|
51
frontend/src/components/ResizeTable/ResizeTable.tsx
Normal file
51
frontend/src/components/ResizeTable/ResizeTable.tsx
Normal 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;
|
1
frontend/src/components/ResizeTable/config.ts
Normal file
1
frontend/src/components/ResizeTable/config.ts
Normal file
@ -0,0 +1 @@
|
||||
export const enableUserSelectHack = { enableUserSelectHack: false };
|
4
frontend/src/components/ResizeTable/index.ts
Normal file
4
frontend/src/components/ResizeTable/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import ResizableHeader from './ResizableHeader';
|
||||
import ResizeTable from './ResizeTable';
|
||||
|
||||
export { ResizableHeader, ResizeTable };
|
11
frontend/src/components/ResizeTable/styles.ts
Normal file
11
frontend/src/components/ResizeTable/styles.ts
Normal 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;
|
||||
`;
|
@ -37,6 +37,7 @@ const themeColors = {
|
||||
matterhornGrey: '#555555',
|
||||
whiteCream: '#ffffffd5',
|
||||
black: '#000000',
|
||||
lightgrey: '#ddd',
|
||||
};
|
||||
|
||||
export { themeColors };
|
||||
|
@ -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" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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 />}
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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 {
|
||||
}×tamp=${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>
|
||||
</>
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
|
@ -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">
|
||||
|
@ -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%;
|
||||
|
@ -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>
|
||||
|
@ -43,6 +43,7 @@ function GridCardGraph({
|
||||
const { ref: graphRef, inView: isGraphVisible } = useInView({
|
||||
threshold: 0,
|
||||
triggerOnce: true,
|
||||
initialInView: true,
|
||||
});
|
||||
|
||||
const [errorMessage, setErrorMessage] = useState<string | undefined>('');
|
||||
|
@ -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,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
|
@ -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} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
{...{
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
/>
|
||||
|
@ -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,
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -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') {
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
8
frontend/src/container/LogsFilters/config.ts
Normal file
8
frontend/src/container/LogsFilters/config.ts
Normal 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] },
|
||||
};
|
@ -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;
|
||||
|
@ -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;
|
||||
|
35
frontend/src/container/LogsFilters/types.ts
Normal file
35
frontend/src/container/LogsFilters/types.ts
Normal 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;
|
||||
}
|
94
frontend/src/container/LogsFilters/utils.ts
Normal file
94
frontend/src/container/LogsFilters/utils.ts
Normal 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),
|
||||
);
|
||||
};
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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));
|
||||
|
@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
|
12
frontend/src/container/LogsSearchFilter/utils.ts
Normal file
12
frontend/src/container/LogsSearchFilter/utils.ts
Normal 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;
|
||||
};
|
@ -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 = [
|
||||
|
@ -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>
|
||||
|
@ -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="%"
|
||||
|
75
frontend/src/container/MetricsApplication/Tabs/util.ts
Normal file
75
frontend/src/container/MetricsApplication/Tabs/util.ts
Normal 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';
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
@ -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"
|
||||
/>
|
||||
);
|
||||
|
@ -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>
|
||||
|
@ -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',
|
||||
|
@ -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
|
||||
|
@ -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 => {
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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}
|
||||
|
@ -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 (
|
||||
|
@ -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', {
|
||||
|
@ -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' }}
|
||||
|
@ -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()}
|
||||
/>
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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'}
|
||||
/>
|
||||
|
@ -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'}
|
||||
/>
|
||||
|
@ -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 don’t 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 don’t 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>
|
||||
);
|
||||
}
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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}>
|
||||
|
@ -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],
|
||||
|
@ -9,13 +9,12 @@ interface Dropdown {
|
||||
export const groupBy: DefaultOptionType[] = [
|
||||
{
|
||||
label: 'None',
|
||||
value: '',
|
||||
value: 'none',
|
||||
},
|
||||
{
|
||||
label: 'Service Name',
|
||||
value: 'serviceName',
|
||||
},
|
||||
|
||||
{
|
||||
label: 'Operation',
|
||||
value: 'name',
|
||||
|
@ -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>
|
||||
|
27
frontend/src/container/Trace/TraceGraphFilter/utils.test.ts
Normal file
27
frontend/src/container/Trace/TraceGraphFilter/utils.test.ts
Normal 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);
|
||||
});
|
||||
});
|
@ -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
|
||||
);
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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
Loading…
x
Reference in New Issue
Block a user