Compare commits

..

2 Commits

Author SHA1 Message Date
Prashant Shahi
e03daa7207 chore(frontend): 🔧 support ARM and copy yarnrc in Dockerfile (#2119)
Signed-off-by: Prashant Shahi <prashant@signoz.io>

Signed-off-by: Prashant Shahi <prashant@signoz.io>
Co-authored-by: Palash Gupta <palashgdev@gmail.com>
(cherry picked from commit 13f9922c53)
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2023-01-26 13:51:48 +05:30
Palash Gupta
c48f72781e fix: yarnrc is added in the root of the frontend (#2114)
Co-authored-by: Prashant Shahi <prashant@signoz.io>
(cherry picked from commit ba8f804b26)
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2023-01-26 13:51:40 +05:30
273 changed files with 18045 additions and 6965 deletions

2
.github/config.yml vendored
View File

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

View File

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

View File

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

25
.github/workflows/repo-stats.yml vendored Normal file
View File

@@ -0,0 +1,25 @@
on:
schedule:
# Run this once per day, towards the end of the day for keeping the most
# recent data point most meaningful (hours are interpreted in UTC).
- cron: "0 8 * * *"
workflow_dispatch: # Allow for running this manually.
jobs:
j1:
name: repostats
runs-on: ubuntu-latest
steps:
- name: run-ghrs
uses: jgehrcke/github-repo-stats@v1.1.0
with:
# Define the stats repository (the repo to fetch
# stats for and to generate the report for).
# Remove the parameter when the stats repository
# and the data repository are the same.
repository: signoz/signoz
# Set a GitHub API token that can read the stats
# repository, and that can push to the data
# repository (which this workflow file lives in),
# to store data and the report files.
ghtoken: ${{ github.token }}

View File

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

View File

@@ -23,7 +23,7 @@
##
SigNoz helps developers monitor applications and troubleshoot problems in their deployed applications. With SigNoz, you can:
SigNoz helps developers monitor applications and troubleshoot problems in their deployed applications. SigNoz uses distributed tracing to gain visibility into your software stack.
👉 Visualise Metrics, Traces and Logs in a single pane of glass

View File

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

View File

@@ -137,7 +137,7 @@ services:
condition: on-failure
query-service:
image: signoz/query-service:0.16.0
image: signoz/query-service:0.14.0
command: ["-config=/root/config/prometheus.yml"]
# ports:
# - "6060:6060" # pprof port
@@ -166,7 +166,7 @@ services:
<<: *clickhouse-depend
frontend:
image: signoz/frontend:0.16.0
image: signoz/frontend:0.14.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.4
image: signoz/signoz-otel-collector:0.66.2
command: ["--config=/etc/otel-collector-config.yaml"]
user: root # required for reading docker container logs
volumes:
@@ -188,7 +188,6 @@ 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
@@ -208,7 +207,7 @@ services:
<<: *clickhouse-depend
otel-collector-metrics:
image: signoz/signoz-otel-collector:0.66.4
image: signoz/signoz-otel-collector:0.66.2
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
volumes:
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml

View File

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

View File

@@ -41,7 +41,7 @@ services:
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
otel-collector:
container_name: otel-collector
image: signoz/signoz-otel-collector:0.66.4
image: signoz/signoz-otel-collector:0.66.2
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.4
image: signoz/signoz-otel-collector:0.66.2
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
volumes:
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml

View File

@@ -114,7 +114,7 @@ services:
# volumes:
# - ./clickhouse-config.xml:/etc/clickhouse-server/config.xml
# - ./clickhouse-users.xml:/etc/clickhouse-server/users.xml
# - ./custom-function.xml:/etc/clickhouse-server/custom-function.xml
# - ./custom-function.xml:/etc/clickhouse-server/custom-function.xml
# - ./clickhouse-cluster.xml:/etc/clickhouse-server/config.d/cluster.xml
# # - ./clickhouse-storage.xml:/etc/clickhouse-server/config.d/storage.xml
# - ./data/clickhouse-2/:/var/lib/clickhouse/
@@ -132,7 +132,7 @@ services:
# volumes:
# - ./clickhouse-config.xml:/etc/clickhouse-server/config.xml
# - ./clickhouse-users.xml:/etc/clickhouse-server/users.xml
# - ./custom-function.xml:/etc/clickhouse-server/custom-function.xml
# - ./custom-function.xml:/etc/clickhouse-server/custom-function.xml
# - ./clickhouse-cluster.xml:/etc/clickhouse-server/config.d/cluster.xml
# # - ./clickhouse-storage.xml:/etc/clickhouse-server/config.d/storage.xml
# - ./data/clickhouse-3/:/var/lib/clickhouse/
@@ -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.16.0}
image: signoz/query-service:${DOCKER_TAG:-0.14.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.16.0}
image: signoz/frontend:${DOCKER_TAG:-0.14.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.4}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.66.2}
command: ["--config=/etc/otel-collector-config.yaml"]
user: root # required for reading docker container logs
volumes:
@@ -202,7 +202,6 @@ 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
@@ -219,7 +218,7 @@ services:
<<: *clickhouse-depend
otel-collector-metrics:
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.66.4}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.66.2}
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
volumes:
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
@@ -232,15 +231,15 @@ services:
<<: *clickhouse-depend
hotrod:
image: jaegertracing/example-hotrod:1.30
container_name: hotrod
logging:
options:
max-size: 50m
max-file: "3"
command: ["all"]
environment:
- JAEGER_ENDPOINT=http://otel-collector:14268/api/traces
image: jaegertracing/example-hotrod:1.30
container_name: hotrod
logging:
options:
max-size: 50m
max-file: "3"
command: ["all"]
environment:
- JAEGER_ENDPOINT=http://otel-collector:14268/api/traces
load-hotrod:
image: "grubykarol/locust:1.2.3-python3.9-alpine3.12"

View File

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

View File

@@ -81,11 +81,6 @@ check_os() {
os="centos"
package_manager="yum"
;;
Rocky*)
desired_os=1
os="centos"
package_manager="yum"
;;
SLES*)
desired_os=1
os="sles"
@@ -516,15 +511,13 @@ else
echo ""
echo -e "🟢 Your frontend is running on http://localhost:3301"
echo ""
echo " By default, retention period is set to 7 days for logs and traces, and 30 days for metrics."
echo -e "To change this, navigate to the General tab on the Settings page of SigNoz UI. For more details, refer to https://signoz.io/docs/userguide/retention-period \n"
echo " To bring down SigNoz and clean volumes : $sudo_cmd docker-compose -f ./docker/clickhouse-setup/docker-compose.yaml down -v"
echo ""
echo "+++++++++++++++++++++++++++++++++++++++++++++++++"
echo ""
echo "👉 Need help in Getting Started?"
echo "👉 Need help Getting Started?"
echo -e "Join us on Slack https://signoz.io/slack"
echo ""
echo -e "\n📨 Please share your email to receive support & updates about SigNoz!"

View File

@@ -30,7 +30,6 @@ import (
"go.signoz.io/signoz/pkg/query-service/healthcheck"
basealm "go.signoz.io/signoz/pkg/query-service/integrations/alertManager"
baseint "go.signoz.io/signoz/pkg/query-service/interfaces"
"go.signoz.io/signoz/pkg/query-service/model"
pqle "go.signoz.io/signoz/pkg/query-service/pqlEngine"
rules "go.signoz.io/signoz/pkg/query-service/rules"
"go.signoz.io/signoz/pkg/query-service/telemetry"
@@ -272,9 +271,8 @@ func (lrw *loggingResponseWriter) Flush() {
func extractDashboardMetaData(path string, r *http.Request) (map[string]interface{}, bool) {
pathToExtractBodyFrom := "/api/v2/metrics/query_range"
var requestBody map[string]interface{}
data := map[string]interface{}{}
var postData *model.QueryRangeParamsV2
if path == pathToExtractBodyFrom && (r.Method == "POST") {
if r.Body != nil {
@@ -284,8 +282,7 @@ func extractDashboardMetaData(path string, r *http.Request) (map[string]interfac
}
r.Body.Close() // must close
r.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))
json.Unmarshal(bodyBytes, &postData)
json.Unmarshal(bodyBytes, &requestBody)
} else {
return nil, false
}
@@ -294,20 +291,31 @@ func extractDashboardMetaData(path string, r *http.Request) (map[string]interfac
return nil, false
}
signozMetricNotFound := false
compositeMetricQuery, compositeMetricQueryExists := requestBody["compositeMetricQuery"]
if postData != nil {
signozMetricNotFound = telemetry.GetInstance().CheckSigNozMetricsV2(postData.CompositeMetricQuery)
signozMetricFound := false
if postData.CompositeMetricQuery != nil {
data["queryType"] = postData.CompositeMetricQuery.QueryType
data["panelType"] = postData.CompositeMetricQuery.PanelType
if compositeMetricQueryExists {
compositeMetricQueryMap := compositeMetricQuery.(map[string]interface{})
signozMetricFound = telemetry.GetInstance().CheckSigNozMetrics(compositeMetricQueryMap)
queryType, queryTypeExists := compositeMetricQueryMap["queryType"]
if queryTypeExists {
data["queryType"] = queryType
}
panelType, panelTypeExists := compositeMetricQueryMap["panelType"]
if panelTypeExists {
data["panelType"] = panelType
}
data["datasource"] = postData.DataSource
}
if signozMetricNotFound {
datasource, datasourceExists := requestBody["dataSource"]
if datasourceExists {
data["datasource"] = datasource
}
if !signozMetricFound {
telemetry.GetInstance().AddActiveMetricsUser()
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_DASHBOARDS_METADATA, data, true)
}

View File

@@ -102,10 +102,9 @@ module.exports = {
},
],
'@typescript-eslint/no-unused-vars': 'error',
'func-style': ['error', 'declaration', { allowArrowFunctions: true }],
'arrow-body-style': ['error', 'as-needed'],
// eslint rules need to remove
'no-shadow': 'off',
'@typescript-eslint/no-shadow': 'off',
'import/no-cycle': 'off',

View File

@@ -1,4 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
cd frontend && yarn run commitlint --edit $1
cd frontend && npm run commitlint

View File

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

View File

@@ -31,6 +31,10 @@
"@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",
@@ -40,7 +44,7 @@
"babel-plugin-named-asset-import": "^0.3.7",
"babel-preset-minify": "^0.5.1",
"babel-preset-react-app": "^10.0.0",
"chart.js": "3.9.1",
"chart.js": "^3.4.0",
"chartjs-adapter-date-fns": "^2.0.0",
"chartjs-plugin-annotation": "^1.4.0",
"color": "^4.2.1",
@@ -66,18 +70,17 @@
"less-loader": "^10.2.0",
"lodash-es": "^4.17.21",
"mini-css-extract-plugin": "2.4.5",
"react": "18.2.0",
"react-dom": "18.2.0",
"react": "17.0.0",
"react-dom": "17.0.0",
"react-force-graph": "^1.41.0",
"react-graph-vis": "^1.0.5",
"react-grid-layout": "^1.3.4",
"react-i18next": "^11.16.1",
"react-intersection-observer": "9.4.1",
"react-query": "^3.34.19",
"react-redux": "^7.2.2",
"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",
@@ -117,9 +120,7 @@
"@commitlint/config-conventional": "^16.2.4",
"@jest/globals": "^27.5.1",
"@playwright/test": "^1.22.0",
"@testing-library/jest-dom": "5.16.5",
"@testing-library/react": "13.4.0",
"@testing-library/user-event": "14.4.3",
"@testing-library/react-hooks": "^7.0.2",
"@types/color": "^3.0.3",
"@types/compression-webpack-plugin": "^9.0.0",
"@types/copy-webpack-plugin": "^8.0.1",
@@ -131,11 +132,10 @@
"@types/lodash-es": "^4.17.4",
"@types/mini-css-extract-plugin": "^2.5.1",
"@types/node": "^16.10.3",
"@types/react": "18.0.26",
"@types/react-dom": "18.0.10",
"@types/react": "^17.0.0",
"@types/react-dom": "^16.9.9",
"@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",
@@ -145,7 +145,6 @@
"@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",
@@ -175,7 +174,6 @@
"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",
@@ -188,7 +186,7 @@
]
},
"resolutions": {
"@types/react": "18.0.26",
"@types/react-dom": "18.0.10"
"@types/react": "17.0.0",
"@types/react-dom": "17.0.0"
}
}

View File

@@ -47,8 +47,6 @@ 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 => {
@@ -108,7 +106,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
} else {
Logout();
notifications.error({
notification.error({
message: response.error || t('something_went_wrong'),
});
}
@@ -157,12 +155,7 @@ 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 (
<>
{NotificationElement}
{children}
</>
);
return <>{children}</>;
}
interface PrivateRouteProps {

View File

@@ -1,4 +1,4 @@
import { ApiV2Instance as axios } from 'api';
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
@@ -8,7 +8,9 @@ const query = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const response = await axios.post(`/variables/query`, props);
const response = await axios.get(
`/variables/query?query=${encodeURIComponent(props.query)}`,
);
return {
statusCode: 200,

View File

@@ -12,9 +12,7 @@ const getSpans = async (
const updatedSelectedTags = props.selectedTags.map((e) => ({
Key: e.Key[0],
Operator: e.Operator,
StringValues: e.StringValues,
NumberValues: e.NumberValues,
BoolValues: e.BoolValues,
Values: e.Values,
}));
const exclude: string[] = [];
@@ -37,7 +35,7 @@ const getSpans = async (
start: String(props.start),
end: String(props.end),
function: props.function,
groupBy: props.groupBy === 'none' ? '' : props.groupBy,
groupBy: props.groupBy,
step: props.step,
tags: updatedSelectedTags,
...nonDuration,

View File

@@ -30,9 +30,7 @@ const getSpanAggregate = async (
const updatedSelectedTags = props.selectedTags.map((e) => ({
Key: e.Key[0],
Operator: e.Operator,
StringValues: e.StringValues,
NumberValues: e.NumberValues,
BoolValues: e.BoolValues,
Values: e.Values,
}));
const other = Object.fromEntries(props.selectedFilter);

View File

@@ -11,11 +11,9 @@ const getTagValue = async (
const response = await axios.post<PayloadProps>(`/getTagValues`, {
start: props.start.toString(),
end: props.end.toString(),
tagKey: {
Key: props.tagKey.Key,
Type: props.tagKey.Type,
},
tagKey: props.tagKey,
});
return {
statusCode: 200,
error: null,

View File

@@ -1,321 +0,0 @@
import { Chart, ChartTypeRegistry, Plugin } from 'chart.js';
import * as ChartHelpers from 'chart.js/helpers';
// utils
import { ChartEventHandler, mergeDefaultOptions } from './utils';
export const dragSelectPluginId = 'drag-select-plugin';
type ChartDragHandlers = {
mousedown: ChartEventHandler;
mousemove: ChartEventHandler;
mouseup: ChartEventHandler;
globalMouseup: () => void;
};
export type DragSelectPluginOptions = {
color?: string;
onSelect?: (startValueX: number, endValueX: number) => void;
};
const defaultDragSelectPluginOptions: Required<DragSelectPluginOptions> = {
color: 'rgba(0, 0, 0, 0.5)',
onSelect: () => {},
};
export function createDragSelectPluginOptions(
isEnabled: boolean,
onSelect?: (start: number, end: number) => void,
color?: string,
): DragSelectPluginOptions | false {
if (!isEnabled) {
return false;
}
return {
onSelect,
color,
};
}
function createMousedownHandler(
chart: Chart,
dragData: DragSelectData,
): ChartEventHandler {
return (ev): void => {
const { left, right } = chart.chartArea;
let { x: startDragPositionX } = ChartHelpers.getRelativePosition(ev, chart);
if (left > startDragPositionX) {
startDragPositionX = left;
}
if (right < startDragPositionX) {
startDragPositionX = right;
}
const startValuePositionX = chart.scales.x.getValueForPixel(
startDragPositionX,
);
dragData.onDragStart(startDragPositionX, startValuePositionX);
};
}
function createMousemoveHandler(
chart: Chart,
dragData: DragSelectData,
): ChartEventHandler {
return (ev): void => {
if (!dragData.isMouseDown) {
return;
}
const { left, right } = chart.chartArea;
let { x: dragPositionX } = ChartHelpers.getRelativePosition(ev, chart);
if (left > dragPositionX) {
dragPositionX = left;
}
if (right < dragPositionX) {
dragPositionX = right;
}
const valuePositionX = chart.scales.x.getValueForPixel(dragPositionX);
dragData.onDrag(dragPositionX, valuePositionX);
chart.update('none');
};
}
function createMouseupHandler(
chart: Chart,
options: DragSelectPluginOptions,
dragData: DragSelectData,
): ChartEventHandler {
return (ev): void => {
const { left, right } = chart.chartArea;
let { x: endRelativePostionX } = ChartHelpers.getRelativePosition(ev, chart);
if (left > endRelativePostionX) {
endRelativePostionX = left;
}
if (right < endRelativePostionX) {
endRelativePostionX = right;
}
const endValuePositionX = chart.scales.x.getValueForPixel(
endRelativePostionX,
);
dragData.onDragEnd(endRelativePostionX, endValuePositionX);
chart.update('none');
if (
typeof options.onSelect === 'function' &&
typeof dragData.startValuePositionX === 'number' &&
typeof dragData.endValuePositionX === 'number'
) {
const start = Math.min(
dragData.startValuePositionX,
dragData.endValuePositionX,
);
const end = Math.max(
dragData.startValuePositionX,
dragData.endValuePositionX,
);
options.onSelect(start, end);
}
};
}
function createGlobalMouseupHandler(
options: DragSelectPluginOptions,
dragData: DragSelectData,
): () => void {
return (): void => {
const { isDragging, endRelativePixelPositionX, endValuePositionX } = dragData;
if (!isDragging) {
return;
}
dragData.onDragEnd(
endRelativePixelPositionX as number,
endValuePositionX as number,
);
if (
typeof options.onSelect === 'function' &&
typeof dragData.startValuePositionX === 'number' &&
typeof dragData.endValuePositionX === 'number'
) {
const start = Math.min(
dragData.startValuePositionX,
dragData.endValuePositionX,
);
const end = Math.max(
dragData.startValuePositionX,
dragData.endValuePositionX,
);
options.onSelect(start, end);
}
};
}
class DragSelectData {
public isDragging = false;
public isMouseDown = false;
public startRelativePixelPositionX: number | null = null;
public startValuePositionX: number | null | undefined = null;
public endRelativePixelPositionX: number | null = null;
public endValuePositionX: number | null | undefined = null;
public initialize(): void {
this.isDragging = false;
this.isMouseDown = false;
this.startRelativePixelPositionX = null;
this.startValuePositionX = null;
this.endRelativePixelPositionX = null;
this.endValuePositionX = null;
}
public onDragStart(
startRelativePixelPositionX: number,
startValuePositionX: number | undefined,
): void {
this.isDragging = false;
this.isMouseDown = true;
this.startRelativePixelPositionX = startRelativePixelPositionX;
this.startValuePositionX = startValuePositionX;
this.endRelativePixelPositionX = null;
this.endValuePositionX = null;
}
public onDrag(
endRelativePixelPositionX: number,
endValuePositionX: number | undefined,
): void {
this.isDragging = true;
this.endRelativePixelPositionX = endRelativePixelPositionX;
this.endValuePositionX = endValuePositionX;
}
public onDragEnd(
endRelativePixelPositionX: number,
endValuePositionX: number | undefined,
): void {
if (!this.isDragging) {
this.initialize();
return;
}
this.isDragging = false;
this.isMouseDown = false;
this.endRelativePixelPositionX = endRelativePixelPositionX;
this.endValuePositionX = endValuePositionX;
}
}
export const createDragSelectPlugin = (): Plugin<
keyof ChartTypeRegistry,
DragSelectPluginOptions
> => {
const dragData = new DragSelectData();
let pluginOptions: Required<DragSelectPluginOptions>;
const handlers: ChartDragHandlers = {
mousedown: () => {},
mousemove: () => {},
mouseup: () => {},
globalMouseup: () => {},
};
const dragSelectPlugin: Plugin<
keyof ChartTypeRegistry,
DragSelectPluginOptions
> = {
id: dragSelectPluginId,
start: (chart: Chart, _, passedOptions) => {
pluginOptions = mergeDefaultOptions(
passedOptions,
defaultDragSelectPluginOptions,
);
const { canvas } = chart;
dragData.initialize();
const mousedownHandler = createMousedownHandler(chart, dragData);
const mousemoveHandler = createMousemoveHandler(chart, dragData);
const mouseupHandler = createMouseupHandler(chart, pluginOptions, dragData);
const globalMouseupHandler = createGlobalMouseupHandler(
pluginOptions,
dragData,
);
canvas.addEventListener('mousedown', mousedownHandler, { passive: true });
canvas.addEventListener('mousemove', mousemoveHandler, { passive: true });
canvas.addEventListener('mouseup', mouseupHandler, { passive: true });
document.addEventListener('mouseup', globalMouseupHandler, {
passive: true,
});
handlers.mousedown = mousedownHandler;
handlers.mousemove = mousemoveHandler;
handlers.mouseup = mouseupHandler;
handlers.globalMouseup = globalMouseupHandler;
},
beforeDestroy: (chart: Chart) => {
const { canvas } = chart;
if (!canvas) {
return;
}
canvas.removeEventListener('mousedown', handlers.mousedown);
canvas.removeEventListener('mousemove', handlers.mousemove);
canvas.removeEventListener('mouseup', handlers.mouseup);
document.removeEventListener('mouseup', handlers.globalMouseup);
},
afterDatasetsDraw: (chart: Chart) => {
const {
startRelativePixelPositionX,
endRelativePixelPositionX,
isDragging,
} = dragData;
if (startRelativePixelPositionX && endRelativePixelPositionX && isDragging) {
const left = Math.min(
startRelativePixelPositionX,
endRelativePixelPositionX,
);
const right = Math.max(
startRelativePixelPositionX,
endRelativePixelPositionX,
);
const top = chart.chartArea.top - 5;
const bottom = chart.chartArea.bottom + 5;
/* eslint-disable-next-line no-param-reassign */
chart.ctx.fillStyle = pluginOptions.color;
chart.ctx.fillRect(left, top, right - left, bottom - top);
}
},
};
return dragSelectPlugin;
};

View File

@@ -1,164 +0,0 @@
import { Chart, ChartEvent, ChartTypeRegistry, Plugin } from 'chart.js';
import * as ChartHelpers from 'chart.js/helpers';
// utils
import { ChartEventHandler, mergeDefaultOptions } from './utils';
export const intersectionCursorPluginId = 'intersection-cursor-plugin';
export type IntersectionCursorPluginOptions = {
color?: string;
dashSize?: number;
gapSize?: number;
};
export const defaultIntersectionCursorPluginOptions: Required<IntersectionCursorPluginOptions> = {
color: 'white',
dashSize: 3,
gapSize: 3,
};
export function createIntersectionCursorPluginOptions(
isEnabled: boolean,
color?: string,
dashSize?: number,
gapSize?: number,
): IntersectionCursorPluginOptions | false {
if (!isEnabled) {
return false;
}
return {
color,
dashSize,
gapSize,
};
}
function createMousemoveHandler(
chart: Chart,
cursorData: IntersectionCursorData,
): ChartEventHandler {
return (ev: ChartEvent | MouseEvent): void => {
const { left, right, top, bottom } = chart.chartArea;
let { x, y } = ChartHelpers.getRelativePosition(ev, chart);
if (left > x) {
x = left;
}
if (right < x) {
x = right;
}
if (y < top) {
y = top;
}
if (y > bottom) {
y = bottom;
}
cursorData.onMouseMove(x, y);
};
}
function createMouseoutHandler(
cursorData: IntersectionCursorData,
): ChartEventHandler {
return (): void => {
cursorData.onMouseOut();
};
}
class IntersectionCursorData {
public positionX: number | null | undefined;
public positionY: number | null | undefined;
public initialize(): void {
this.positionX = null;
this.positionY = null;
}
public onMouseMove(x: number | undefined, y: number | undefined): void {
this.positionX = x;
this.positionY = y;
}
public onMouseOut(): void {
this.positionX = null;
this.positionY = null;
}
}
export const createIntersectionCursorPlugin = (): Plugin<
keyof ChartTypeRegistry,
IntersectionCursorPluginOptions
> => {
const cursorData = new IntersectionCursorData();
let pluginOptions: Required<IntersectionCursorPluginOptions>;
let mousemoveHandler: (ev: ChartEvent | MouseEvent) => void;
let mouseoutHandler: (ev: ChartEvent | MouseEvent) => void;
const intersectionCursorPlugin: Plugin<
keyof ChartTypeRegistry,
IntersectionCursorPluginOptions
> = {
id: intersectionCursorPluginId,
start: (chart: Chart, _, passedOptions) => {
const { canvas } = chart;
cursorData.initialize();
pluginOptions = mergeDefaultOptions(
passedOptions,
defaultIntersectionCursorPluginOptions,
);
mousemoveHandler = createMousemoveHandler(chart, cursorData);
mouseoutHandler = createMouseoutHandler(cursorData);
canvas.addEventListener('mousemove', mousemoveHandler, { passive: true });
canvas.addEventListener('mouseout', mouseoutHandler, { passive: true });
},
beforeDestroy: (chart: Chart) => {
const { canvas } = chart;
if (!canvas) {
return;
}
canvas.removeEventListener('mousemove', mousemoveHandler);
canvas.removeEventListener('mouseout', mouseoutHandler);
},
afterDatasetsDraw: (chart: Chart) => {
const { positionX, positionY } = cursorData;
const lineDashData = [pluginOptions.dashSize, pluginOptions.gapSize];
if (typeof positionX === 'number' && typeof positionY === 'number') {
const { top, bottom, left, right } = chart.chartArea;
chart.ctx.beginPath();
/* eslint-disable-next-line no-param-reassign */
chart.ctx.strokeStyle = pluginOptions.color;
chart.ctx.setLineDash(lineDashData);
chart.ctx.moveTo(left, positionY);
chart.ctx.lineTo(right, positionY);
chart.ctx.stroke();
chart.ctx.beginPath();
chart.ctx.setLineDash(lineDashData);
/* eslint-disable-next-line no-param-reassign */
chart.ctx.strokeStyle = pluginOptions.color;
chart.ctx.moveTo(positionX, top);
chart.ctx.lineTo(positionX, bottom);
chart.ctx.stroke();
}
},
};
return intersectionCursorPlugin;
};

View File

@@ -22,86 +22,87 @@ const getOrCreateLegendList = (
listContainer.style.height = '100%';
listContainer.style.flexWrap = 'wrap';
listContainer.style.justifyContent = 'center';
listContainer.style.fontSize = '0.75rem';
legendContainer?.appendChild(listContainer);
}
return listContainer;
};
export const legend = (id: string, isLonger: boolean): Plugin<ChartType> => ({
id: 'htmlLegend',
afterUpdate(chart): void {
const ul = getOrCreateLegendList(chart, id || 'legend', isLonger);
export const legend = (id: string, isLonger: boolean): Plugin<ChartType> => {
return {
id: 'htmlLegend',
afterUpdate(chart): void {
const ul = getOrCreateLegendList(chart, id || 'legend', isLonger);
// Remove old legend items
while (ul.firstChild) {
ul.firstChild.remove();
}
// Reuse the built-in legendItems generator
const items = get(chart, [
'options',
'plugins',
'legend',
'labels',
'generateLabels',
])
? get(chart, ['options', 'plugins', 'legend', 'labels', 'generateLabels'])(
chart,
)
: null;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
items?.forEach((item: Record<any, any>, index: number) => {
const li = document.createElement('li');
li.style.alignItems = 'center';
li.style.cursor = 'pointer';
li.style.display = 'flex';
li.style.marginLeft = '10px';
// li.style.marginTop = '5px';
li.onclick = (): void => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const { type } = chart.config;
if (type === 'pie' || type === 'doughnut') {
// Pie and doughnut charts only have a single dataset and visibility is per item
chart.toggleDataVisibility(index);
} else {
chart.setDatasetVisibility(
item.datasetIndex,
!chart.isDatasetVisible(item.datasetIndex),
);
}
chart.update();
};
// Color box
const boxSpan = document.createElement('span');
boxSpan.style.background = `${item.strokeStyle}` || `${colors[0]}`;
boxSpan.style.borderColor = `${item?.strokeStyle}`;
boxSpan.style.borderWidth = `${item.lineWidth}px`;
boxSpan.style.display = 'inline-block';
boxSpan.style.minHeight = '0.75rem';
boxSpan.style.marginRight = '0.5rem';
boxSpan.style.minWidth = '0.75rem';
boxSpan.style.borderRadius = '50%';
if (item.text) {
// Text
const textContainer = document.createElement('span');
textContainer.style.margin = '0';
textContainer.style.padding = '0';
textContainer.style.textDecoration = item.hidden ? 'line-through' : '';
const text = document.createTextNode(item.text);
textContainer.appendChild(text);
li.appendChild(boxSpan);
li.appendChild(textContainer);
ul.appendChild(li);
// Remove old legend items
while (ul.firstChild) {
ul.firstChild.remove();
}
});
},
});
// Reuse the built-in legendItems generator
const items = get(chart, [
'options',
'plugins',
'legend',
'labels',
'generateLabels',
])
? get(chart, ['options', 'plugins', 'legend', 'labels', 'generateLabels'])(
chart,
)
: null;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
items?.forEach((item: Record<any, any>, index: number) => {
const li = document.createElement('li');
li.style.alignItems = 'center';
li.style.cursor = 'pointer';
li.style.display = 'flex';
li.style.marginLeft = '10px';
li.style.marginTop = '5px';
li.onclick = (): void => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const { type } = chart.config;
if (type === 'pie' || type === 'doughnut') {
// Pie and doughnut charts only have a single dataset and visibility is per item
chart.toggleDataVisibility(index);
} else {
chart.setDatasetVisibility(
item.datasetIndex,
!chart.isDatasetVisible(item.datasetIndex),
);
}
chart.update();
};
// Color box
const boxSpan = document.createElement('span');
boxSpan.style.background = `${item.strokeStyle}` || `${colors[0]}`;
boxSpan.style.borderColor = `${item?.strokeStyle}`;
boxSpan.style.borderWidth = `${item.lineWidth}px`;
boxSpan.style.display = 'inline-block';
boxSpan.style.minHeight = '20px';
boxSpan.style.marginRight = '10px';
boxSpan.style.minWidth = '20px';
boxSpan.style.borderRadius = '50%';
if (item.text) {
// Text
const textContainer = document.createElement('span');
textContainer.style.margin = '0';
textContainer.style.padding = '0';
textContainer.style.textDecoration = item.hidden ? 'line-through' : '';
const text = document.createTextNode(item.text);
textContainer.appendChild(text);
li.appendChild(boxSpan);
li.appendChild(textContainer);
ul.appendChild(li);
}
});
},
};
};

View File

@@ -1,20 +0,0 @@
import { ChartEvent } from 'chart.js';
export type ChartEventHandler = (ev: ChartEvent | MouseEvent) => void;
export function mergeDefaultOptions<T extends Record<string, unknown>>(
options: T,
defaultOptions: Required<T>,
): Required<T> {
const sanitizedOptions = { ...options };
Object.keys(options).forEach((key) => {
if (sanitizedOptions[key as keyof T] === undefined) {
delete sanitizedOptions[key as keyof T];
}
});
return {
...defaultOptions,
...sanitizedOptions,
};
}

View File

@@ -1,8 +0,0 @@
import { themeColors } from 'constants/theme';
export const getAxisLabelColor = (currentTheme: string): string => {
if (currentTheme === 'light') {
return themeColors.black;
}
return themeColors.whiteCream;
};

View File

@@ -23,27 +23,12 @@ 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 isEqual from 'lodash-es/isEqual';
import React, { memo, useCallback, useEffect, useRef } from 'react';
import React, { useCallback, useEffect, useRef } from 'react';
import { hasData } from './hasData';
import { getAxisLabelColor } from './helpers';
import { legend } from './Plugin';
import {
createDragSelectPlugin,
createDragSelectPluginOptions,
dragSelectPluginId,
DragSelectPluginOptions,
} from './Plugin/DragSelect';
import { emptyGraph } from './Plugin/EmptyGraph';
import {
createIntersectionCursorPlugin,
createIntersectionCursorPluginOptions,
intersectionCursorPluginId,
IntersectionCursorPluginOptions,
} from './Plugin/IntersectionCursor';
import { LegendsContainer } from './styles';
import { useXAxisTimeUnit } from './xAxisConfig';
import { getToolTipValue, getYAxisFormattedValue } from './yAxisConfig';
@@ -79,10 +64,7 @@ function Graph({
forceReRender,
staticLine,
containerHeight,
onDragSelect,
dragSelectColor,
}: GraphProps): JSX.Element {
const nearestDatasetIndex = useRef<null | number>(null);
const chartRef = useRef<HTMLCanvasElement>(null);
const isDarkMode = useIsDarkMode();
@@ -109,7 +91,7 @@ function Graph({
}
if (chartRef.current !== null) {
const options: CustomChartOptions = {
const options: ChartOptions = {
animation: {
duration: animate ? 200 : 0,
},
@@ -153,10 +135,6 @@ 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 || '';
@@ -166,27 +144,10 @@ 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(
!!onDragSelect,
onDragSelect,
dragSelectColor,
),
[intersectionCursorPluginId]: createIntersectionCursorPluginOptions(
!!onDragSelect,
currentTheme === 'dark' ? 'white' : 'black',
),
},
layout: {
padding: 0,
@@ -216,7 +177,6 @@ function Graph({
},
},
type: 'time',
ticks: { color: getAxisLabelColor(currentTheme) },
},
y: {
display: true,
@@ -225,7 +185,6 @@ function Graph({
color: getGridColor(),
},
ticks: {
color: getAxisLabelColor(currentTheme),
// Include a dollar sign in the ticks
callback(value) {
return getYAxisFormattedValue(value.toString(), yAxisUnit);
@@ -241,50 +200,18 @@ 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);
const chartPlugins = [];
if (chartHasData) {
chartPlugins.push(createIntersectionCursorPlugin());
chartPlugins.push(createDragSelectPlugin());
} else {
chartPlugins.push(emptyGraph);
}
if (!chartHasData) chartPlugins.push(emptyGraph);
chartPlugins.push(legend(name, data.datasets.length > 3));
lineChartRef.current = new Chart(chartRef.current, {
@@ -307,9 +234,6 @@ function Graph({
yAxisUnit,
onClickHandler,
staticLine,
onDragSelect,
dragSelectColor,
currentTheme,
]);
useEffect(() => {
@@ -324,13 +248,6 @@ function Graph({
);
}
type CustomChartOptions = ChartOptions & {
plugins: {
[dragSelectPluginId]: DragSelectPluginOptions | false;
[intersectionCursorPluginId]: IntersectionCursorPluginOptions | false;
};
};
interface GraphProps {
animate?: boolean;
type: ChartType;
@@ -343,8 +260,6 @@ interface GraphProps {
forceReRender?: boolean | null | number;
staticLine?: StaticLineProps | undefined;
containerHeight?: string | number;
onDragSelect?: (start: number, end: number) => void;
dragSelectColor?: string;
}
export interface StaticLineProps {
@@ -371,11 +286,6 @@ Graph.defaultProps = {
yAxisUnit: undefined,
forceReRender: undefined,
staticLine: undefined,
containerHeight: '90%',
onDragSelect: undefined,
dragSelectColor: undefined,
containerHeight: '85%',
};
export default memo(Graph, (prevProps, nextProps) =>
isEqual(prevProps.data, nextProps.data),
);
export default Graph;

View File

@@ -1,22 +1,14 @@
import { themeColors } from 'constants/theme';
import styled from 'styled-components';
export const LegendsContainer = styled.div`
height: 10%;
height: 15%;
* {
::-webkit-scrollbar {
width: 0.5rem;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: ${themeColors.royalGrey};
border-radius: 0.625rem;
}
::-webkit-scrollbar-thumb:hover {
background: ${themeColors.matterhornGrey};
display: none !important;
}
-ms-overflow-style: none !important; /* IE and Edge */
scrollbar-width: none !important; /* Firefox */
}
`;

View File

@@ -2,9 +2,7 @@ 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';
@@ -16,7 +14,7 @@ function AddToQueryHOC({
const {
searchFilter: { queryString },
} = useSelector<AppState, ILogsReducer>((store) => store.logs);
const dispatch = useDispatch<Dispatch<AppActions>>();
const dispatch = useDispatch();
const generatedQuery = useMemo(
() => generateFilterQuery({ fieldKey, fieldValue, type: 'IN' }),
@@ -33,9 +31,7 @@ function AddToQueryHOC({
}
dispatch({
type: SET_SEARCH_QUERY_STRING,
payload: {
searchQueryString: updatedQueryString,
},
payload: updatedQueryString,
});
}, [dispatch, generatedQuery, queryString]);

View File

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

View File

@@ -1,6 +1,6 @@
import { blue, grey, orange } from '@ant-design/colors';
import { CopyFilled, ExpandAltOutlined } from '@ant-design/icons';
import { Button, Divider, notification, Row, Typography } from 'antd';
import { Button, Divider, Row, Typography } from 'antd';
import { map } from 'd3';
import dayjs from 'dayjs';
import { FlatLogData } from 'lib/logs/flatLogData';
@@ -14,7 +14,7 @@ import { ILogsReducer } from 'types/reducer/logs';
import AddToQueryHOC from '../AddToQueryHOC';
import CopyClipboardHOC from '../CopyClipboardHOC';
import { Container, LogContainer, Text, TextContainer } from './styles';
import { Container, Text, TextContainer } from './styles';
import { isValidLogField } from './util';
interface LogFieldProps {
@@ -79,7 +79,6 @@ 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({
@@ -90,41 +89,40 @@ function LogItem({ logData }: LogItemProps): JSX.Element {
const handleCopyJSON = (): void => {
setCopy(JSON.stringify(logData, null, 2));
notifications.success({
message: 'Copied to clipboard',
});
};
return (
<Container>
{NotificationElement}
<div>
<div style={{ maxWidth: '100%' }}>
<div>
{'{'}
<LogContainer>
<>
<LogGeneralField fieldKey="log" fieldValue={flattenLogData.body} />
{flattenLogData.stream && (
<LogGeneralField fieldKey="stream" fieldValue={flattenLogData.stream} />
)}
<div style={{ marginLeft: '0.5rem' }}>
<LogGeneralField
fieldKey="log"
fieldValue={flattenLogData.body as never}
/>
{flattenLogData.stream && (
<LogGeneralField
fieldKey="timestamp"
fieldValue={dayjs((flattenLogData.timestamp as never) / 1e6).format()}
fieldKey="stream"
fieldValue={flattenLogData.stream as never}
/>
</>
</LogContainer>
)}
<LogGeneralField
fieldKey="timestamp"
fieldValue={dayjs((flattenLogData.timestamp as never) / 1e6).format()}
/>
</div>
{'}'}
</div>
<div>
{map(selected, (field) =>
isValidLogField(flattenLogData[field.name] as never) ? (
{map(selected, (field) => {
return isValidLogField(flattenLogData[field.name] as never) ? (
<LogSelectedField
key={field.name}
fieldKey={field.name}
fieldValue={flattenLogData[field.name] as never}
/>
) : null,
)}
) : null;
})}
</div>
</div>
<Divider style={{ padding: 0, margin: '0.4rem 0', opacity: 0.5 }} />

View File

@@ -29,7 +29,3 @@ export const TextContainer = styled.div`
overflow: hidden;
width: 100%;
`;
export const LogContainer = styled.div`
margin-left: 0.5rem;
`;

View File

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

View File

@@ -48,9 +48,9 @@ function ReleaseNote({ path }: ReleaseNoteProps): JSX.Element | null {
(state) => state.app,
);
const c = allComponentMap.find((item) =>
item.match(path, currentVersion, userFlags),
);
const c = allComponentMap.find((item) => {
return item.match(path, currentVersion, userFlags);
});
if (!c) {
return null;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,9 +4,9 @@ import React from 'react';
import { SpinerStyle } from './styles';
function Spinner({ size, tip, height, style }: SpinnerProps): JSX.Element {
function Spinner({ size, tip, height }: SpinnerProps): JSX.Element {
return (
<SpinerStyle height={height} style={style}>
<SpinerStyle height={height}>
<Spin spinning size={size} tip={tip} indicator={<LoadingOutlined spin />} />
</SpinerStyle>
);
@@ -16,13 +16,11 @@ interface SpinnerProps {
size?: SpinProps['size'];
tip?: SpinProps['tip'];
height?: React.CSSProperties['height'];
style?: React.CSSProperties;
}
Spinner.defaultProps = {
size: undefined,
tip: undefined,
height: undefined,
style: {},
};
export default Spinner;

View File

@@ -6,16 +6,18 @@ import React from 'react';
function TextToolTip({ text, url }: TextToolTipProps): JSX.Element {
return (
<Tooltip
overlay={(): JSX.Element => (
<div>
{`${text} `}
{url && (
<a href={url} rel="noopener noreferrer" target="_blank">
here
</a>
)}
</div>
)}
overlay={(): JSX.Element => {
return (
<div>
{`${text} `}
{url && (
<a href={url} rel="noopener noreferrer" target="_blank">
here
</a>
)}
</div>
);
}}
>
<QuestionCircleFilled style={{ fontSize: '1.3125rem' }} />
</Tooltip>

View File

@@ -8,6 +8,7 @@ export const TextContainer = styled.div<TextContainerProps>`
display: flex;
> button {
margin-left: ${({ noButtonMargin }): string =>
noButtonMargin ? '0' : '0.5rem'}
margin-left: ${({ noButtonMargin }): string => {
return noButtonMargin ? '0' : '0.5rem';
}}
`;

View File

@@ -8,11 +8,11 @@ export const OperatorConversions: Array<{
{
label: 'IN',
metricValue: '=~',
traceValue: 'In',
traceValue: 'in',
},
{
label: 'Not IN',
metricValue: '!~',
traceValue: 'NotIn',
traceValue: 'not in',
},
];

View File

@@ -1,43 +0,0 @@
const themeColors = {
chartcolors: {
dodgerBlue: '#2F80ED',
mediumOrchid: '#BB6BD9',
seaBuckthorn: '#F2994A',
seaGreen: '#219653',
turquoiseBlue: '#56CCF2',
festivalOrange: '#F2C94C',
silver: '#BDBDBD',
outrageousOrange: '#FF6633',
roseBud: '#FFB399',
magentaPink: '#FF33FF',
canary: '#FFFF99',
deepSkyBlue: '#00B3E6',
goldTips: '#E6B333',
royalBlue: '#3366E6',
avocado: '#999966',
mintGreen: '#99FF99',
chestnut: '#B34D4D',
lima: '#80B300',
olive: '#809900',
beautyBush: '#E6B3B3',
danube: '#6680B3',
oliveDrab: '#66991A',
lavenderRose: '#FF99E6',
electricLime: '#CCFF1A',
radicalRed: '#FF1A66',
harleyOrange: '#E6331A',
turquoise: '#33FFCC',
gladeGreen: '#66994D',
hemlock: '#66664D',
vidaLoca: '#4D8000',
rust: '#B33300',
},
errorColor: '#d32f2f',
royalGrey: '#888888',
matterhornGrey: '#555555',
whiteCream: '#ffffffd5',
black: '#000000',
lightgrey: '#ddd',
};
export { themeColors };

View File

@@ -1,7 +1,6 @@
/* eslint-disable react/display-name */
import { Button, notification } from 'antd';
import { Button, notification, Table } 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';
@@ -35,13 +34,11 @@ 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,
},
];
@@ -51,7 +48,6 @@ 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">
@@ -66,7 +62,8 @@ function AlertChannels({ allChannels }: AlertChannelsProps): JSX.Element {
return (
<>
{Element}
<ResizeTable columns={columns} dataSource={channels} rowKey="id" />
<Table rowKey="id" dataSource={channels} columns={columns} />
</>
);
}

View File

@@ -5,17 +5,16 @@ import {
Input,
notification,
Space,
Table,
TableProps,
Tooltip,
Typography,
} from 'antd';
import { ColumnType, TablePaginationConfig } from 'antd/es/table';
import { FilterValue, SorterResult } from 'antd/es/table/interface';
import { ColumnType } from 'antd/es/table';
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';
@@ -31,7 +30,6 @@ import { ErrorResponse, SuccessResponse } from 'types/api';
import { Exception, PayloadProps } from 'types/api/errors/getAll';
import { GlobalReducer } from 'types/reducer/globalTime';
import { FilterDropdownExtendsProps } from './types';
import {
extractFilterValues,
getDefaultFilterValue,
@@ -127,15 +125,14 @@ function AllErrors(): JSX.Element {
enabled: !loading,
},
]);
const [notifications, NotificationElement] = notification.useNotification();
useEffect(() => {
if (data?.error) {
notifications.error({
notification.error({
message: data.error || t('something_went_wrong'),
});
}
}, [data?.error, data?.payload, t, notifications]);
}, [data?.error, data?.payload, t]);
const getDateValue = (value: string): JSX.Element => (
<Typography>{dayjs(value).format('DD/MM/YYYY HH:mm:ss A')}</Typography>
@@ -179,45 +176,41 @@ function AllErrors(): JSX.Element {
);
const filterDropdownWrapper = useCallback(
({
setSelectedKeys,
selectedKeys,
confirm,
placeholder,
filterKey,
}: FilterDropdownExtendsProps) => (
<Card size="small">
<Space align="start" direction="vertical">
<Input
placeholder={placeholder}
value={selectedKeys[0]}
onChange={(e): void =>
setSelectedKeys(e.target.value ? [e.target.value] : [])
}
allowClear
defaultValue={getDefaultFilterValue(
filterKey,
getUpdatedServiceName,
getUpdatedExceptionType,
)}
onPressEnter={handleSearch(confirm, String(selectedKeys[0]), filterKey)}
/>
<Button
type="primary"
onClick={handleSearch(confirm, String(selectedKeys[0]), filterKey)}
icon={<SearchOutlined />}
size="small"
>
Search
</Button>
</Space>
</Card>
),
({ setSelectedKeys, selectedKeys, confirm, placeholder, filterKey }) => {
return (
<Card size="small">
<Space align="start" direction="vertical">
<Input
placeholder={placeholder}
value={selectedKeys[0]}
onChange={(e): void =>
setSelectedKeys(e.target.value ? [e.target.value] : [])
}
allowClear
defaultValue={getDefaultFilterValue(
filterKey,
getUpdatedServiceName,
getUpdatedExceptionType,
)}
onPressEnter={handleSearch(confirm, selectedKeys[0], filterKey)}
/>
<Button
type="primary"
onClick={handleSearch(confirm, selectedKeys[0], filterKey)}
icon={<SearchOutlined />}
size="small"
>
Search
</Button>
</Space>
</Card>
);
},
[getUpdatedExceptionType, getUpdatedServiceName, handleSearch],
);
const onExceptionTypeFilter: ColumnType<Exception>['onFilter'] = useCallback(
(value: unknown, record: Exception): boolean => {
const onExceptionTypeFilter = useCallback(
(value, record: Exception): boolean => {
if (record.exceptionType && typeof value === 'string') {
return record.exceptionType.toLowerCase().includes(value.toLowerCase());
}
@@ -227,7 +220,7 @@ function AllErrors(): JSX.Element {
);
const onApplicationTypeFilter = useCallback(
(value: unknown, record: Exception): boolean => {
(value, record: Exception): boolean => {
if (record.serviceName && typeof value === 'string') {
return record.serviceName.toLowerCase().includes(value.toLowerCase());
}
@@ -259,7 +252,6 @@ function AllErrors(): JSX.Element {
const columns: ColumnsType<Exception> = [
{
title: 'Exception Type',
width: 100,
dataIndex: 'exceptionType',
key: 'exceptionType',
...getFilter(onExceptionTypeFilter, 'Search By Exception', 'exceptionType'),
@@ -285,7 +277,6 @@ function AllErrors(): JSX.Element {
title: 'Error Message',
dataIndex: 'exceptionMessage',
key: 'exceptionMessage',
width: 100,
render: (value): JSX.Element => (
<Tooltip overlay={(): JSX.Element => value}>
<Typography.Paragraph
@@ -300,7 +291,6 @@ function AllErrors(): JSX.Element {
},
{
title: 'Count',
width: 50,
dataIndex: 'exceptionCount',
key: 'exceptionCount',
sorter: true,
@@ -313,7 +303,6 @@ function AllErrors(): JSX.Element {
{
title: 'Last Seen',
dataIndex: 'lastSeen',
width: 80,
key: 'lastSeen',
render: getDateValue,
sorter: true,
@@ -326,7 +315,6 @@ function AllErrors(): JSX.Element {
{
title: 'First Seen',
dataIndex: 'firstSeen',
width: 80,
key: 'firstSeen',
render: getDateValue,
sorter: true,
@@ -339,7 +327,6 @@ function AllErrors(): JSX.Element {
{
title: 'Application',
dataIndex: 'serviceName',
width: 100,
key: 'serviceName',
sorter: true,
defaultSortOrder: getDefaultOrder(
@@ -356,11 +343,7 @@ function AllErrors(): JSX.Element {
];
const onChangeHandler: TableProps<Exception>['onChange'] = useCallback(
(
paginations: TablePaginationConfig,
filters: Record<string, FilterValue | null>,
sorter: SorterResult<Exception>[] | SorterResult<Exception>,
) => {
(paginations, filters, sorter) => {
if (!Array.isArray(sorter)) {
const { pageSize = 0, current = 0 } = paginations;
const { columnKey = '', order } = sorter;
@@ -386,24 +369,21 @@ function AllErrors(): JSX.Element {
);
return (
<>
{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}
/>
</>
<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}
/>
);
}

View File

@@ -1,9 +0,0 @@
import { FilterDropdownProps } from 'antd/es/table/interface';
export interface FilterDropdownExtendsProps {
placeholder: string;
filterKey: string;
confirm: FilterDropdownProps['confirm'];
setSelectedKeys: FilterDropdownProps['setSelectedKeys'];
selectedKeys: FilterDropdownProps['selectedKeys'];
}

View File

@@ -20,14 +20,15 @@ export const urlKey = {
serviceName: 'serviceName',
};
export const isOrderParams = (orderBy: string | null): orderBy is OrderBy =>
!!(
export const isOrderParams = (orderBy: string | null): orderBy is OrderBy => {
return !!(
orderBy === 'serviceName' ||
orderBy === 'exceptionCount' ||
orderBy === 'lastSeen' ||
orderBy === 'firstSeen' ||
orderBy === 'exceptionType'
);
};
export const getOrder = (order: string | null): Order => {
if (isOrder(order)) {
@@ -81,9 +82,12 @@ export const getDefaultOrder = (
return undefined;
};
export const getNanoSeconds = (date: string): string =>
Math.floor(new Date(date).getTime() / 1e3).toString() +
String(Timestamp.fromString(date).getNano().toString()).padStart(9, '0');
export const getNanoSeconds = (date: string): string => {
return (
Math.floor(new Date(date).getTime() / 1e3).toString() +
String(Timestamp.fromString(date).getNano().toString()).padStart(9, '0')
);
};
export const getUpdatePageSize = (pageSize: string | null): number => {
if (pageSize) {

View File

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

View File

@@ -78,17 +78,16 @@ function CreateAlertChannels({
[type, selectedConfig],
);
const prepareSlackRequest = useCallback(
() => ({
const prepareSlackRequest = useCallback(() => {
return {
api_url: selectedConfig?.api_url || '',
channel: selectedConfig?.channel || '',
name: selectedConfig?.name || '',
send_resolved: true,
text: selectedConfig?.text || '',
title: selectedConfig?.title || '',
}),
[selectedConfig],
);
};
}, [selectedConfig]);
const onSlackHandler = useCallback(async () => {
setSavingState(true);

View File

@@ -6,16 +6,6 @@ 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: {
@@ -48,7 +38,9 @@ export const alertDefaults: AlertDef = {
labels: {
severity: 'warning',
},
annotations: defaultAnnotations,
annotations: {
description: 'A new alert',
},
evalWindow: defaultEvalWindow,
};
@@ -93,7 +85,9 @@ export const logAlertDefaults: AlertDef = {
severity: 'warning',
details: `${window.location.protocol}//${window.location.host}/logs`,
},
annotations: defaultAnnotations,
annotations: {
description: 'A new log-based alert',
},
evalWindow: defaultEvalWindow,
};
@@ -138,7 +132,9 @@ export const traceAlertDefaults: AlertDef = {
severity: 'warning',
details: `${window.location.protocol}//${window.location.host}/traces`,
},
annotations: defaultAnnotations,
annotations: {
description: 'A new trace-based alert',
},
evalWindow: defaultEvalWindow,
};
@@ -183,6 +179,8 @@ export const exceptionAlertDefaults: AlertDef = {
severity: 'warning',
details: `${window.location.protocol}//${window.location.host}/exceptions`,
},
annotations: defaultAnnotations,
annotations: {
description: 'A new exceptions-based alert',
},
evalWindow: defaultEvalWindow,
};

View File

@@ -47,8 +47,8 @@ function EditAlertChannels({
setType(value as ChannelType);
}, []);
const prepareSlackRequest = useCallback(
() => ({
const prepareSlackRequest = useCallback(() => {
return {
api_url: selectedConfig?.api_url || '',
channel: selectedConfig?.channel || '',
name: selectedConfig?.name || '',
@@ -56,9 +56,8 @@ function EditAlertChannels({
text: selectedConfig?.text || '',
title: selectedConfig?.title || '',
id,
}),
[id, selectedConfig],
);
};
}, [id, selectedConfig]);
const onSlackEditHandler = useCallback(async () => {
setSavingState(true);
@@ -144,8 +143,8 @@ function EditAlertChannels({
setSavingState(false);
}, [prepareWebhookRequest, t, notifications, selectedConfig]);
const preparePagerRequest = useCallback(
() => ({
const preparePagerRequest = useCallback(() => {
return {
name: selectedConfig.name || '',
routing_key: selectedConfig.routing_key,
client: selectedConfig.client,
@@ -158,9 +157,8 @@ function EditAlertChannels({
details: selectedConfig.details,
detailsArray: JSON.parse(selectedConfig.details || '{}'),
id,
}),
[id, selectedConfig],
);
};
}, [id, selectedConfig]);
const onPagerEditHandler = useCallback(async () => {
setSavingState(true);

View File

@@ -1,7 +1,6 @@
import { Button, Divider, notification, Space, Typography } from 'antd';
import { Button, Divider, notification, Space, Table, 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';
@@ -54,14 +53,12 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
() => [
{
title: 'Key',
width: 100,
dataIndex: 'key',
key: 'key',
},
{
title: 'Value',
dataIndex: 'value',
width: 100,
key: 'value',
},
],
@@ -80,15 +77,13 @@ 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) {
notifications.error({
notification.error({
message: 'Error Id cannot be empty',
});
return;
@@ -100,7 +95,7 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
}&timestamp=${getNanoSeconds(timestamp)}&errorId=${id}`,
);
} catch (error) {
notifications.error({
notification.error({
message: t('something_went_wrong'),
});
}
@@ -121,7 +116,6 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
return (
<>
{NotificationElement}
<Typography>{errorDetail.exceptionType}</Typography>
<Typography>{errorDetail.exceptionMessage}</Typography>
<Divider />
@@ -173,7 +167,7 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
<EditorContainer>
<Space direction="vertical">
<ResizeTable columns={columns} tableLayout="fixed" dataSource={data} />
<Table tableLayout="fixed" columns={columns} dataSource={data} />
</Space>
</EditorContainer>
</>

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
import { Form, Input } from 'antd';
import { Input } from 'antd';
import FormItem from 'antd/lib/form/FormItem';
import React from 'react';
import { useTranslation } from 'react-i18next';
@@ -11,7 +12,7 @@ function Slack({ setSelectedConfig }: SlackProps): JSX.Element {
return (
<>
<Form.Item name="api_url" label={t('field_webhook_url')}>
<FormItem name="api_url" label={t('field_webhook_url')}>
<Input
onChange={(event): void => {
setSelectedConfig((value) => ({
@@ -20,9 +21,9 @@ function Slack({ setSelectedConfig }: SlackProps): JSX.Element {
}));
}}
/>
</Form.Item>
</FormItem>
<Form.Item
<FormItem
name="channel"
help={t('slack_channel_help')}
label={t('field_slack_recipient')}
@@ -35,9 +36,9 @@ function Slack({ setSelectedConfig }: SlackProps): JSX.Element {
}))
}
/>
</Form.Item>
</FormItem>
<Form.Item name="title" label={t('field_slack_title')}>
<FormItem 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 }}`}
@@ -48,9 +49,9 @@ function Slack({ setSelectedConfig }: SlackProps): JSX.Element {
}))
}
/>
</Form.Item>
</FormItem>
<Form.Item name="text" label={t('field_slack_description')}>
<FormItem name="text" label={t('field_slack_description')}>
<TextArea
onChange={(event): void =>
setSelectedConfig((value) => ({
@@ -60,7 +61,7 @@ function Slack({ setSelectedConfig }: SlackProps): JSX.Element {
}
placeholder={t('placeholder_slack_description')}
/>
</Form.Item>
</FormItem>
</>
);
}

View File

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

View File

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

View File

@@ -32,8 +32,6 @@ export const rawQueryToIChQuery = (
// ClickHouseQueryBuilder format. The main difference is
// use of rawQuery (in ClickHouseQueryBuilder)
// and query (in alert builder)
export const toIClickHouseQuery = (src: IChQuery): IClickHouseQuery => ({
...src,
name: 'A',
rawQuery: src.query,
});
export const toIClickHouseQuery = (src: IChQuery): IClickHouseQuery => {
return { ...src, name: 'A', rawQuery: src.query };
};

View File

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

View File

@@ -163,11 +163,10 @@ function QuerySection({
...allQueries,
});
};
const [notifications, NotificationElement] = notification.useNotification();
const addMetricQuery = useCallback(() => {
if (Object.keys(metricQueries).length > 5) {
notifications.error({
notification.error({
message: t('metric_query_max_limit'),
});
return;
@@ -192,7 +191,7 @@ function QuerySection({
expression: queryLabel,
};
setMetricQueries({ ...queries });
}, [t, getNextQueryLabel, metricQueries, setMetricQueries, notifications]);
}, [t, getNextQueryLabel, metricQueries, setMetricQueries]);
const addFormula = useCallback(() => {
// defaulting to F1 as only one formula is supported
@@ -212,70 +211,78 @@ function QuerySection({
setFormulaQueries({ ...formulas });
}, [formulaQueries, setFormulaQueries]);
const renderPromqlUI = (): JSX.Element => (
<PromqlSection promQueries={promQueries} setPromQueries={setPromQueries} />
);
const renderPromqlUI = (): JSX.Element => {
return (
<PromqlSection promQueries={promQueries} setPromQueries={setPromQueries} />
);
};
const renderChQueryUI = (): JSX.Element => (
<ChQuerySection chQueries={chQueries} setChQueries={setChQueries} />
);
const renderChQueryUI = (): JSX.Element => {
return <ChQuerySection chQueries={chQueries} setChQueries={setChQueries} />;
};
const renderFormulaButton = (): JSX.Element => (
<QueryButton onClick={addFormula} icon={<PlusOutlined />}>
{t('button_formula')}
</QueryButton>
);
const renderFormulaButton = (): JSX.Element => {
return (
<QueryButton onClick={addFormula} icon={<PlusOutlined />}>
{t('button_formula')}
</QueryButton>
);
};
const renderQueryButton = (): JSX.Element => (
<QueryButton onClick={addMetricQuery} icon={<PlusOutlined />}>
{t('button_query')}
</QueryButton>
);
const renderQueryButton = (): JSX.Element => {
return (
<QueryButton onClick={addMetricQuery} icon={<PlusOutlined />}>
{t('button_query')}
</QueryButton>
);
};
const renderMetricUI = (): JSX.Element => (
<div>
{metricQueries &&
Object.keys(metricQueries).map((key: string) => {
// todo(amol): need to handle this in fetch
const current = metricQueries[key];
current.name = key;
return (
<MetricsBuilder
key={key}
queryIndex={key}
queryData={toIMetricsBuilderQuery(current)}
selectedGraph="TIME_SERIES"
handleQueryChange={handleMetricQueryChange}
/>
);
})}
{queryCategory !== EQueryType.PROM && renderQueryButton()}
<div style={{ marginTop: '1rem' }}>
{formulaQueries &&
Object.keys(formulaQueries).map((key: string) => {
const renderMetricUI = (): JSX.Element => {
return (
<div>
{metricQueries &&
Object.keys(metricQueries).map((key: string) => {
// todo(amol): need to handle this in fetch
const current = formulaQueries[key];
const current = metricQueries[key];
current.name = key;
return (
<MetricsBuilderFormula
<MetricsBuilder
key={key}
formulaIndex={key}
formulaData={current}
handleFormulaChange={handleFormulaChange}
queryIndex={key}
queryData={toIMetricsBuilderQuery(current)}
selectedGraph="TIME_SERIES"
handleQueryChange={handleMetricQueryChange}
/>
);
})}
{queryCategory === EQueryType.QUERY_BUILDER &&
(!formulaQueries || Object.keys(formulaQueries).length === 0) &&
metricQueries &&
Object.keys(metricQueries).length > 0 &&
renderFormulaButton()}
{queryCategory !== EQueryType.PROM && renderQueryButton()}
<div style={{ marginTop: '1rem' }}>
{formulaQueries &&
Object.keys(formulaQueries).map((key: string) => {
// todo(amol): need to handle this in fetch
const current = formulaQueries[key];
current.name = key;
return (
<MetricsBuilderFormula
key={key}
formulaIndex={key}
formulaData={current}
handleFormulaChange={handleFormulaChange}
/>
);
})}
{queryCategory === EQueryType.QUERY_BUILDER &&
(!formulaQueries || Object.keys(formulaQueries).length === 0) &&
metricQueries &&
Object.keys(metricQueries).length > 0 &&
renderFormulaButton()}
</div>
</div>
</div>
);
);
};
const handleRunQuery = (): void => {
runQuery();
@@ -351,7 +358,6 @@ function QuerySection({
};
return (
<>
{NotificationElement}
<StepHeading> {t('alert_form_step1')}</StepHeading>
<FormContainer>
<div style={{ display: 'flex' }}>{renderTabs(alertType)}</div>

View File

@@ -1,4 +1,5 @@
import { Form, Select, Typography } from 'antd';
import { Select, Typography } from 'antd';
import FormItem from 'antd/lib/form/FormItem';
import React from 'react';
import { useTranslation } from 'react-i18next';
import {
@@ -17,7 +18,6 @@ import {
} from './styles';
const { Option } = Select;
const FormItem = Form.Item;
function RuleOptions({
alertDef,
@@ -38,94 +38,106 @@ function RuleOptions({
});
};
const renderCompareOps = (): JSX.Element => (
<InlineSelect
defaultValue={defaultCompareOp}
value={alertDef.condition?.op}
style={{ minWidth: '120px' }}
onChange={(value: string | unknown): void => {
const newOp = (value as string) || '';
const renderCompareOps = (): JSX.Element => {
return (
<InlineSelect
defaultValue={defaultCompareOp}
value={alertDef.condition?.op}
style={{ minWidth: '120px' }}
onChange={(value: string | unknown): void => {
const newOp = (value as string) || '';
setAlertDef({
...alertDef,
condition: {
...alertDef.condition,
op: newOp,
},
});
}}
>
<Option value="1">{t('option_above')}</Option>
<Option value="2">{t('option_below')}</Option>
<Option value="3">{t('option_equal')}</Option>
<Option value="4">{t('option_notequal')}</Option>
</InlineSelect>
);
setAlertDef({
...alertDef,
condition: {
...alertDef.condition,
op: newOp,
},
});
}}
>
<Option value="1">{t('option_above')}</Option>
<Option value="2">{t('option_below')}</Option>
<Option value="3">{t('option_equal')}</Option>
<Option value="4">{t('option_notequal')}</Option>
</InlineSelect>
);
};
const renderThresholdMatchOpts = (): JSX.Element => (
<InlineSelect
defaultValue={defaultMatchType}
style={{ minWidth: '130px' }}
value={alertDef.condition?.matchType}
onChange={(value: string | unknown): void => handleMatchOptChange(value)}
>
<Option value="1">{t('option_atleastonce')}</Option>
<Option value="2">{t('option_allthetimes')}</Option>
<Option value="3">{t('option_onaverage')}</Option>
<Option value="4">{t('option_intotal')}</Option>
</InlineSelect>
);
const renderThresholdMatchOpts = (): JSX.Element => {
return (
<InlineSelect
defaultValue={defaultMatchType}
style={{ minWidth: '130px' }}
value={alertDef.condition?.matchType}
onChange={(value: string | unknown): void => handleMatchOptChange(value)}
>
<Option value="1">{t('option_atleastonce')}</Option>
<Option value="2">{t('option_allthetimes')}</Option>
<Option value="3">{t('option_onaverage')}</Option>
<Option value="4">{t('option_intotal')}</Option>
</InlineSelect>
);
};
const renderPromMatchOpts = (): JSX.Element => (
<InlineSelect
defaultValue={defaultMatchType}
style={{ minWidth: '130px' }}
value={alertDef.condition?.matchType}
onChange={(value: string | unknown): void => handleMatchOptChange(value)}
>
<Option value="1">{t('option_atleastonce')}</Option>
</InlineSelect>
);
const renderPromMatchOpts = (): JSX.Element => {
return (
<InlineSelect
defaultValue={defaultMatchType}
style={{ minWidth: '130px' }}
value={alertDef.condition?.matchType}
onChange={(value: string | unknown): void => handleMatchOptChange(value)}
>
<Option value="1">{t('option_atleastonce')}</Option>
</InlineSelect>
);
};
const renderEvalWindows = (): JSX.Element => (
<InlineSelect
defaultValue={defaultEvalWindow}
style={{ minWidth: '120px' }}
value={alertDef.evalWindow}
onChange={(value: string | unknown): void => {
const ew = (value as string) || alertDef.evalWindow;
setAlertDef({
...alertDef,
evalWindow: ew,
});
}}
>
{' '}
<Option value="5m0s">{t('option_5min')}</Option>
<Option value="10m0s">{t('option_10min')}</Option>
<Option value="15m0s">{t('option_15min')}</Option>
<Option value="60m0s">{t('option_60min')}</Option>
<Option value="4h0m0s">{t('option_4hours')}</Option>
<Option value="24h0m0s">{t('option_24hours')}</Option>
</InlineSelect>
);
const renderEvalWindows = (): JSX.Element => {
return (
<InlineSelect
defaultValue={defaultEvalWindow}
style={{ minWidth: '120px' }}
value={alertDef.evalWindow}
onChange={(value: string | unknown): void => {
const ew = (value as string) || alertDef.evalWindow;
setAlertDef({
...alertDef,
evalWindow: ew,
});
}}
>
{' '}
<Option value="5m0s">{t('option_5min')}</Option>
<Option value="10m0s">{t('option_10min')}</Option>
<Option value="15m0s">{t('option_15min')}</Option>
<Option value="60m0s">{t('option_60min')}</Option>
<Option value="4h0m0s">{t('option_4hours')}</Option>
<Option value="24h0m0s">{t('option_24hours')}</Option>
</InlineSelect>
);
};
const renderThresholdRuleOpts = (): JSX.Element => (
<FormItem>
<Typography.Text>
{t('text_condition1')} {renderCompareOps()} {t('text_condition2')}{' '}
{renderThresholdMatchOpts()} {t('text_condition3')} {renderEvalWindows()}
</Typography.Text>
</FormItem>
);
const renderPromRuleOptions = (): JSX.Element => (
<FormItem>
<Typography.Text>
{t('text_condition1')} {renderCompareOps()} {t('text_condition2')}{' '}
{renderPromMatchOpts()}
</Typography.Text>
</FormItem>
);
const renderThresholdRuleOpts = (): JSX.Element => {
return (
<FormItem>
<Typography.Text>
{t('text_condition1')} {renderCompareOps()} {t('text_condition2')}{' '}
{renderThresholdMatchOpts()} {t('text_condition3')} {renderEvalWindows()}
</Typography.Text>
</FormItem>
);
};
const renderPromRuleOptions = (): JSX.Element => {
return (
<FormItem>
<Typography.Text>
{t('text_condition1')} {renderCompareOps()} {t('text_condition2')}{' '}
{renderPromMatchOpts()}
</Typography.Text>
</FormItem>
);
};
return (
<>

View File

@@ -15,130 +15,154 @@ function UserGuide({ queryType }: UserGuideProps): JSX.Element {
// init namespace for translations
const { t } = useTranslation('alerts');
const renderStep1QB = (): JSX.Element => (
<>
<StyledTopic>{t('user_guide_qb_step1')}</StyledTopic>
<StyledList>
<StyledListItem>{t('user_guide_qb_step1a')}</StyledListItem>
<StyledListItem>{t('user_guide_qb_step1b')}</StyledListItem>
<StyledListItem>{t('user_guide_qb_step1c')}</StyledListItem>
<StyledListItem>{t('user_guide_qb_step1d')}</StyledListItem>
</StyledList>
</>
);
const renderStep2QB = (): JSX.Element => (
<>
<StyledTopic>{t('user_guide_qb_step2')}</StyledTopic>
<StyledList>
<StyledListItem>{t('user_guide_qb_step2a')}</StyledListItem>
<StyledListItem>{t('user_guide_qb_step2b')}</StyledListItem>
</StyledList>
</>
);
const renderStep1QB = (): JSX.Element => {
return (
<>
<StyledTopic>{t('user_guide_qb_step1')}</StyledTopic>
<StyledList>
<StyledListItem>{t('user_guide_qb_step1a')}</StyledListItem>
<StyledListItem>{t('user_guide_qb_step1b')}</StyledListItem>
<StyledListItem>{t('user_guide_qb_step1c')}</StyledListItem>
<StyledListItem>{t('user_guide_qb_step1d')}</StyledListItem>
</StyledList>
</>
);
};
const renderStep2QB = (): JSX.Element => {
return (
<>
<StyledTopic>{t('user_guide_qb_step2')}</StyledTopic>
<StyledList>
<StyledListItem>{t('user_guide_qb_step2a')}</StyledListItem>
<StyledListItem>{t('user_guide_qb_step2b')}</StyledListItem>
</StyledList>
</>
);
};
const renderStep3QB = (): JSX.Element => (
<>
<StyledTopic>{t('user_guide_qb_step3')}</StyledTopic>
<StyledList>
<StyledListItem>{t('user_guide_qb_step3a')}</StyledListItem>
<StyledListItem>{t('user_guide_qb_step3b')}</StyledListItem>
</StyledList>
</>
);
const renderStep3QB = (): JSX.Element => {
return (
<>
<StyledTopic>{t('user_guide_qb_step3')}</StyledTopic>
<StyledList>
<StyledListItem>{t('user_guide_qb_step3a')}</StyledListItem>
<StyledListItem>{t('user_guide_qb_step3b')}</StyledListItem>
</StyledList>
</>
);
};
const renderGuideForQB = (): JSX.Element => (
<>
{renderStep1QB()}
{renderStep2QB()}
{renderStep3QB()}
</>
);
const renderStep1PQL = (): JSX.Element => (
<>
<StyledTopic>{t('user_guide_pql_step1')}</StyledTopic>
<StyledList>
<StyledListItem>{t('user_guide_pql_step1a')}</StyledListItem>
<StyledListItem>{t('user_guide_pql_step1b')}</StyledListItem>
</StyledList>
</>
);
const renderStep2PQL = (): JSX.Element => (
<>
<StyledTopic>{t('user_guide_pql_step2')}</StyledTopic>
<StyledList>
<StyledListItem>{t('user_guide_pql_step2a')}</StyledListItem>
<StyledListItem>{t('user_guide_pql_step2b')}</StyledListItem>
</StyledList>
</>
);
const renderGuideForQB = (): JSX.Element => {
return (
<>
{renderStep1QB()}
{renderStep2QB()}
{renderStep3QB()}
</>
);
};
const renderStep1PQL = (): JSX.Element => {
return (
<>
<StyledTopic>{t('user_guide_pql_step1')}</StyledTopic>
<StyledList>
<StyledListItem>{t('user_guide_pql_step1a')}</StyledListItem>
<StyledListItem>{t('user_guide_pql_step1b')}</StyledListItem>
</StyledList>
</>
);
};
const renderStep2PQL = (): JSX.Element => {
return (
<>
<StyledTopic>{t('user_guide_pql_step2')}</StyledTopic>
<StyledList>
<StyledListItem>{t('user_guide_pql_step2a')}</StyledListItem>
<StyledListItem>{t('user_guide_pql_step2b')}</StyledListItem>
</StyledList>
</>
);
};
const renderStep3PQL = (): JSX.Element => (
<>
<StyledTopic>{t('user_guide_pql_step3')}</StyledTopic>
<StyledList>
<StyledListItem>{t('user_guide_pql_step3a')}</StyledListItem>
<StyledListItem>{t('user_guide_pql_step3b')}</StyledListItem>
</StyledList>
</>
);
const renderStep3PQL = (): JSX.Element => {
return (
<>
<StyledTopic>{t('user_guide_pql_step3')}</StyledTopic>
<StyledList>
<StyledListItem>{t('user_guide_pql_step3a')}</StyledListItem>
<StyledListItem>{t('user_guide_pql_step3b')}</StyledListItem>
</StyledList>
</>
);
};
const renderGuideForPQL = (): JSX.Element => (
<>
{renderStep1PQL()}
{renderStep2PQL()}
{renderStep3PQL()}
</>
);
const renderGuideForPQL = (): JSX.Element => {
return (
<>
{renderStep1PQL()}
{renderStep2PQL()}
{renderStep3PQL()}
</>
);
};
const renderStep1CH = (): JSX.Element => (
<>
<StyledTopic>{t('user_guide_ch_step1')}</StyledTopic>
<StyledList>
<StyledListItem>
<Trans
i18nKey="user_guide_ch_step1a"
t={t}
components={[
// eslint-disable-next-line jsx-a11y/control-has-associated-label, jsx-a11y/anchor-has-content
<a
key={1}
target="_blank"
href=" https://signoz.io/docs/tutorial/writing-clickhouse-queries-in-dashboard/?utm_source=frontend&utm_medium=product&utm_id=alerts</>"
/>,
]}
/>
</StyledListItem>
<StyledListItem>{t('user_guide_ch_step1b')}</StyledListItem>
</StyledList>
</>
);
const renderStep2CH = (): JSX.Element => (
<>
<StyledTopic>{t('user_guide_ch_step2')}</StyledTopic>
<StyledList>
<StyledListItem>{t('user_guide_ch_step2a')}</StyledListItem>
<StyledListItem>{t('user_guide_ch_step2b')}</StyledListItem>
</StyledList>
</>
);
const renderStep1CH = (): JSX.Element => {
return (
<>
<StyledTopic>{t('user_guide_ch_step1')}</StyledTopic>
<StyledList>
<StyledListItem>
<Trans
i18nKey="user_guide_ch_step1a"
t={t}
components={[
// eslint-disable-next-line jsx-a11y/control-has-associated-label, jsx-a11y/anchor-has-content
<a
key={1}
target="_blank"
href=" https://signoz.io/docs/tutorial/writing-clickhouse-queries-in-dashboard/?utm_source=frontend&utm_medium=product&utm_id=alerts</>"
/>,
]}
/>
</StyledListItem>
<StyledListItem>{t('user_guide_ch_step1b')}</StyledListItem>
</StyledList>
</>
);
};
const renderStep2CH = (): JSX.Element => {
return (
<>
<StyledTopic>{t('user_guide_ch_step2')}</StyledTopic>
<StyledList>
<StyledListItem>{t('user_guide_ch_step2a')}</StyledListItem>
<StyledListItem>{t('user_guide_ch_step2b')}</StyledListItem>
</StyledList>
</>
);
};
const renderStep3CH = (): JSX.Element => (
<>
<StyledTopic>{t('user_guide_ch_step3')}</StyledTopic>
<StyledList>
<StyledListItem>{t('user_guide_ch_step3a')}</StyledListItem>
<StyledListItem>{t('user_guide_ch_step3b')}</StyledListItem>
</StyledList>
</>
);
const renderStep3CH = (): JSX.Element => {
return (
<>
<StyledTopic>{t('user_guide_ch_step3')}</StyledTopic>
<StyledList>
<StyledListItem>{t('user_guide_ch_step3a')}</StyledListItem>
<StyledListItem>{t('user_guide_ch_step3b')}</StyledListItem>
</StyledList>
</>
);
};
const renderGuideForCH = (): JSX.Element => (
<>
{renderStep1CH()}
{renderStep2CH()}
{renderStep3CH()}
</>
);
const renderGuideForCH = (): JSX.Element => {
return (
<>
{renderStep1CH()}
{renderStep2CH()}
{renderStep3CH()}
</>
);
};
return (
<StyledMainContainer>
<Row>

View File

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

View File

@@ -119,15 +119,17 @@ function LabelSelect({
{queries.length > 0 &&
map(
queries,
(query): JSX.Element => (
<QueryChip key={query.key} queryData={query} onRemove={handleClose} />
),
(query): JSX.Element => {
return (
<QueryChip key={query.key} queryData={query} onRemove={handleClose} />
);
},
)}
</div>
<div>
{map(staging, (item) => (
<QueryChipItem key={uuid()}>{item}</QueryChipItem>
))}
{map(staging, (item) => {
return <QueryChipItem key={uuid()}>{item}</QueryChipItem>;
})}
</div>
<div style={{ display: 'flex', width: '100%' }}>

View File

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

View File

@@ -42,15 +42,17 @@ export const toMetricQueries = (b: IBuilderQueries): IMetricQueries => {
export const toIMetricsBuilderQuery = (
q: IMetricQuery,
): IMetricsBuilderQuery => ({
name: q.name,
metricName: q.metricName,
tagFilters: q.tagFilters,
groupBy: q.groupBy,
aggregateOperator: q.aggregateOperator,
disabled: q.disabled,
legend: q.legend,
});
): IMetricsBuilderQuery => {
return {
name: q.name,
metricName: q.metricName,
tagFilters: q.tagFilters,
groupBy: q.groupBy,
aggregateOperator: q.aggregateOperator,
disabled: q.disabled,
legend: q.legend,
};
};
export const prepareBuilderQueries = (
m: IMetricQueries,

View File

@@ -112,19 +112,21 @@ export const getNodeById = (
const getSpanWithoutChildren = (
span: ITraceTree,
): Omit<ITraceTree, 'children'> => ({
id: span.id,
name: span.name,
parent: span.parent,
serviceColour: span.serviceColour,
serviceName: span.serviceName,
startTime: span.startTime,
tags: span.tags,
time: span.time,
value: span.value,
event: span.event,
hasError: span.hasError,
});
): Omit<ITraceTree, 'children'> => {
return {
id: span.id,
name: span.name,
parent: span.parent,
serviceColour: span.serviceColour,
serviceName: span.serviceName,
startTime: span.startTime,
tags: span.tags,
time: span.time,
value: span.value,
event: span.event,
hasError: span.hasError,
};
};
export const isSpanPresentInSearchString = (
searchedString: string,

View File

@@ -172,8 +172,6 @@ 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);
@@ -188,14 +186,14 @@ function GeneralSettings({
const onClickSaveHandler = useCallback(
(type: TTTLType) => {
if (!setRetentionPermission) {
notifications.error({
notification.error({
message: `Sorry you don't have permission to make these changes`,
});
return;
}
onModalToggleHandler(type);
},
[setRetentionPermission, notifications],
[setRetentionPermission],
);
const s3Enabled = useMemo(
@@ -354,7 +352,7 @@ function GeneralSettings({
let hasSetTTLFailed = false;
if (setTTLResponse.statusCode === 409) {
hasSetTTLFailed = true;
notifications.error({
notification.error({
message: 'Error',
description: t('retention_request_race_condition'),
placement: 'topRight',
@@ -392,7 +390,7 @@ function GeneralSettings({
});
}
} catch (error) {
notifications.error({
notification.error({
message: 'Error',
description: t('retention_failed_message'),
placement: 'topRight',
@@ -592,23 +590,20 @@ function GeneralSettings({
});
return (
<>
{NotificationElement}
<Col xs={24} md={22} xl={20} xxl={18} style={{ margin: 'auto' }}>
{Element}
<Col xs={24} md={22} xl={20} xxl={18} style={{ margin: 'auto' }}>
<ErrorTextContainer>
<TextToolTip
{...{
text: `More details on how to set retention period`,
url: 'https://signoz.io/docs/userguide/retention-period/',
}}
/>
{errorText && <ErrorText>{errorText}</ErrorText>}
</ErrorTextContainer>
<ErrorTextContainer>
<TextToolTip
{...{
text: `More details on how to set retention period`,
url: 'https://signoz.io/docs/userguide/retention-period/',
}}
/>
{errorText && <ErrorText>{errorText}</ErrorText>}
</ErrorTextContainer>
<Row justify="start">{renderConfig}</Row>
</Col>
</>
<Row justify="start">{renderConfig}</Row>
</Col>
);
}

View File

@@ -19,7 +19,6 @@ function GridGraphComponent({
name,
yAxisUnit,
staticLine,
onDragSelect,
}: GridGraphComponentProps): JSX.Element | null {
const location = history.location.pathname;
@@ -39,7 +38,6 @@ function GridGraphComponent({
name,
yAxisUnit,
staticLine,
onDragSelect,
}}
/>
);
@@ -87,7 +85,6 @@ export interface GridGraphComponentProps {
name: string;
yAxisUnit?: string;
staticLine?: StaticLineProps;
onDragSelect?: (start: number, end: number) => void;
}
GridGraphComponent.defaultProps = {
@@ -97,7 +94,6 @@ GridGraphComponent.defaultProps = {
onClickHandler: undefined,
yAxisUnit: undefined,
staticLine: undefined,
onDragSelect: undefined,
};
export default GridGraphComponent;

View File

@@ -9,7 +9,7 @@ import {
} from 'container/NewWidget/RightContainer/timeItems';
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
import getChartData from 'lib/getChartData';
import React, { useCallback, useMemo, useState } from 'react';
import React, { useCallback, useState } from 'react';
import { useQuery } from 'react-query';
import { useSelector } from 'react-redux';
import { GetMetricQueryRange } from 'store/actions/dashboard/getQueryResults';
@@ -27,7 +27,6 @@ function FullView({
onClickHandler,
name,
yAxisUnit,
onDragSelect,
}: FullViewProps): JSX.Element {
const { selectedTime: globalSelectedTime } = useSelector<
AppState,
@@ -58,18 +57,6 @@ function FullView({
}),
);
const chartDataSet = useMemo(
() =>
getChartData({
queryData: [
{
queryData: response?.data?.payload?.data?.result || [],
},
],
}),
[response],
);
const isLoading = response.isLoading === true;
if (isLoading) {
@@ -98,15 +85,24 @@ function FullView({
)}
<GridGraphComponent
GRAPH_TYPES={widget.panelTypes}
data={chartDataSet}
isStacked={widget.isStacked}
opacity={widget.opacity}
title={widget.title}
onClickHandler={onClickHandler}
name={name}
yAxisUnit={yAxisUnit}
onDragSelect={onDragSelect}
{...{
GRAPH_TYPES: widget.panelTypes,
data: getChartData({
queryData: [
{
queryData: response.data?.payload?.data?.result
? response.data?.payload?.data?.result
: [],
},
],
}),
isStacked: widget.isStacked,
opacity: widget.opacity,
title: widget.title,
onClickHandler,
name,
yAxisUnit,
}}
/>
</>
);
@@ -118,14 +114,12 @@ interface FullViewProps {
onClickHandler?: GraphOnClickHandler;
name: string;
yAxisUnit?: string;
onDragSelect?: (start: number, end: number) => void;
}
FullView.defaultProps = {
fullViewOptions: undefined,
onClickHandler: undefined,
yAxisUnit: undefined,
onDragSelect: undefined,
};
export default FullView;

View File

@@ -15,7 +15,7 @@ import GetMaxMinTime from 'lib/getMaxMinTime';
import GetMinMax from 'lib/getMinMax';
import getStartAndEndTime from 'lib/getStartAndEndTime';
import getStep from 'lib/getStep';
import React, { useCallback, useMemo, useState } from 'react';
import React, { useCallback, useState } from 'react';
import { useQueries } from 'react-query';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
@@ -30,7 +30,6 @@ function FullView({
onClickHandler,
name,
yAxisUnit,
onDragSelect,
}: FullViewProps): JSX.Element {
const { minTime, maxTime, selectedTime: globalSelectedTime } = useSelector<
AppState,
@@ -81,22 +80,25 @@ function FullView({
const queryLength = widget.query.filter((e) => e.query.length !== 0);
const response = useQueries(
queryLength.map((query) => ({
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
queryFn: () =>
getQueryResult({
end: queryMinMax.max.toString(),
query: query.query,
start: queryMinMax.min.toString(),
step: `${getStep({
start: queryMinMax.min,
end: queryMinMax.max,
inputFormat: 's',
})}`,
}),
queryHash: `${query.query}-${query.legend}-${selectedTime.enum}`,
retryOnMount: false,
})),
queryLength.map((query) => {
return {
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
queryFn: () => {
return getQueryResult({
end: queryMinMax.max.toString(),
query: query.query,
start: queryMinMax.min.toString(),
step: `${getStep({
start: queryMinMax.min,
end: queryMinMax.max,
inputFormat: 's',
})}`,
});
},
queryHash: `${query.query}-${query.legend}-${selectedTime.enum}`,
retryOnMount: false,
};
}),
);
const isError =
@@ -115,18 +117,6 @@ function FullView({
})),
);
const chartDataSet = useMemo(
() =>
getChartData({
queryData: data.map((e) => ({
query: e?.map((e) => e.query).join(' ') || '',
queryData: e?.map((e) => e.queryData) || [],
legend: e?.map((e) => e.legend).join('') || '',
})),
}),
[data],
);
if (isLoading) {
return <Spinner height="100%" size="large" tip="Loading..." />;
}
@@ -161,15 +151,22 @@ function FullView({
)}
<GridGraphComponent
GRAPH_TYPES={widget.panelTypes}
data={chartDataSet}
isStacked={widget.isStacked}
opacity={widget.opacity}
title={widget.title}
onClickHandler={onClickHandler}
name={name}
yAxisUnit={yAxisUnit}
onDragSelect={onDragSelect}
{...{
GRAPH_TYPES: widget.panelTypes,
data: getChartData({
queryData: data.map((e) => ({
query: e?.map((e) => e.query).join(' ') || '',
queryData: e?.map((e) => e.queryData) || [],
legend: e?.map((e) => e.legend).join('') || '',
})),
}),
isStacked: widget.isStacked,
opacity: widget.opacity,
title: widget.title,
onClickHandler,
name,
yAxisUnit,
}}
/>
</>
);
@@ -181,14 +178,12 @@ interface FullViewProps {
onClickHandler?: GraphOnClickHandler;
name: string;
yAxisUnit?: string;
onDragSelect?: (start: number, end: number) => void;
}
FullView.defaultProps = {
fullViewOptions: undefined,
onClickHandler: undefined,
yAxisUnit: undefined,
onDragSelect: undefined,
};
export default FullView;

View File

@@ -1,15 +1,13 @@
import { Typography } from 'antd';
import { AxiosError } from 'axios';
import { ChartData } from 'chart.js';
import Spinner from 'components/Spinner';
import GridGraphComponent from 'container/GridGraphComponent';
import usePreviousValue from 'hooks/usePreviousValue';
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
import getChartData from 'lib/getChartData';
import isEmpty from 'lodash-es/isEmpty';
import React, { memo, useCallback, useMemo, useState } from 'react';
import React, { memo, useCallback, useEffect, useState } from 'react';
import { Layout } from 'react-grid-layout';
import { useInView } from 'react-intersection-observer';
import { useQuery } from 'react-query';
import { connect, useSelector } from 'react-redux';
import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
@@ -22,14 +20,13 @@ import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import { GlobalTime } from 'types/actions/globalTime';
import { Widgets } from 'types/api/dashboard/getAll';
import DashboardReducer from 'types/reducer/dashboards';
import { GlobalReducer } from 'types/reducer/globalTime';
import { LayoutProps } from '..';
import EmptyWidget from '../EmptyWidget';
import WidgetHeader from '../WidgetHeader';
import FullView from './FullView/index.metricsBuilder';
import { FullViewContainer, Modal } from './styles';
import { ErrorContainer, FullViewContainer, Modal } from './styles';
function GridCardGraph({
widget,
@@ -38,15 +35,13 @@ function GridCardGraph({
yAxisUnit,
layout = [],
setLayout,
onDragSelect,
}: GridCardGraphProps): JSX.Element {
const { ref: graphRef, inView: isGraphVisible } = useInView({
threshold: 0,
triggerOnce: true,
initialInView: true,
const [state, setState] = useState<GridCardGraphState>({
loading: true,
errorMessage: '',
error: false,
payload: undefined,
});
const [errorMessage, setErrorMessage] = useState<string | undefined>('');
const [hovered, setHovered] = useState(false);
const [modal, setModal] = useState(false);
const [deleteModal, setDeleteModal] = useState(false);
@@ -58,57 +53,113 @@ function GridCardGraph({
AppState,
GlobalReducer
>((state) => state.globalTime);
const { dashboards } = useSelector<AppState, DashboardReducer>(
(state) => state.dashboards,
);
const [selectedDashboard] = dashboards;
const selectedData = selectedDashboard?.data;
const { variables } = selectedData;
const queryResponse = useQuery(
[
`GetMetricsQueryRange-${widget.timePreferance}-${globalSelectedInterval}-${widget.id}`,
{
widget,
maxTime,
minTime,
globalSelectedInterval,
variables,
},
],
() =>
GetMetricQueryRange({
selectedTime: widget.timePreferance,
graphType: widget.panelTypes,
query: widget.query,
globalSelectedInterval,
variables: getDashboardVariables(),
}),
{
keepPreviousData: true,
enabled: isGraphVisible,
refetchOnMount: false,
onError: (error) => {
if (error instanceof Error) {
setErrorMessage(error.message);
// const getMaxMinTime = GetMaxMinTime({
// graphType: widget?.panelTypes,
// maxTime,
// minTime,
// });
// const { start, end } = GetStartAndEndTime({
// type: widget?.timePreferance,
// maxTime: getMaxMinTime.maxTime,
// minTime: getMaxMinTime.minTime,
// });
// const queryLength = widget?.query?.filter((e) => e.query.length !== 0) || [];
// const response = useQueries(
// queryLength?.map((query) => {
// return {
// // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
// queryFn: () => {
// return getQueryResult({
// end,
// query: query?.query,
// start,
// step: '60',
// });
// },
// queryHash: `${query?.query}-${query?.legend}-${start}-${end}`,
// retryOnMount: false,
// };
// }),
// );
// const isError =
// response.find((e) => e?.data?.statusCode !== 200) !== undefined ||
// response.some((e) => e.isError === true);
// const isLoading = response.some((e) => e.isLoading === true);
// const errorMessage = response.find((e) => e.data?.error !== null)?.data?.error;
// const data = response.map((responseOfQuery) =>
// responseOfQuery?.data?.payload?.result.map((e, index) => ({
// query: queryLength[index]?.query,
// queryData: e,
// legend: queryLength[index]?.legend,
// })),
// );
useEffect(() => {
(async (): Promise<void> => {
try {
setState((state) => ({
...state,
error: false,
errorMessage: '',
loading: true,
}));
const response = await GetMetricQueryRange({
selectedTime: widget.timePreferance,
graphType: widget.panelTypes,
query: widget.query,
globalSelectedInterval,
variables: getDashboardVariables(),
});
const isError = response.error;
if (isError != null) {
setState((state) => ({
...state,
error: true,
errorMessage: isError || 'Something went wrong',
loading: false,
}));
} else {
const chartDataSet = getChartData({
queryData: [
{
queryData: response.payload?.data?.result
? response.payload?.data?.result
: [],
},
],
});
setState((state) => ({
...state,
loading: false,
payload: chartDataSet,
}));
}
},
},
);
const chartData = useMemo(
() =>
getChartData({
queryData: [
{
queryData: queryResponse?.data?.payload?.data?.result || [],
},
],
}),
[queryResponse],
);
const prevChartDataSetRef = usePreviousValue<ChartData>(chartData);
} catch (error) {
setState((state) => ({
...state,
error: true,
errorMessage: (error as AxiosError).toString(),
loading: false,
}));
} finally {
setState((state) => ({
...state,
loading: false,
}));
}
})();
}, [widget, maxTime, minTime, globalSelectedInterval]);
const onToggleModal = useCallback(
(func: React.Dispatch<React.SetStateAction<boolean>>) => {
@@ -126,114 +177,70 @@ function GridCardGraph({
onToggleModal(setDeleteModal);
}, [deleteWidget, layout, onToggleModal, setLayout, widget]);
const getModals = (): JSX.Element => (
<>
<Modal
destroyOnClose
onCancel={(): void => onToggleModal(setDeleteModal)}
open={deleteModal}
title="Delete"
height="10vh"
onOk={onDeleteHandler}
centered
>
<Typography>Are you sure you want to delete this widget</Typography>
</Modal>
const getModals = (): JSX.Element => {
return (
<>
<Modal
destroyOnClose
onCancel={(): void => onToggleModal(setDeleteModal)}
open={deleteModal}
title="Delete"
height="10vh"
onOk={onDeleteHandler}
centered
>
<Typography>Are you sure you want to delete this widget</Typography>
</Modal>
<Modal
title="View"
footer={[]}
centered
open={modal}
onCancel={(): void => onToggleModal(setModal)}
width="85%"
destroyOnClose
>
<FullViewContainer>
<FullView name={`${name}expanded`} widget={widget} yAxisUnit={yAxisUnit} />
</FullViewContainer>
</Modal>
</>
);
const handleOnView = (): void => {
onToggleModal(setModal);
};
const handleOnDelete = (): void => {
onToggleModal(setDeleteModal);
<Modal
title="View"
footer={[]}
centered
open={modal}
onCancel={(): void => onToggleModal(setModal)}
width="85%"
destroyOnClose
>
<FullViewContainer>
<FullView
name={`${name}expanded`}
widget={widget}
yAxisUnit={yAxisUnit}
/>
</FullViewContainer>
</Modal>
</>
);
};
const isEmptyLayout = widget?.id === 'empty' || isEmpty(widget);
if (queryResponse.isError && !isEmptyLayout) {
if (state.error && !isEmptyLayout) {
return (
<span ref={graphRef}>
<>
{getModals()}
{!isEmpty(widget) && prevChartDataSetRef && (
<>
<div className="drag-handle">
<WidgetHeader
parentHover={hovered}
title={widget?.title}
widget={widget}
onView={handleOnView}
onDelete={handleOnDelete}
queryResponse={queryResponse}
errorMessage={errorMessage}
/>
</div>
<GridGraphComponent
GRAPH_TYPES={widget.panelTypes}
data={prevChartDataSetRef}
isStacked={widget.isStacked}
opacity={widget.opacity}
title={' '}
name={name}
yAxisUnit={yAxisUnit}
/>
</>
)}
</span>
<WidgetHeader
parentHover={hovered}
title={widget?.title}
widget={widget}
onView={(): void => onToggleModal(setModal)}
onDelete={(): void => onToggleModal(setDeleteModal)}
/>
<ErrorContainer>{state.errorMessage}</ErrorContainer>
</>
);
}
if (prevChartDataSetRef?.labels === undefined && queryResponse.isLoading) {
return (
<span ref={graphRef}>
{!isEmpty(widget) && prevChartDataSetRef?.labels ? (
<>
<div className="drag-handle">
<WidgetHeader
parentHover={hovered}
title={widget?.title}
widget={widget}
onView={handleOnView}
onDelete={handleOnDelete}
queryResponse={queryResponse}
errorMessage={errorMessage}
/>
</div>
<GridGraphComponent
GRAPH_TYPES={widget.panelTypes}
data={prevChartDataSetRef}
isStacked={widget.isStacked}
opacity={widget.opacity}
title={' '}
name={name}
yAxisUnit={yAxisUnit}
/>
</>
) : (
<Spinner height="20vh" tip="Loading..." />
)}
</span>
);
if (
(state.loading === true || state.payload === undefined) &&
!isEmptyLayout
) {
return <Spinner height="20vh" tip="Loading..." />;
}
return (
<span
ref={graphRef}
onMouseOver={(): void => {
setHovered(true);
}}
@@ -248,31 +255,28 @@ function GridCardGraph({
}}
>
{!isEmptyLayout && (
<div className="drag-handle">
<WidgetHeader
parentHover={hovered}
title={widget?.title}
widget={widget}
onView={handleOnView}
onDelete={handleOnDelete}
queryResponse={queryResponse}
errorMessage={errorMessage}
/>
</div>
<WidgetHeader
parentHover={hovered}
title={widget?.title}
widget={widget}
onView={(): void => onToggleModal(setModal)}
onDelete={(): void => onToggleModal(setDeleteModal)}
/>
)}
{!isEmptyLayout && getModals()}
{!isEmpty(widget) && !!queryResponse.data?.payload && (
{!isEmpty(widget) && !!state.payload && (
<GridGraphComponent
GRAPH_TYPES={widget.panelTypes}
data={chartData}
isStacked={widget.isStacked}
opacity={widget.opacity}
title={' '} // `empty title to accommodate absolutely positioned widget header
name={name}
yAxisUnit={yAxisUnit}
onDragSelect={onDragSelect}
{...{
GRAPH_TYPES: widget.panelTypes,
data: state.payload,
isStacked: widget.isStacked,
opacity: widget.opacity,
title: ' ', // empty title to accommodate absolutely positioned widget header
name,
yAxisUnit,
}}
/>
)}
@@ -281,6 +285,13 @@ function GridCardGraph({
);
}
interface GridCardGraphState {
loading: boolean;
error: boolean;
errorMessage: string;
payload: ChartData | undefined;
}
interface DispatchProps {
deleteWidget: ({
widgetId,
@@ -295,13 +306,8 @@ interface GridCardGraphProps extends DispatchProps {
layout?: Layout[];
// eslint-disable-next-line react/require-default-props
setLayout?: React.Dispatch<React.SetStateAction<LayoutProps[]>>;
onDragSelect?: (start: number, end: number) => void;
}
GridCardGraph.defaultProps = {
onDragSelect: undefined,
};
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({

View File

@@ -72,7 +72,6 @@ function GraphLayout({
useCSSTransforms
allowOverlap={false}
onLayoutChange={onLayoutChangeHandler}
draggableHandle=".drag-handle"
>
{layouts.map(({ Component, ...rest }) => {
const currentWidget = (widgets || [])?.find((e) => e.id === rest.i);

View File

@@ -1,22 +0,0 @@
import { themeColors } from 'constants/theme';
const positionCss: React.CSSProperties['position'] = 'fixed';
export const spinnerStyles = { position: positionCss, right: '0.5rem' };
export const tooltipStyles = {
fontSize: '1rem',
top: '0.313rem',
position: positionCss,
right: '0.313rem',
color: themeColors.errorColor,
};
export const errorTooltipPosition = 'top';
export const overlayStyles: React.CSSProperties = {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
position: 'absolute',
};

View File

@@ -2,33 +2,22 @@ import {
DeleteOutlined,
DownOutlined,
EditFilled,
ExclamationCircleOutlined,
FullscreenOutlined,
} from '@ant-design/icons';
import { Dropdown, MenuProps, Tooltip, Typography } from 'antd';
import { MenuItemType } from 'antd/es/menu/hooks/useItems';
import Spinner from 'components/Spinner';
import { Dropdown, Menu, Typography } from 'antd';
import useComponentPermission from 'hooks/useComponentPermission';
import history from 'lib/history';
import React, { useCallback, useMemo, useState } from 'react';
import { UseQueryResult } from 'react-query';
import React, { useState } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { Widgets } from 'types/api/dashboard/getAll';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import AppReducer from 'types/reducer/app';
import {
errorTooltipPosition,
overlayStyles,
spinnerStyles,
tooltipStyles,
} from './config';
import {
ArrowContainer,
HeaderContainer,
HeaderContentContainer,
MenuItemContainer,
} from './styles';
type TWidgetOptions = 'view' | 'edit' | 'delete' | string;
@@ -38,10 +27,6 @@ interface IWidgetHeaderProps {
onView: VoidFunction;
onDelete: VoidFunction;
parentHover: boolean;
queryResponse: UseQueryResult<
SuccessResponse<MetricRangePayloadProps> | ErrorResponse
>;
errorMessage: string | undefined;
}
function WidgetHeader({
title,
@@ -49,49 +34,35 @@ function WidgetHeader({
onView,
onDelete,
parentHover,
queryResponse,
errorMessage,
}: IWidgetHeaderProps): JSX.Element {
const [localHover, setLocalHover] = useState(false);
const [isOpen, setIsOpen] = useState<boolean>(false);
const onEditHandler = useCallback((): void => {
const onEditHandler = (): void => {
const widgetId = widget.id;
history.push(
`${window.location.pathname}/new?widgetId=${widgetId}&graphType=${widget.panelTypes}`,
);
}, [widget.id, widget.panelTypes]);
};
const keyMethodMapping: {
[K in TWidgetOptions]: { key: TWidgetOptions; method: VoidFunction };
} = useMemo(
() => ({
view: {
key: 'view',
method: onView,
},
edit: {
key: 'edit',
method: onEditHandler,
},
delete: {
key: 'delete',
method: onDelete,
},
}),
[onDelete, onEditHandler, onView],
);
const onMenuItemSelectHandler: MenuProps['onClick'] = useCallback(
({ key }: { key: TWidgetOptions }): void => {
const functionToCall = keyMethodMapping[key]?.method;
if (functionToCall) {
functionToCall();
setIsOpen(false);
}
} = {
view: {
key: 'view',
method: onView,
},
[keyMethodMapping],
);
edit: {
key: 'edit',
method: onEditHandler,
},
delete: {
key: 'delete',
method: onDelete,
},
};
const onMenuItemSelectHandler = ({ key }: { key: TWidgetOptions }): void => {
keyMethodMapping[key]?.method();
};
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
const [deleteWidget, editWidget] = useComponentPermission(
@@ -99,85 +70,57 @@ function WidgetHeader({
role,
);
const menuList: MenuItemType[] = useMemo(
() => [
{
key: keyMethodMapping.view.key,
icon: <FullscreenOutlined />,
disabled: queryResponse.isLoading,
label: 'View',
},
{
key: keyMethodMapping.edit.key,
icon: <EditFilled />,
disabled: !editWidget,
label: 'Edit',
},
{
key: keyMethodMapping.delete.key,
icon: <DeleteOutlined />,
disabled: !deleteWidget,
danger: true,
label: 'Delete',
},
],
[
deleteWidget,
editWidget,
keyMethodMapping.delete.key,
keyMethodMapping.edit.key,
keyMethodMapping.view.key,
queryResponse.isLoading,
],
);
const menu = (
<Menu onClick={onMenuItemSelectHandler}>
<Menu.Item key={keyMethodMapping.view.key}>
<MenuItemContainer>
<span>View</span> <FullscreenOutlined />
</MenuItemContainer>
</Menu.Item>
const onClickHandler = useCallback(() => {
setIsOpen((open) => !open);
}, []);
{editWidget && (
<Menu.Item key={keyMethodMapping.edit.key}>
<MenuItemContainer>
<span>Edit</span> <EditFilled />
</MenuItemContainer>
</Menu.Item>
)}
const menu = useMemo(
() => ({
items: menuList,
onClick: onMenuItemSelectHandler,
}),
[menuList, onMenuItemSelectHandler],
{deleteWidget && (
<>
<Menu.Divider />
<Menu.Item key={keyMethodMapping.delete.key} danger>
<MenuItemContainer>
<span>Delete</span> <DeleteOutlined />
</MenuItemContainer>
</Menu.Item>
</>
)}
</Menu>
);
return (
<div>
<Dropdown
destroyPopupOnHide
open={isOpen}
onOpenChange={setIsOpen}
menu={menu}
trigger={['click']}
overlayStyle={overlayStyles}
<Dropdown
overlay={menu}
trigger={['click']}
overlayStyle={{ minWidth: 100 }}
placement="bottom"
>
<HeaderContainer
onMouseOver={(): void => setLocalHover(true)}
onMouseOut={(): void => setLocalHover(false)}
hover={localHover}
>
<HeaderContainer
onMouseOver={(): void => setLocalHover(true)}
onMouseOut={(): void => setLocalHover(false)}
hover={localHover}
onClick={onClickHandler}
>
<HeaderContentContainer>
<Typography.Text style={{ maxWidth: '80%' }} ellipsis>
{title}
</Typography.Text>
<ArrowContainer hover={parentHover}>
<DownOutlined />
</ArrowContainer>
</HeaderContentContainer>
</HeaderContainer>
</Dropdown>
{queryResponse.isFetching && !queryResponse.isError && (
<Spinner height="5vh" style={spinnerStyles} />
)}
{queryResponse.isError && (
<Tooltip title={errorMessage} placement={errorTooltipPosition}>
<ExclamationCircleOutlined style={tooltipStyles} />
</Tooltip>
)}
</div>
<HeaderContentContainer onClick={(e): void => e.preventDefault()}>
<Typography.Text style={{ maxWidth: '80%' }} ellipsis>
{title}
</Typography.Text>
<ArrowContainer hover={parentHover}>
<DownOutlined />
</ArrowContainer>
</HeaderContentContainer>
</HeaderContainer>
</Dropdown>
);
}

View File

@@ -1,6 +1,12 @@
import { grey } from '@ant-design/colors';
import styled from 'styled-components';
export const MenuItemContainer = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
`;
export const HeaderContainer = styled.div<{ hover: boolean }>`
width: 100%;
text-align: center;

View File

@@ -8,8 +8,6 @@ import { useTranslation } from 'react-i18next';
import { connect, useDispatch, useSelector } from 'react-redux';
import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import { AppDispatch } from 'store';
import { UpdateTimeInterval } from 'store/actions';
import {
ToggleAddWidget,
ToggleAddWidgetProps,
@@ -65,22 +63,12 @@ function GridGraph(props: Props): JSX.Element {
const [selectedDashboard] = dashboards;
const { data } = selectedDashboard;
const { widgets } = data;
const dispatch: AppDispatch = useDispatch<Dispatch<AppActions>>();
const dispatch = useDispatch<Dispatch<AppActions>>();
const [layouts, setLayout] = useState<LayoutProps[]>(
getPreLayouts(widgets, selectedDashboard.data.layout || []),
);
const onDragSelect = useCallback(
(start: number, end: number) => {
const startTimestamp = Math.trunc(start);
const endTimestamp = Math.trunc(end);
dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp]));
},
[dispatch],
);
useEffect(() => {
(async (): Promise<void> => {
if (!isAddWidget) {
@@ -194,18 +182,15 @@ function GridGraph(props: Props): JSX.Element {
yAxisUnit={currentWidget?.yAxisUnit}
layout={layout}
setLayout={setLayout}
onDragSelect={onDragSelect}
/>
),
};
}),
);
},
[widgets, onDragSelect],
[widgets],
);
const [notifications, NotificationElement] = notification.useNotification();
const onEmptyWidgetHandler = useCallback(async () => {
try {
const id = 'empty';
@@ -221,25 +206,22 @@ function GridGraph(props: Props): JSX.Element {
...(data.layout || []),
];
await UpdateDashboard(
{
data,
generateWidgetId: id,
graphType: 'EMPTY_WIDGET',
selectedDashboard,
layout,
isRedirected: false,
},
notifications,
);
await UpdateDashboard({
data,
generateWidgetId: id,
graphType: 'EMPTY_WIDGET',
selectedDashboard,
layout,
isRedirected: false,
});
setLayoutFunction(layout);
} catch (error) {
notifications.error({
notification.error({
message: error instanceof Error ? error.toString() : 'Something went wrong',
});
}
}, [data, selectedDashboard, setLayoutFunction, notifications]);
}, [data, selectedDashboard, setLayoutFunction]);
const onLayoutChangeHandler = async (layout: Layout[]): Promise<void> => {
setLayoutFunction(layout);
@@ -260,7 +242,7 @@ function GridGraph(props: Props): JSX.Element {
toggleAddWidget(true);
})
.catch(() => {
notifications.error(t('something_went_wrong'));
notification.error(t('something_went_wrong'));
});
} else {
toggleAddWidget(true);
@@ -268,29 +250,26 @@ function GridGraph(props: Props): JSX.Element {
}
} catch (error) {
if (typeof error === 'string') {
notifications.error({
notification.error({
message: error || t('something_went_wrong'),
});
}
}
}, [layouts, onEmptyWidgetHandler, t, toggleAddWidget, notifications]);
}, [layouts, onEmptyWidgetHandler, t, toggleAddWidget]);
return (
<>
{NotificationElement}
<GraphLayoutContainer
{...{
addPanelLoading,
layouts,
onAddPanelHandler,
onLayoutChangeHandler,
onLayoutSaveHandler,
saveLayoutState,
widgets,
setLayout,
}}
/>
</>
<GraphLayoutContainer
{...{
addPanelLoading,
layouts,
onAddPanelHandler,
onLayoutChangeHandler,
onLayoutSaveHandler,
saveLayoutState,
widgets,
setLayout,
}}
/>
);
}

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
import { Table } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import { ResizeTable } from 'components/ResizeTable';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { License } from 'types/api/licenses/def';
@@ -13,29 +13,25 @@ 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 <ResizeTable columns={columns} rowKey="id" dataSource={licenses} />;
return <Table rowKey="id" dataSource={licenses} columns={columns} />;
}
interface ListLicensesProps {

View File

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

View File

@@ -1,8 +1,7 @@
/* eslint-disable react/display-name */
import { PlusOutlined } from '@ant-design/icons';
import { notification, Typography } from 'antd';
import { notification, Table, 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';
@@ -31,8 +30,6 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
role,
);
const [notificationsApi, NotificationElement] = notification.useNotification();
useInterval(() => {
(async (): Promise<void> => {
const { data: refetchData, status } = await refetch();
@@ -40,7 +37,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
setData(refetchData?.payload || []);
}
if (status === 'error') {
notificationsApi.error({
notification.error({
message: t('something_went_wrong'),
});
}
@@ -61,7 +58,6 @@ 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) -
@@ -71,7 +67,6 @@ 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) -
@@ -87,7 +82,6 @@ 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) -
@@ -105,7 +99,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
dataIndex: 'labels',
key: 'tags',
align: 'center',
width: 100,
width: 350,
render: (value): JSX.Element => {
const objectKeys = Object.keys(value);
const withOutSeverityKeys = objectKeys.filter((e) => e !== 'severity');
@@ -116,11 +110,13 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
return (
<>
{withOutSeverityKeys.map((e) => (
<StyledTag key={e} color="magenta">
{e}: {value[e]}
</StyledTag>
))}
{withOutSeverityKeys.map((e) => {
return (
<StyledTag key={e} color="magenta">
{e}: {value[e]}
</StyledTag>
);
})}
</>
);
},
@@ -132,27 +128,27 @@ 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} />
render: (id: GettableAlert['id'], record): JSX.Element => {
return (
<>
<ToggleAlertState disabled={record.disabled} setData={setData} id={id} />
<ColumnButton
onClick={(): void => onEditHandler(id.toString())}
type="link"
>
Edit
</ColumnButton>
<ColumnButton
onClick={(): void => onEditHandler(id.toString())}
type="link"
>
Edit
</ColumnButton>
<DeleteAlert notifications={notifications} setData={setData} id={id} />
</>
),
<DeleteAlert notifications={notifications} setData={setData} id={id} />
</>
);
},
});
}
return (
<>
{NotificationElement}
{Element}
<ButtonContainer>
@@ -169,7 +165,8 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
</Button>
)}
</ButtonContainer>
<ResizeTable columns={columns} rowKey="id" dataSource={data} />
<Table rowKey="id" columns={columns} dataSource={data} />
</>
);
}

View File

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

View File

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

View File

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

View File

@@ -184,14 +184,16 @@ function SearchFilter({
{optionsData.options &&
Array.isArray(optionsData.options) &&
optionsData.options.map(
(optionItem): JSX.Element => (
<Select.Option
key={(optionItem.value as string) || (optionItem.name as string)}
value={optionItem.value || optionItem.name}
>
{optionItem.name}
</Select.Option>
),
(optionItem): JSX.Element => {
return (
<Select.Option
key={(optionItem.value as string) || (optionItem.name as string)}
value={optionItem.value || optionItem.name}
>
{optionItem.name}
</Select.Option>
);
},
)}
</Select>
)}

View File

@@ -19,7 +19,9 @@ export const convertQueriesToURLQuery = (
export const convertURLQueryStringToQuery = (
queryString: string,
): IQueryStructure[] => JSON.parse(decode(queryString));
): IQueryStructure[] => {
return JSON.parse(decode(queryString));
};
export const resolveOperator = (
result: unknown,

View File

@@ -1,8 +1,15 @@
import { PlusOutlined } from '@ant-design/icons';
import { Card, Dropdown, Menu, Row, TableColumnProps, Typography } from 'antd';
import {
Card,
Dropdown,
Menu,
Row,
Table,
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';
@@ -67,24 +74,20 @@ 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();
@@ -95,7 +98,6 @@ 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();
@@ -112,7 +114,6 @@ function ListOfAllDashboard(): JSX.Element {
title: 'Action',
dataIndex: '',
key: 'x',
width: 40,
render: DeleteButton,
});
}
@@ -270,8 +271,7 @@ function ListOfAllDashboard(): JSX.Element {
uploadedGrafana={uploadedGrafana}
onModalHandler={(): void => onModalHandler(false)}
/>
<ResizeTable
columns={columns}
<Table
pagination={{
pageSize: 9,
defaultPageSize: 9,
@@ -280,6 +280,7 @@ function ListOfAllDashboard(): JSX.Element {
bordered
sticky
loading={loading}
columns={columns}
dataSource={data}
showSorterTooltip
/>

View File

@@ -4,8 +4,6 @@ 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';
@@ -15,7 +13,6 @@ 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';
@@ -31,33 +28,18 @@ 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: {
logLinesPerPage: e,
},
payload: e,
});
};
const handleGoToLatest = (): void => {
const { maxTime, minTime } = getMinMax(
globalTime.selectedTime,
globalTime.minTime,
globalTime.maxTime,
);
dispatch({
type: RESET_ID_START_AND_END,
payload: getGlobalTime(globalTime.selectedTime, {
maxTime,
minTime,
}),
});
};

View File

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

View File

@@ -1,8 +1,7 @@
import { blue, orange } from '@ant-design/colors';
import { Input } from 'antd';
import { Input, Table } 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';
@@ -31,11 +30,13 @@ function TableView({ logData }: TableViewProps): JSX.Element | null {
flattenLogData !== null &&
Object.keys(flattenLogData)
.filter((field) => fieldSearchFilter(field, fieldSearchInput))
.map((key) => ({
key,
field: key,
value: JSON.stringify(flattenLogData[key]),
}));
.map((key) => {
return {
key,
field: key,
value: JSON.stringify(flattenLogData[key]),
};
});
if (!dataSource) {
return null;
@@ -57,7 +58,7 @@ function TableView({ logData }: TableViewProps): JSX.Element | null {
title: 'Field',
dataIndex: 'field',
key: 'field',
width: 100,
width: '35%',
render: (field: string): JSX.Element => {
const fieldKey = field.split('.').slice(-1);
const renderedField = <span style={{ color: blue[4] }}>{field}</span>;
@@ -65,6 +66,7 @@ function TableView({ logData }: TableViewProps): JSX.Element | null {
if (!RESTRICTED_FIELDS.includes(fieldKey[0])) {
return (
<AddToQueryHOC fieldKey={fieldKey[0]} fieldValue={flattenLogData[field]}>
{' '}
{renderedField}
</AddToQueryHOC>
);
@@ -76,16 +78,15 @@ 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
@@ -94,10 +95,11 @@ function TableView({ logData }: TableViewProps): JSX.Element | null {
value={fieldSearchInput}
onChange={(e): void => setFieldSearchInput(e.target.value)}
/>
<ResizeTable
columns={columns as never}
<Table
// scroll={{ x: true }}
tableLayout="fixed"
dataSource={dataSource}
columns={columns as never}
pagination={false}
/>
</div>

View File

@@ -55,26 +55,23 @@ function LogLiveTail(): JSX.Element {
const batchedEventsRef = useRef<Record<string, unknown>[]>([]);
const pushLiveLog = useCallback(() => {
dispatch({
type: PUSH_LIVE_TAIL_EVENT,
payload: batchedEventsRef.current.reverse(),
});
batchedEventsRef.current = [];
}, [dispatch]);
const pushLiveLogThrottled = useMemo(() => throttle(pushLiveLog, 1000), [
pushLiveLog,
]);
const batchLiveLog = useCallback(
(e: { data: string }): void => {
batchedEventsRef.current.push(JSON.parse(e.data as string) as never);
pushLiveLogThrottled();
},
[pushLiveLogThrottled],
// eslint-disable-next-line react-hooks/exhaustive-deps
const pushLiveLog = useCallback(
throttle(() => {
dispatch({
type: PUSH_LIVE_TAIL_EVENT,
payload: batchedEventsRef.current.reverse(),
});
batchedEventsRef.current = [];
}, 1500),
[],
);
const batchLiveLog = (e: { data: string }): void => {
batchedEventsRef.current.push(JSON.parse(e.data as string) as never);
pushLiveLog();
};
// This ref depicts thats whether the live tail is played from paused state or not.
const liveTailSourceRef = useRef<EventSource | null>(null);
useEffect(() => {

View File

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

View File

@@ -77,8 +77,8 @@ function LogsAggregate({ getLogsAggregate }: LogsAggregateProps): JSX.Element {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getLogsAggregate, maxTime, minTime, liveTail]);
const graphData = useMemo(
() => ({
const graphData = useMemo(() => {
return {
labels: logsAggregate.map((s) => new Date(s.timestamp / 1000000)),
datasets: [
{
@@ -86,9 +86,8 @@ function LogsAggregate({ getLogsAggregate }: LogsAggregateProps): JSX.Element {
backgroundColor: blue[4],
},
],
}),
[logsAggregate],
);
};
}, [logsAggregate]);
return (
<Container>

View File

@@ -1,17 +1,20 @@
import { LoadingOutlined } from '@ant-design/icons';
import { Button, Popover, Spin, Typography } from 'antd';
import { Button, Popover, Spin } from 'antd';
import { useIsDarkMode } from 'hooks/useDarkMode';
import React, { useCallback, useMemo, useState } from 'react';
import {
IField,
IInterestingFields,
ISelectedFields,
} from 'types/api/logs/fields';
import React, { useState } from 'react';
import { ICON_STYLE } from './config';
import { Field } from './styles';
function FieldItem({
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({
name,
buttonIcon,
buttonOnClick,
@@ -20,65 +23,33 @@ function FieldItem({
isLoading,
iconHoverText,
}: FieldItemProps): JSX.Element {
const [isHovered, setIsHovered] = useState<boolean>(false);
const [isHovered, setIsHovered] = useState(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={onMouseHoverHandler(true)}
onMouseLeave={onMouseHoverHandler(false)}
onMouseEnter={(): void => {
setIsHovered(true);
}}
onMouseLeave={(): void => setIsHovered(false)}
isDarkMode={isDarkMode}
>
<Typography style={ICON_STYLE.PLUS}>{name}</Typography>
{renderContent}
<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>
)
)}
</Field>
);
}
interface FieldItemProps {
name: string;
buttonIcon: React.ReactNode;
buttonOnClick: (props: {
fieldData: IInterestingFields | ISelectedFields;
fieldIndex: number;
}) => void;
fieldData: IField;
fieldIndex: number;
isLoading: boolean;
iconHoverText: string;
}
export default FieldItem;

View File

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

View File

@@ -1,19 +1,27 @@
/* 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, { useCallback, useState } from 'react';
import { useSelector } from 'react-redux';
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 { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import { IInterestingFields, ISelectedFields } from 'types/api/logs/fields';
import { ILogsReducer } from 'types/reducer/logs';
import { ICON_STYLE } from './config';
import FieldItem from './FieldItem';
import { FieldItem } from './FieldItem';
import { CategoryContainer, Container, FieldContainer } from './styles';
import { IHandleInterestProps, IHandleRemoveInterestProps } from './types';
import { onHandleAddInterest, onHandleRemoveInterest } from './utils';
function LogsFilters(): JSX.Element {
const RESTRICTED_SELECTED_FIELDS = ['timestamp', 'id'];
function LogsFilters({ getLogsFields }: LogsFiltersProps): JSX.Element {
const {
fields: { interesting, selected },
} = useSelector<AppState, ILogsReducer>((state) => state.logs);
@@ -28,40 +36,57 @@ function LogsFilters(): JSX.Element {
setFilterValuesInput((e.target as HTMLInputElement).value);
};
const onHandleAddSelectedToInteresting = useCallback(
({ fieldData, fieldIndex }: IHandleInterestProps) => (): Promise<void> =>
onHandleAddInterest({
fieldData,
fieldIndex,
interesting,
interestingFieldLoading,
setInterestingFieldLoading,
selected,
}),
[interesting, interestingFieldLoading, selected],
);
const handleAddInterestingToSelected = async ({
fieldData,
fieldIndex,
}: {
fieldData: IInterestingFields;
fieldIndex: number;
}): Promise<void> => {
setInterestingFieldLoading((prevState: number[]) => {
prevState.push(fieldIndex);
return [...prevState];
});
const onHandleRemoveSelected = useCallback(
({
fieldData,
fieldIndex,
}: IHandleRemoveInterestProps) => (): Promise<void> =>
onHandleRemoveInterest({
fieldData,
fieldIndex,
interesting,
interestingFieldLoading,
selected,
setSelectedFieldLoading,
}),
[interesting, interestingFieldLoading, selected, setSelectedFieldLoading],
);
await AddToSelectedFields({
...fieldData,
selected: true,
});
getLogsFields();
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}
/>
@@ -73,15 +98,15 @@ function LogsFilters(): JSX.Element {
.filter((field) => fieldSearchFilter(field.name, filterValuesInput))
.map((field, idx) => (
<FieldItem
key={`${JSON.stringify(field)}`}
key={`${JSON.stringify(field)}-${idx}`}
name={field.name}
fieldData={field}
fieldData={field as never}
fieldIndex={idx}
buttonIcon={<CloseOutlined style={ICON_STYLE.CLOSE} />}
buttonOnClick={onHandleRemoveSelected({
fieldData: field,
fieldIndex: idx,
})}
buttonIcon={<CloseOutlined style={{ color: red[5] }} />}
buttonOnClick={
(!RESTRICTED_SELECTED_FIELDS.includes(field.name) &&
handleRemoveSelectedField) as never
}
isLoading={selectedFieldLoading.includes(idx)}
iconHoverText="Remove from Selected Fields"
/>
@@ -95,23 +120,33 @@ function LogsFilters(): JSX.Element {
.filter((field) => fieldSearchFilter(field.name, filterValuesInput))
.map((field, idx) => (
<FieldItem
key={`${JSON.stringify(field)}`}
key={`${JSON.stringify(field)}-${idx}`}
name={field.name}
fieldData={field}
fieldData={field as never}
fieldIndex={idx}
buttonIcon={<PlusCircleFilled style={ICON_STYLE.PLUS} />}
buttonOnClick={onHandleAddSelectedToInteresting({
fieldData: field,
fieldIndex: idx,
})}
buttonIcon={<PlusCircleFilled />}
buttonOnClick={handleAddInterestingToSelected as never}
isLoading={interestingFieldLoading.includes(idx)}
iconHoverText="Add to Selected Fields"
/>
))}
</FieldContainer>
</CategoryContainer>
{/* <ExtractField>Extract Fields</ExtractField> */}
</Container>
);
}
export default LogsFilters;
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));

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