mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-05 18:40:56 +08:00
commit
3b01bb2614
25
.github/workflows/repo-stats.yml
vendored
25
.github/workflows/repo-stats.yml
vendored
@ -1,25 +0,0 @@
|
||||
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 }}
|
2
Makefile
2
Makefile
@ -54,7 +54,7 @@ build-push-frontend:
|
||||
@echo "--> Building and pushing frontend docker image"
|
||||
@echo "------------------"
|
||||
@cd $(FRONTEND_DIRECTORY) && \
|
||||
docker buildx build --file Dockerfile --progress plane --push --platform linux/amd64 \
|
||||
docker buildx build --file Dockerfile --progress plane --push --platform linux/arm64,linux/amd64 \
|
||||
--tag $(REPONAME)/$(FRONTEND_DOCKER_IMAGE):$(DOCKER_TAG) .
|
||||
|
||||
# Steps to build and push docker image of query service
|
||||
|
@ -23,7 +23,7 @@
|
||||
|
||||
##
|
||||
|
||||
SigNoz helps developers monitor applications and troubleshoot problems in their deployed applications. SigNoz uses distributed tracing to gain visibility into your software stack.
|
||||
SigNoz helps developers monitor applications and troubleshoot problems in their deployed applications. With SigNoz, you can:
|
||||
|
||||
👉 Visualise Metrics, Traces and Logs in a single pane of glass
|
||||
|
||||
|
@ -137,7 +137,7 @@ services:
|
||||
condition: on-failure
|
||||
|
||||
query-service:
|
||||
image: signoz/query-service:0.14.0
|
||||
image: signoz/query-service:0.15.0
|
||||
command: ["-config=/root/config/prometheus.yml"]
|
||||
# ports:
|
||||
# - "6060:6060" # pprof port
|
||||
@ -166,7 +166,7 @@ services:
|
||||
<<: *clickhouse-depend
|
||||
|
||||
frontend:
|
||||
image: signoz/frontend:0.14.0
|
||||
image: signoz/frontend:0.15.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.2
|
||||
image: signoz/signoz-otel-collector:0.66.3
|
||||
command: ["--config=/etc/otel-collector-config.yaml"]
|
||||
user: root # required for reading docker container logs
|
||||
volumes:
|
||||
@ -207,7 +207,7 @@ services:
|
||||
<<: *clickhouse-depend
|
||||
|
||||
otel-collector-metrics:
|
||||
image: signoz/signoz-otel-collector:0.66.2
|
||||
image: signoz/signoz-otel-collector:0.66.3
|
||||
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
|
||||
volumes:
|
||||
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
|
||||
|
@ -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.2
|
||||
image: signoz/signoz-otel-collector:0.66.3
|
||||
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.2
|
||||
image: signoz/signoz-otel-collector:0.66.3
|
||||
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
|
||||
volumes:
|
||||
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
|
||||
|
@ -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.14.0}
|
||||
image: signoz/query-service:${DOCKER_TAG:-0.15.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.14.0}
|
||||
image: signoz/frontend:${DOCKER_TAG:-0.15.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.2}
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.66.3}
|
||||
command: ["--config=/etc/otel-collector-config.yaml"]
|
||||
user: root # required for reading docker container logs
|
||||
volumes:
|
||||
@ -218,7 +218,7 @@ services:
|
||||
<<: *clickhouse-depend
|
||||
|
||||
otel-collector-metrics:
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.66.2}
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.66.3}
|
||||
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
|
||||
volumes:
|
||||
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
|
||||
|
@ -511,13 +511,15 @@ 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 Getting Started?"
|
||||
echo "👉 Need help in 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!"
|
||||
|
@ -30,6 +30,7 @@ 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"
|
||||
@ -271,8 +272,9 @@ 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 {
|
||||
@ -282,7 +284,8 @@ 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, &requestBody)
|
||||
json.Unmarshal(bodyBytes, &postData)
|
||||
|
||||
} else {
|
||||
return nil, false
|
||||
}
|
||||
@ -291,31 +294,20 @@ func extractDashboardMetaData(path string, r *http.Request) (map[string]interfac
|
||||
return nil, false
|
||||
}
|
||||
|
||||
compositeMetricQuery, compositeMetricQueryExists := requestBody["compositeMetricQuery"]
|
||||
signozMetricNotFound := false
|
||||
|
||||
signozMetricFound := false
|
||||
if postData != nil {
|
||||
signozMetricNotFound = telemetry.GetInstance().CheckSigNozMetricsV2(postData.CompositeMetricQuery)
|
||||
|
||||
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
|
||||
if postData.CompositeMetricQuery != nil {
|
||||
data["queryType"] = postData.CompositeMetricQuery.QueryType
|
||||
data["panelType"] = postData.CompositeMetricQuery.PanelType
|
||||
}
|
||||
|
||||
data["datasource"] = postData.DataSource
|
||||
}
|
||||
|
||||
datasource, datasourceExists := requestBody["dataSource"]
|
||||
if datasourceExists {
|
||||
data["datasource"] = datasource
|
||||
}
|
||||
|
||||
if !signozMetricFound {
|
||||
if signozMetricNotFound {
|
||||
telemetry.GetInstance().AddActiveMetricsUser()
|
||||
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_DASHBOARDS_METADATA, data, true)
|
||||
}
|
||||
|
@ -102,9 +102,10 @@ 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',
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
cd frontend && npm run commitlint
|
||||
cd frontend && yarn run commitlint --edit $1
|
||||
|
1
frontend/.yarnrc
Normal file
1
frontend/.yarnrc
Normal file
@ -0,0 +1 @@
|
||||
network-timeout 600000
|
@ -9,8 +9,9 @@ ARG TARGETARCH
|
||||
|
||||
WORKDIR /frontend
|
||||
|
||||
# Copy the package.json to install dependencies
|
||||
# Copy the package.json and .yarnrc files prior to install dependencies
|
||||
COPY package.json ./
|
||||
COPY .yarnrc ./
|
||||
|
||||
# Install the dependencies and make the folder
|
||||
RUN CI=1 yarn install
|
||||
|
@ -44,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.4.0",
|
||||
"chart.js": "3.9.1",
|
||||
"chartjs-adapter-date-fns": "^2.0.0",
|
||||
"chartjs-plugin-annotation": "^1.4.0",
|
||||
"color": "^4.2.1",
|
||||
@ -70,16 +70,18 @@
|
||||
"less-loader": "^10.2.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"mini-css-extract-plugin": "2.4.5",
|
||||
"react": "17.0.0",
|
||||
"react-dom": "17.0.0",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.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",
|
||||
@ -132,8 +134,8 @@
|
||||
"@types/lodash-es": "^4.17.4",
|
||||
"@types/mini-css-extract-plugin": "^2.5.1",
|
||||
"@types/node": "^16.10.3",
|
||||
"@types/react": "^17.0.0",
|
||||
"@types/react-dom": "^16.9.9",
|
||||
"@types/react": "18.0.26",
|
||||
"@types/react-dom": "18.0.10",
|
||||
"@types/react-grid-layout": "^1.1.2",
|
||||
"@types/react-redux": "^7.1.11",
|
||||
"@types/react-router-dom": "^5.1.6",
|
||||
@ -186,7 +188,7 @@
|
||||
]
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/react": "17.0.0",
|
||||
"@types/react-dom": "17.0.0"
|
||||
"@types/react": "18.0.26",
|
||||
"@types/react-dom": "18.0.10"
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import axios from 'api';
|
||||
import { ApiV2Instance as axios } from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
@ -8,9 +8,7 @@ const query = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`/variables/query?query=${encodeURIComponent(props.query)}`,
|
||||
);
|
||||
const response = await axios.post(`/variables/query`, props);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
|
@ -12,7 +12,9 @@ const getSpans = async (
|
||||
const updatedSelectedTags = props.selectedTags.map((e) => ({
|
||||
Key: e.Key[0],
|
||||
Operator: e.Operator,
|
||||
Values: e.Values,
|
||||
StringValues: e.StringValues,
|
||||
NumberValues: e.NumberValues,
|
||||
BoolValues: e.BoolValues,
|
||||
}));
|
||||
|
||||
const exclude: string[] = [];
|
||||
|
@ -30,7 +30,9 @@ const getSpanAggregate = async (
|
||||
const updatedSelectedTags = props.selectedTags.map((e) => ({
|
||||
Key: e.Key[0],
|
||||
Operator: e.Operator,
|
||||
Values: e.Values,
|
||||
StringValues: e.StringValues,
|
||||
NumberValues: e.NumberValues,
|
||||
BoolValues: e.BoolValues,
|
||||
}));
|
||||
|
||||
const other = Object.fromEntries(props.selectedFilter);
|
||||
|
@ -11,9 +11,11 @@ const getTagValue = async (
|
||||
const response = await axios.post<PayloadProps>(`/getTagValues`, {
|
||||
start: props.start.toString(),
|
||||
end: props.end.toString(),
|
||||
tagKey: props.tagKey,
|
||||
tagKey: {
|
||||
Key: props.tagKey.Key,
|
||||
Type: props.tagKey.Type,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
|
321
frontend/src/components/Graph/Plugin/DragSelect.ts
Normal file
321
frontend/src/components/Graph/Plugin/DragSelect.ts
Normal file
@ -0,0 +1,321 @@
|
||||
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;
|
||||
};
|
164
frontend/src/components/Graph/Plugin/IntersectionCursor.ts
Normal file
164
frontend/src/components/Graph/Plugin/IntersectionCursor.ts
Normal file
@ -0,0 +1,164 @@
|
||||
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;
|
||||
};
|
@ -22,87 +22,86 @@ 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> => {
|
||||
return {
|
||||
id: 'htmlLegend',
|
||||
afterUpdate(chart): void {
|
||||
const ul = getOrCreateLegendList(chart, id || 'legend', isLonger);
|
||||
export const legend = (id: string, isLonger: boolean): Plugin<ChartType> => ({
|
||||
id: 'htmlLegend',
|
||||
afterUpdate(chart): void {
|
||||
const ul = getOrCreateLegendList(chart, id || 'legend', isLonger);
|
||||
|
||||
// Remove old legend items
|
||||
while (ul.firstChild) {
|
||||
ul.firstChild.remove();
|
||||
}
|
||||
// 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;
|
||||
// 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';
|
||||
// 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);
|
||||
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);
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
20
frontend/src/components/Graph/Plugin/utils.ts
Normal file
20
frontend/src/components/Graph/Plugin/utils.ts
Normal file
@ -0,0 +1,20 @@
|
||||
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,
|
||||
};
|
||||
}
|
8
frontend/src/components/Graph/helpers.ts
Normal file
8
frontend/src/components/Graph/helpers.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { themeColors } from 'constants/theme';
|
||||
|
||||
export const getAxisLabelColor = (currentTheme: string): string => {
|
||||
if (currentTheme === 'light') {
|
||||
return themeColors.black;
|
||||
}
|
||||
return themeColors.whiteCream;
|
||||
};
|
@ -27,8 +27,21 @@ import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
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';
|
||||
@ -64,6 +77,8 @@ function Graph({
|
||||
forceReRender,
|
||||
staticLine,
|
||||
containerHeight,
|
||||
onDragSelect,
|
||||
dragSelectColor,
|
||||
}: GraphProps): JSX.Element {
|
||||
const chartRef = useRef<HTMLCanvasElement>(null);
|
||||
const isDarkMode = useIsDarkMode();
|
||||
@ -91,7 +106,7 @@ function Graph({
|
||||
}
|
||||
|
||||
if (chartRef.current !== null) {
|
||||
const options: ChartOptions = {
|
||||
const options: CustomChartOptions = {
|
||||
animation: {
|
||||
duration: animate ? 200 : 0,
|
||||
},
|
||||
@ -148,6 +163,15 @@ function Graph({
|
||||
},
|
||||
},
|
||||
},
|
||||
[dragSelectPluginId]: createDragSelectPluginOptions(
|
||||
!!onDragSelect,
|
||||
onDragSelect,
|
||||
dragSelectColor,
|
||||
),
|
||||
[intersectionCursorPluginId]: createIntersectionCursorPluginOptions(
|
||||
!!onDragSelect,
|
||||
currentTheme === 'dark' ? 'white' : 'black',
|
||||
),
|
||||
},
|
||||
layout: {
|
||||
padding: 0,
|
||||
@ -177,6 +201,7 @@ function Graph({
|
||||
},
|
||||
},
|
||||
type: 'time',
|
||||
ticks: { color: getAxisLabelColor(currentTheme) },
|
||||
},
|
||||
y: {
|
||||
display: true,
|
||||
@ -185,6 +210,7 @@ function Graph({
|
||||
color: getGridColor(),
|
||||
},
|
||||
ticks: {
|
||||
color: getAxisLabelColor(currentTheme),
|
||||
// Include a dollar sign in the ticks
|
||||
callback(value) {
|
||||
return getYAxisFormattedValue(value.toString(), yAxisUnit);
|
||||
@ -211,7 +237,13 @@ function Graph({
|
||||
const chartHasData = hasData(data);
|
||||
const chartPlugins = [];
|
||||
|
||||
if (!chartHasData) chartPlugins.push(emptyGraph);
|
||||
if (chartHasData) {
|
||||
chartPlugins.push(createIntersectionCursorPlugin());
|
||||
chartPlugins.push(createDragSelectPlugin());
|
||||
} else {
|
||||
chartPlugins.push(emptyGraph);
|
||||
}
|
||||
|
||||
chartPlugins.push(legend(name, data.datasets.length > 3));
|
||||
|
||||
lineChartRef.current = new Chart(chartRef.current, {
|
||||
@ -234,6 +266,9 @@ function Graph({
|
||||
yAxisUnit,
|
||||
onClickHandler,
|
||||
staticLine,
|
||||
onDragSelect,
|
||||
dragSelectColor,
|
||||
currentTheme,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
@ -248,6 +283,13 @@ function Graph({
|
||||
);
|
||||
}
|
||||
|
||||
type CustomChartOptions = ChartOptions & {
|
||||
plugins: {
|
||||
[dragSelectPluginId]: DragSelectPluginOptions | false;
|
||||
[intersectionCursorPluginId]: IntersectionCursorPluginOptions | false;
|
||||
};
|
||||
};
|
||||
|
||||
interface GraphProps {
|
||||
animate?: boolean;
|
||||
type: ChartType;
|
||||
@ -260,6 +302,8 @@ interface GraphProps {
|
||||
forceReRender?: boolean | null | number;
|
||||
staticLine?: StaticLineProps | undefined;
|
||||
containerHeight?: string | number;
|
||||
onDragSelect?: (start: number, end: number) => void;
|
||||
dragSelectColor?: string;
|
||||
}
|
||||
|
||||
export interface StaticLineProps {
|
||||
@ -286,6 +330,8 @@ Graph.defaultProps = {
|
||||
yAxisUnit: undefined,
|
||||
forceReRender: undefined,
|
||||
staticLine: undefined,
|
||||
containerHeight: '85%',
|
||||
containerHeight: '90%',
|
||||
onDragSelect: undefined,
|
||||
dragSelectColor: undefined,
|
||||
};
|
||||
export default Graph;
|
||||
|
@ -1,14 +1,22 @@
|
||||
import { themeColors } from 'constants/theme';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const LegendsContainer = styled.div`
|
||||
height: 15%;
|
||||
height: 10%;
|
||||
|
||||
* {
|
||||
::-webkit-scrollbar {
|
||||
display: none !important;
|
||||
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};
|
||||
}
|
||||
|
||||
-ms-overflow-style: none !important; /* IE and Edge */
|
||||
scrollbar-width: none !important; /* Firefox */
|
||||
}
|
||||
`;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { blue, grey, orange } from '@ant-design/colors';
|
||||
import { CopyFilled, ExpandAltOutlined } from '@ant-design/icons';
|
||||
import { Button, Divider, Row, Typography } from 'antd';
|
||||
import { Button, Divider, notification, 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, Text, TextContainer } from './styles';
|
||||
import { Container, LogContainer, Text, TextContainer } from './styles';
|
||||
import { isValidLogField } from './util';
|
||||
|
||||
interface LogFieldProps {
|
||||
@ -89,40 +89,46 @@ function LogItem({ logData }: LogItemProps): JSX.Element {
|
||||
|
||||
const handleCopyJSON = (): void => {
|
||||
setCopy(JSON.stringify(logData, null, 2));
|
||||
notification.success({
|
||||
message: 'Copied to clipboard',
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<div style={{ maxWidth: '100%' }}>
|
||||
<div>
|
||||
<div>
|
||||
{'{'}
|
||||
<div style={{ marginLeft: '0.5rem' }}>
|
||||
<LogGeneralField
|
||||
fieldKey="log"
|
||||
fieldValue={flattenLogData.body as never}
|
||||
/>
|
||||
{flattenLogData.stream && (
|
||||
<LogContainer>
|
||||
<>
|
||||
<LogGeneralField
|
||||
fieldKey="stream"
|
||||
fieldValue={flattenLogData.stream as never}
|
||||
fieldKey="log"
|
||||
fieldValue={flattenLogData.body as never}
|
||||
/>
|
||||
)}
|
||||
<LogGeneralField
|
||||
fieldKey="timestamp"
|
||||
fieldValue={dayjs((flattenLogData.timestamp as never) / 1e6).format()}
|
||||
/>
|
||||
</div>
|
||||
{flattenLogData.stream && (
|
||||
<LogGeneralField
|
||||
fieldKey="stream"
|
||||
fieldValue={flattenLogData.stream as never}
|
||||
/>
|
||||
)}
|
||||
<LogGeneralField
|
||||
fieldKey="timestamp"
|
||||
fieldValue={dayjs((flattenLogData.timestamp as never) / 1e6).format()}
|
||||
/>
|
||||
</>
|
||||
</LogContainer>
|
||||
{'}'}
|
||||
</div>
|
||||
<div>
|
||||
{map(selected, (field) => {
|
||||
return isValidLogField(flattenLogData[field.name] as never) ? (
|
||||
{map(selected, (field) =>
|
||||
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 }} />
|
||||
|
@ -29,3 +29,7 @@ export const TextContainer = styled.div`
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const LogContainer = styled.div`
|
||||
margin-left: 0.5rem;
|
||||
`;
|
||||
|
@ -48,9 +48,9 @@ function ReleaseNote({ path }: ReleaseNoteProps): JSX.Element | null {
|
||||
(state) => state.app,
|
||||
);
|
||||
|
||||
const c = allComponentMap.find((item) => {
|
||||
return item.match(path, currentVersion, userFlags);
|
||||
});
|
||||
const c = allComponentMap.find((item) =>
|
||||
item.match(path, currentVersion, userFlags),
|
||||
);
|
||||
|
||||
if (!c) {
|
||||
return null;
|
||||
|
@ -4,9 +4,9 @@ import React from 'react';
|
||||
|
||||
import { SpinerStyle } from './styles';
|
||||
|
||||
function Spinner({ size, tip, height }: SpinnerProps): JSX.Element {
|
||||
function Spinner({ size, tip, height, style }: SpinnerProps): JSX.Element {
|
||||
return (
|
||||
<SpinerStyle height={height}>
|
||||
<SpinerStyle height={height} style={style}>
|
||||
<Spin spinning size={size} tip={tip} indicator={<LoadingOutlined spin />} />
|
||||
</SpinerStyle>
|
||||
);
|
||||
@ -16,11 +16,13 @@ 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;
|
||||
|
@ -6,18 +6,16 @@ import React from 'react';
|
||||
function TextToolTip({ text, url }: TextToolTipProps): JSX.Element {
|
||||
return (
|
||||
<Tooltip
|
||||
overlay={(): JSX.Element => {
|
||||
return (
|
||||
<div>
|
||||
{`${text} `}
|
||||
{url && (
|
||||
<a href={url} rel="noopener noreferrer" target="_blank">
|
||||
here
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
overlay={(): JSX.Element => (
|
||||
<div>
|
||||
{`${text} `}
|
||||
{url && (
|
||||
<a href={url} rel="noopener noreferrer" target="_blank">
|
||||
here
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<QuestionCircleFilled style={{ fontSize: '1.3125rem' }} />
|
||||
</Tooltip>
|
||||
|
@ -8,7 +8,6 @@ export const TextContainer = styled.div<TextContainerProps>`
|
||||
display: flex;
|
||||
|
||||
> button {
|
||||
margin-left: ${({ noButtonMargin }): string => {
|
||||
return noButtonMargin ? '0' : '0.5rem';
|
||||
}}
|
||||
margin-left: ${({ noButtonMargin }): string =>
|
||||
noButtonMargin ? '0' : '0.5rem'}
|
||||
`;
|
||||
|
@ -8,11 +8,11 @@ export const OperatorConversions: Array<{
|
||||
{
|
||||
label: 'IN',
|
||||
metricValue: '=~',
|
||||
traceValue: 'in',
|
||||
traceValue: 'In',
|
||||
},
|
||||
{
|
||||
label: 'Not IN',
|
||||
metricValue: '!~',
|
||||
traceValue: 'not in',
|
||||
traceValue: 'NotIn',
|
||||
},
|
||||
];
|
||||
|
42
frontend/src/constants/theme.ts
Normal file
42
frontend/src/constants/theme.ts
Normal file
@ -0,0 +1,42 @@
|
||||
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',
|
||||
};
|
||||
|
||||
export { themeColors };
|
@ -10,7 +10,8 @@ import {
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import { ColumnType } from 'antd/es/table';
|
||||
import { ColumnType, TablePaginationConfig } from 'antd/es/table';
|
||||
import { FilterValue, SorterResult } from 'antd/es/table/interface';
|
||||
import { ColumnsType } from 'antd/lib/table';
|
||||
import { FilterConfirmProps } from 'antd/lib/table/interface';
|
||||
import getAll from 'api/errors/getAll';
|
||||
@ -30,6 +31,7 @@ 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,
|
||||
@ -176,41 +178,45 @@ function AllErrors(): JSX.Element {
|
||||
);
|
||||
|
||||
const filterDropdownWrapper = useCallback(
|
||||
({ 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>
|
||||
);
|
||||
},
|
||||
({
|
||||
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>
|
||||
),
|
||||
[getUpdatedExceptionType, getUpdatedServiceName, handleSearch],
|
||||
);
|
||||
|
||||
const onExceptionTypeFilter = useCallback(
|
||||
(value, record: Exception): boolean => {
|
||||
const onExceptionTypeFilter: ColumnType<Exception>['onFilter'] = useCallback(
|
||||
(value: unknown, record: Exception): boolean => {
|
||||
if (record.exceptionType && typeof value === 'string') {
|
||||
return record.exceptionType.toLowerCase().includes(value.toLowerCase());
|
||||
}
|
||||
@ -220,7 +226,7 @@ function AllErrors(): JSX.Element {
|
||||
);
|
||||
|
||||
const onApplicationTypeFilter = useCallback(
|
||||
(value, record: Exception): boolean => {
|
||||
(value: unknown, record: Exception): boolean => {
|
||||
if (record.serviceName && typeof value === 'string') {
|
||||
return record.serviceName.toLowerCase().includes(value.toLowerCase());
|
||||
}
|
||||
@ -343,7 +349,11 @@ function AllErrors(): JSX.Element {
|
||||
];
|
||||
|
||||
const onChangeHandler: TableProps<Exception>['onChange'] = useCallback(
|
||||
(paginations, filters, sorter) => {
|
||||
(
|
||||
paginations: TablePaginationConfig,
|
||||
filters: Record<string, FilterValue | null>,
|
||||
sorter: SorterResult<Exception>[] | SorterResult<Exception>,
|
||||
) => {
|
||||
if (!Array.isArray(sorter)) {
|
||||
const { pageSize = 0, current = 0 } = paginations;
|
||||
const { columnKey = '', order } = sorter;
|
||||
|
9
frontend/src/container/AllError/types.ts
Normal file
9
frontend/src/container/AllError/types.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { FilterDropdownProps } from 'antd/es/table/interface';
|
||||
|
||||
export interface FilterDropdownExtendsProps {
|
||||
placeholder: string;
|
||||
filterKey: string;
|
||||
confirm: FilterDropdownProps['confirm'];
|
||||
setSelectedKeys: FilterDropdownProps['setSelectedKeys'];
|
||||
selectedKeys: FilterDropdownProps['selectedKeys'];
|
||||
}
|
@ -20,15 +20,14 @@ export const urlKey = {
|
||||
serviceName: 'serviceName',
|
||||
};
|
||||
|
||||
export const isOrderParams = (orderBy: string | null): orderBy is OrderBy => {
|
||||
return !!(
|
||||
export const isOrderParams = (orderBy: string | null): orderBy is OrderBy =>
|
||||
!!(
|
||||
orderBy === 'serviceName' ||
|
||||
orderBy === 'exceptionCount' ||
|
||||
orderBy === 'lastSeen' ||
|
||||
orderBy === 'firstSeen' ||
|
||||
orderBy === 'exceptionType'
|
||||
);
|
||||
};
|
||||
|
||||
export const getOrder = (order: string | null): Order => {
|
||||
if (isOrder(order)) {
|
||||
@ -82,12 +81,9 @@ export const getDefaultOrder = (
|
||||
return undefined;
|
||||
};
|
||||
|
||||
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 getNanoSeconds = (date: string): string =>
|
||||
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) {
|
||||
|
@ -78,16 +78,17 @@ function CreateAlertChannels({
|
||||
[type, selectedConfig],
|
||||
);
|
||||
|
||||
const prepareSlackRequest = useCallback(() => {
|
||||
return {
|
||||
const prepareSlackRequest = useCallback(
|
||||
() => ({
|
||||
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);
|
||||
|
@ -47,8 +47,8 @@ function EditAlertChannels({
|
||||
setType(value as ChannelType);
|
||||
}, []);
|
||||
|
||||
const prepareSlackRequest = useCallback(() => {
|
||||
return {
|
||||
const prepareSlackRequest = useCallback(
|
||||
() => ({
|
||||
api_url: selectedConfig?.api_url || '',
|
||||
channel: selectedConfig?.channel || '',
|
||||
name: selectedConfig?.name || '',
|
||||
@ -56,8 +56,9 @@ function EditAlertChannels({
|
||||
text: selectedConfig?.text || '',
|
||||
title: selectedConfig?.title || '',
|
||||
id,
|
||||
};
|
||||
}, [id, selectedConfig]);
|
||||
}),
|
||||
[id, selectedConfig],
|
||||
);
|
||||
|
||||
const onSlackEditHandler = useCallback(async () => {
|
||||
setSavingState(true);
|
||||
@ -143,8 +144,8 @@ function EditAlertChannels({
|
||||
setSavingState(false);
|
||||
}, [prepareWebhookRequest, t, notifications, selectedConfig]);
|
||||
|
||||
const preparePagerRequest = useCallback(() => {
|
||||
return {
|
||||
const preparePagerRequest = useCallback(
|
||||
() => ({
|
||||
name: selectedConfig.name || '',
|
||||
routing_key: selectedConfig.routing_key,
|
||||
client: selectedConfig.client,
|
||||
@ -157,8 +158,9 @@ function EditAlertChannels({
|
||||
details: selectedConfig.details,
|
||||
detailsArray: JSON.parse(selectedConfig.details || '{}'),
|
||||
id,
|
||||
};
|
||||
}, [id, selectedConfig]);
|
||||
}),
|
||||
[id, selectedConfig],
|
||||
);
|
||||
|
||||
const onPagerEditHandler = useCallback(async () => {
|
||||
setSavingState(true);
|
||||
|
@ -32,6 +32,8 @@ 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 => {
|
||||
return { ...src, name: 'A', rawQuery: src.query };
|
||||
};
|
||||
export const toIClickHouseQuery = (src: IChQuery): IClickHouseQuery => ({
|
||||
...src,
|
||||
name: 'A',
|
||||
rawQuery: src.query,
|
||||
});
|
||||
|
@ -211,78 +211,70 @@ function QuerySection({
|
||||
setFormulaQueries({ ...formulas });
|
||||
}, [formulaQueries, setFormulaQueries]);
|
||||
|
||||
const renderPromqlUI = (): JSX.Element => {
|
||||
return (
|
||||
<PromqlSection promQueries={promQueries} setPromQueries={setPromQueries} />
|
||||
);
|
||||
};
|
||||
const renderPromqlUI = (): JSX.Element => (
|
||||
<PromqlSection promQueries={promQueries} setPromQueries={setPromQueries} />
|
||||
);
|
||||
|
||||
const renderChQueryUI = (): JSX.Element => {
|
||||
return <ChQuerySection chQueries={chQueries} setChQueries={setChQueries} />;
|
||||
};
|
||||
const renderChQueryUI = (): JSX.Element => (
|
||||
<ChQuerySection chQueries={chQueries} setChQueries={setChQueries} />
|
||||
);
|
||||
|
||||
const renderFormulaButton = (): JSX.Element => {
|
||||
return (
|
||||
<QueryButton onClick={addFormula} icon={<PlusOutlined />}>
|
||||
{t('button_formula')}
|
||||
</QueryButton>
|
||||
);
|
||||
};
|
||||
const renderFormulaButton = (): JSX.Element => (
|
||||
<QueryButton onClick={addFormula} icon={<PlusOutlined />}>
|
||||
{t('button_formula')}
|
||||
</QueryButton>
|
||||
);
|
||||
|
||||
const renderQueryButton = (): JSX.Element => {
|
||||
return (
|
||||
<QueryButton onClick={addMetricQuery} icon={<PlusOutlined />}>
|
||||
{t('button_query')}
|
||||
</QueryButton>
|
||||
);
|
||||
};
|
||||
const renderQueryButton = (): JSX.Element => (
|
||||
<QueryButton onClick={addMetricQuery} icon={<PlusOutlined />}>
|
||||
{t('button_query')}
|
||||
</QueryButton>
|
||||
);
|
||||
|
||||
const renderMetricUI = (): JSX.Element => {
|
||||
return (
|
||||
<div>
|
||||
{metricQueries &&
|
||||
Object.keys(metricQueries).map((key: string) => {
|
||||
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) => {
|
||||
// todo(amol): need to handle this in fetch
|
||||
const current = metricQueries[key];
|
||||
const current = formulaQueries[key];
|
||||
current.name = key;
|
||||
|
||||
return (
|
||||
<MetricsBuilder
|
||||
<MetricsBuilderFormula
|
||||
key={key}
|
||||
queryIndex={key}
|
||||
queryData={toIMetricsBuilderQuery(current)}
|
||||
selectedGraph="TIME_SERIES"
|
||||
handleQueryChange={handleMetricQueryChange}
|
||||
formulaIndex={key}
|
||||
formulaData={current}
|
||||
handleFormulaChange={handleFormulaChange}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{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>
|
||||
{queryCategory === EQueryType.QUERY_BUILDER &&
|
||||
(!formulaQueries || Object.keys(formulaQueries).length === 0) &&
|
||||
metricQueries &&
|
||||
Object.keys(metricQueries).length > 0 &&
|
||||
renderFormulaButton()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
</div>
|
||||
);
|
||||
|
||||
const handleRunQuery = (): void => {
|
||||
runQuery();
|
||||
|
@ -38,106 +38,94 @@ function RuleOptions({
|
||||
});
|
||||
};
|
||||
|
||||
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) || '';
|
||||
const renderCompareOps = (): JSX.Element => (
|
||||
<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 => {
|
||||
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 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 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 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 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 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 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>
|
||||
);
|
||||
};
|
||||
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>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -15,154 +15,130 @@ function UserGuide({ queryType }: UserGuideProps): JSX.Element {
|
||||
// init namespace for translations
|
||||
const { t } = useTranslation('alerts');
|
||||
|
||||
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 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 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 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 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 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 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 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 renderGuideForPQL = (): JSX.Element => {
|
||||
return (
|
||||
<>
|
||||
{renderStep1PQL()}
|
||||
{renderStep2PQL()}
|
||||
{renderStep3PQL()}
|
||||
</>
|
||||
);
|
||||
};
|
||||
const renderGuideForPQL = (): JSX.Element => (
|
||||
<>
|
||||
{renderStep1PQL()}
|
||||
{renderStep2PQL()}
|
||||
{renderStep3PQL()}
|
||||
</>
|
||||
);
|
||||
|
||||
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 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 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 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 renderGuideForCH = (): JSX.Element => {
|
||||
return (
|
||||
<>
|
||||
{renderStep1CH()}
|
||||
{renderStep2CH()}
|
||||
{renderStep3CH()}
|
||||
</>
|
||||
);
|
||||
};
|
||||
const renderGuideForCH = (): JSX.Element => (
|
||||
<>
|
||||
{renderStep1CH()}
|
||||
{renderStep2CH()}
|
||||
{renderStep3CH()}
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<StyledMainContainer>
|
||||
<Row>
|
||||
|
@ -436,41 +436,35 @@ function FormAlertRules({
|
||||
<BasicInfo alertDef={alertDef} setAlertDef={setAlertDef} />
|
||||
);
|
||||
|
||||
const renderQBChartPreview = (): JSX.Element => {
|
||||
return (
|
||||
<ChartPreview
|
||||
headline={<PlotTag queryType={queryCategory} />}
|
||||
name=""
|
||||
threshold={alertDef.condition?.target}
|
||||
query={debouncedStagedQuery}
|
||||
selectedInterval={toChartInterval(alertDef.evalWindow)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
const renderQBChartPreview = (): JSX.Element => (
|
||||
<ChartPreview
|
||||
headline={<PlotTag queryType={queryCategory} />}
|
||||
name=""
|
||||
threshold={alertDef.condition?.target}
|
||||
query={debouncedStagedQuery}
|
||||
selectedInterval={toChartInterval(alertDef.evalWindow)}
|
||||
/>
|
||||
);
|
||||
|
||||
const renderPromChartPreview = (): JSX.Element => {
|
||||
return (
|
||||
<ChartPreview
|
||||
headline={<PlotTag queryType={queryCategory} />}
|
||||
name="Chart Preview"
|
||||
threshold={alertDef.condition?.target}
|
||||
query={debouncedStagedQuery}
|
||||
/>
|
||||
);
|
||||
};
|
||||
const renderPromChartPreview = (): JSX.Element => (
|
||||
<ChartPreview
|
||||
headline={<PlotTag queryType={queryCategory} />}
|
||||
name="Chart Preview"
|
||||
threshold={alertDef.condition?.target}
|
||||
query={debouncedStagedQuery}
|
||||
/>
|
||||
);
|
||||
|
||||
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)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
const renderChQueryChartPreview = (): JSX.Element => (
|
||||
<ChartPreview
|
||||
headline={<PlotTag queryType={queryCategory} />}
|
||||
name="Chart Preview"
|
||||
threshold={alertDef.condition?.target}
|
||||
query={manualStagedQuery}
|
||||
userQueryKey={runQueryId}
|
||||
selectedInterval={toChartInterval(alertDef.evalWindow)}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
{Element}
|
||||
|
@ -119,17 +119,15 @@ function LabelSelect({
|
||||
{queries.length > 0 &&
|
||||
map(
|
||||
queries,
|
||||
(query): JSX.Element => {
|
||||
return (
|
||||
<QueryChip key={query.key} queryData={query} onRemove={handleClose} />
|
||||
);
|
||||
},
|
||||
(query): JSX.Element => (
|
||||
<QueryChip key={query.key} queryData={query} onRemove={handleClose} />
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
{map(staging, (item) => {
|
||||
return <QueryChipItem key={uuid()}>{item}</QueryChipItem>;
|
||||
})}
|
||||
{map(staging, (item) => (
|
||||
<QueryChipItem key={uuid()}>{item}</QueryChipItem>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', width: '100%' }}>
|
||||
|
@ -42,17 +42,15 @@ export const toMetricQueries = (b: IBuilderQueries): IMetricQueries => {
|
||||
|
||||
export const toIMetricsBuilderQuery = (
|
||||
q: IMetricQuery,
|
||||
): IMetricsBuilderQuery => {
|
||||
return {
|
||||
name: q.name,
|
||||
metricName: q.metricName,
|
||||
tagFilters: q.tagFilters,
|
||||
groupBy: q.groupBy,
|
||||
aggregateOperator: q.aggregateOperator,
|
||||
disabled: q.disabled,
|
||||
legend: q.legend,
|
||||
};
|
||||
};
|
||||
): IMetricsBuilderQuery => ({
|
||||
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,
|
||||
|
@ -112,21 +112,19 @@ export const getNodeById = (
|
||||
|
||||
const getSpanWithoutChildren = (
|
||||
span: ITraceTree,
|
||||
): 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,
|
||||
};
|
||||
};
|
||||
): 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,
|
||||
});
|
||||
|
||||
export const isSpanPresentInSearchString = (
|
||||
searchedString: string,
|
||||
|
@ -590,20 +590,22 @@ function GeneralSettings({
|
||||
});
|
||||
|
||||
return (
|
||||
<Col xs={24} md={22} xl={20} xxl={18} style={{ margin: 'auto' }}>
|
||||
<>
|
||||
{Element}
|
||||
<ErrorTextContainer>
|
||||
<TextToolTip
|
||||
{...{
|
||||
text: `More details on how to set retention period`,
|
||||
url: 'https://signoz.io/docs/userguide/retention-period/',
|
||||
}}
|
||||
/>
|
||||
{errorText && <ErrorText>{errorText}</ErrorText>}
|
||||
</ErrorTextContainer>
|
||||
<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>
|
||||
|
||||
<Row justify="start">{renderConfig}</Row>
|
||||
</Col>
|
||||
<Row justify="start">{renderConfig}</Row>
|
||||
</Col>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -19,6 +19,7 @@ function GridGraphComponent({
|
||||
name,
|
||||
yAxisUnit,
|
||||
staticLine,
|
||||
onDragSelect,
|
||||
}: GridGraphComponentProps): JSX.Element | null {
|
||||
const location = history.location.pathname;
|
||||
|
||||
@ -38,6 +39,7 @@ function GridGraphComponent({
|
||||
name,
|
||||
yAxisUnit,
|
||||
staticLine,
|
||||
onDragSelect,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@ -85,6 +87,7 @@ export interface GridGraphComponentProps {
|
||||
name: string;
|
||||
yAxisUnit?: string;
|
||||
staticLine?: StaticLineProps;
|
||||
onDragSelect?: (start: number, end: number) => void;
|
||||
}
|
||||
|
||||
GridGraphComponent.defaultProps = {
|
||||
@ -94,6 +97,7 @@ GridGraphComponent.defaultProps = {
|
||||
onClickHandler: undefined,
|
||||
yAxisUnit: undefined,
|
||||
staticLine: undefined,
|
||||
onDragSelect: undefined,
|
||||
};
|
||||
|
||||
export default GridGraphComponent;
|
||||
|
@ -9,7 +9,7 @@ import {
|
||||
} from 'container/NewWidget/RightContainer/timeItems';
|
||||
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
|
||||
import getChartData from 'lib/getChartData';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { GetMetricQueryRange } from 'store/actions/dashboard/getQueryResults';
|
||||
@ -27,6 +27,7 @@ function FullView({
|
||||
onClickHandler,
|
||||
name,
|
||||
yAxisUnit,
|
||||
onDragSelect,
|
||||
}: FullViewProps): JSX.Element {
|
||||
const { selectedTime: globalSelectedTime } = useSelector<
|
||||
AppState,
|
||||
@ -57,6 +58,18 @@ function FullView({
|
||||
}),
|
||||
);
|
||||
|
||||
const chartDataSet = useMemo(
|
||||
() =>
|
||||
getChartData({
|
||||
queryData: [
|
||||
{
|
||||
queryData: response?.data?.payload?.data?.result || [],
|
||||
},
|
||||
],
|
||||
}),
|
||||
[response],
|
||||
);
|
||||
|
||||
const isLoading = response.isLoading === true;
|
||||
|
||||
if (isLoading) {
|
||||
@ -85,24 +98,15 @@ function FullView({
|
||||
)}
|
||||
|
||||
<GridGraphComponent
|
||||
{...{
|
||||
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,
|
||||
}}
|
||||
GRAPH_TYPES={widget.panelTypes}
|
||||
data={chartDataSet}
|
||||
isStacked={widget.isStacked}
|
||||
opacity={widget.opacity}
|
||||
title={widget.title}
|
||||
onClickHandler={onClickHandler}
|
||||
name={name}
|
||||
yAxisUnit={yAxisUnit}
|
||||
onDragSelect={onDragSelect}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
@ -114,12 +118,14 @@ 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;
|
||||
|
@ -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, useState } from 'react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { useQueries } from 'react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
@ -30,6 +30,7 @@ function FullView({
|
||||
onClickHandler,
|
||||
name,
|
||||
yAxisUnit,
|
||||
onDragSelect,
|
||||
}: FullViewProps): JSX.Element {
|
||||
const { minTime, maxTime, selectedTime: globalSelectedTime } = useSelector<
|
||||
AppState,
|
||||
@ -80,25 +81,22 @@ function FullView({
|
||||
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: 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) => ({
|
||||
// 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,
|
||||
})),
|
||||
);
|
||||
|
||||
const isError =
|
||||
@ -117,6 +115,18 @@ 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..." />;
|
||||
}
|
||||
@ -151,22 +161,15 @@ function FullView({
|
||||
)}
|
||||
|
||||
<GridGraphComponent
|
||||
{...{
|
||||
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,
|
||||
}}
|
||||
GRAPH_TYPES={widget.panelTypes}
|
||||
data={chartDataSet}
|
||||
isStacked={widget.isStacked}
|
||||
opacity={widget.opacity}
|
||||
title={widget.title}
|
||||
onClickHandler={onClickHandler}
|
||||
name={name}
|
||||
yAxisUnit={yAxisUnit}
|
||||
onDragSelect={onDragSelect}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
@ -178,12 +181,14 @@ 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;
|
||||
|
@ -1,13 +1,15 @@
|
||||
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, useEffect, useState } from 'react';
|
||||
import React, { memo, useCallback, useMemo, 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';
|
||||
@ -20,13 +22,14 @@ 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 { ErrorContainer, FullViewContainer, Modal } from './styles';
|
||||
import { FullViewContainer, Modal } from './styles';
|
||||
|
||||
function GridCardGraph({
|
||||
widget,
|
||||
@ -35,13 +38,14 @@ function GridCardGraph({
|
||||
yAxisUnit,
|
||||
layout = [],
|
||||
setLayout,
|
||||
onDragSelect,
|
||||
}: GridCardGraphProps): JSX.Element {
|
||||
const [state, setState] = useState<GridCardGraphState>({
|
||||
loading: true,
|
||||
errorMessage: '',
|
||||
error: false,
|
||||
payload: undefined,
|
||||
const { ref: graphRef, inView: isGraphVisible } = useInView({
|
||||
threshold: 0,
|
||||
triggerOnce: true,
|
||||
});
|
||||
|
||||
const [errorMessage, setErrorMessage] = useState<string | undefined>('');
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const [modal, setModal] = useState(false);
|
||||
const [deleteModal, setDeleteModal] = useState(false);
|
||||
@ -53,113 +57,57 @@ 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 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 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);
|
||||
}
|
||||
} 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 chartData = useMemo(
|
||||
() =>
|
||||
getChartData({
|
||||
queryData: [
|
||||
{
|
||||
queryData: queryResponse?.data?.payload?.data?.result || [],
|
||||
},
|
||||
],
|
||||
}),
|
||||
[queryResponse],
|
||||
);
|
||||
|
||||
const prevChartDataSetRef = usePreviousValue<ChartData>(chartData);
|
||||
|
||||
const onToggleModal = useCallback(
|
||||
(func: React.Dispatch<React.SetStateAction<boolean>>) => {
|
||||
@ -177,70 +125,114 @@ function GridCardGraph({
|
||||
onToggleModal(setDeleteModal);
|
||||
}, [deleteWidget, layout, onToggleModal, setLayout, widget]);
|
||||
|
||||
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>
|
||||
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>
|
||||
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
<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);
|
||||
};
|
||||
|
||||
const isEmptyLayout = widget?.id === 'empty' || isEmpty(widget);
|
||||
|
||||
if (state.error && !isEmptyLayout) {
|
||||
if (queryResponse.isError && !isEmptyLayout) {
|
||||
return (
|
||||
<>
|
||||
<span ref={graphRef}>
|
||||
{getModals()}
|
||||
<WidgetHeader
|
||||
parentHover={hovered}
|
||||
title={widget?.title}
|
||||
widget={widget}
|
||||
onView={(): void => onToggleModal(setModal)}
|
||||
onDelete={(): void => onToggleModal(setDeleteModal)}
|
||||
/>
|
||||
|
||||
<ErrorContainer>{state.errorMessage}</ErrorContainer>
|
||||
</>
|
||||
{!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>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
(state.loading === true || state.payload === undefined) &&
|
||||
!isEmptyLayout
|
||||
) {
|
||||
return <Spinner height="20vh" tip="Loading..." />;
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span
|
||||
ref={graphRef}
|
||||
onMouseOver={(): void => {
|
||||
setHovered(true);
|
||||
}}
|
||||
@ -255,28 +247,31 @@ function GridCardGraph({
|
||||
}}
|
||||
>
|
||||
{!isEmptyLayout && (
|
||||
<WidgetHeader
|
||||
parentHover={hovered}
|
||||
title={widget?.title}
|
||||
widget={widget}
|
||||
onView={(): void => onToggleModal(setModal)}
|
||||
onDelete={(): void => onToggleModal(setDeleteModal)}
|
||||
/>
|
||||
<div className="drag-handle">
|
||||
<WidgetHeader
|
||||
parentHover={hovered}
|
||||
title={widget?.title}
|
||||
widget={widget}
|
||||
onView={handleOnView}
|
||||
onDelete={handleOnDelete}
|
||||
queryResponse={queryResponse}
|
||||
errorMessage={errorMessage}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isEmptyLayout && getModals()}
|
||||
|
||||
{!isEmpty(widget) && !!state.payload && (
|
||||
{!isEmpty(widget) && !!queryResponse.data?.payload && (
|
||||
<GridGraphComponent
|
||||
{...{
|
||||
GRAPH_TYPES: widget.panelTypes,
|
||||
data: state.payload,
|
||||
isStacked: widget.isStacked,
|
||||
opacity: widget.opacity,
|
||||
title: ' ', // empty title to accommodate absolutely positioned widget header
|
||||
name,
|
||||
yAxisUnit,
|
||||
}}
|
||||
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}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -285,13 +280,6 @@ function GridCardGraph({
|
||||
);
|
||||
}
|
||||
|
||||
interface GridCardGraphState {
|
||||
loading: boolean;
|
||||
error: boolean;
|
||||
errorMessage: string;
|
||||
payload: ChartData | undefined;
|
||||
}
|
||||
|
||||
interface DispatchProps {
|
||||
deleteWidget: ({
|
||||
widgetId,
|
||||
@ -306,8 +294,13 @@ 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 => ({
|
||||
|
@ -72,6 +72,7 @@ function GraphLayout({
|
||||
useCSSTransforms
|
||||
allowOverlap={false}
|
||||
onLayoutChange={onLayoutChangeHandler}
|
||||
draggableHandle=".drag-handle"
|
||||
>
|
||||
{layouts.map(({ Component, ...rest }) => {
|
||||
const currentWidget = (widgets || [])?.find((e) => e.id === rest.i);
|
||||
|
@ -0,0 +1,22 @@
|
||||
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',
|
||||
};
|
@ -2,22 +2,33 @@ import {
|
||||
DeleteOutlined,
|
||||
DownOutlined,
|
||||
EditFilled,
|
||||
ExclamationCircleOutlined,
|
||||
FullscreenOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Dropdown, Menu, Typography } from 'antd';
|
||||
import { Dropdown, MenuProps, Tooltip, Typography } from 'antd';
|
||||
import { MenuItemType } from 'antd/es/menu/hooks/useItems';
|
||||
import Spinner from 'components/Spinner';
|
||||
import useComponentPermission from 'hooks/useComponentPermission';
|
||||
import history from 'lib/history';
|
||||
import React, { useState } from 'react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
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;
|
||||
@ -27,6 +38,10 @@ interface IWidgetHeaderProps {
|
||||
onView: VoidFunction;
|
||||
onDelete: VoidFunction;
|
||||
parentHover: boolean;
|
||||
queryResponse: UseQueryResult<
|
||||
SuccessResponse<MetricRangePayloadProps> | ErrorResponse
|
||||
>;
|
||||
errorMessage: string | undefined;
|
||||
}
|
||||
function WidgetHeader({
|
||||
title,
|
||||
@ -34,35 +49,49 @@ function WidgetHeader({
|
||||
onView,
|
||||
onDelete,
|
||||
parentHover,
|
||||
queryResponse,
|
||||
errorMessage,
|
||||
}: IWidgetHeaderProps): JSX.Element {
|
||||
const [localHover, setLocalHover] = useState(false);
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
|
||||
const onEditHandler = (): void => {
|
||||
const onEditHandler = useCallback((): 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 };
|
||||
} = {
|
||||
view: {
|
||||
key: 'view',
|
||||
method: onView,
|
||||
} = 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);
|
||||
}
|
||||
},
|
||||
edit: {
|
||||
key: 'edit',
|
||||
method: onEditHandler,
|
||||
},
|
||||
delete: {
|
||||
key: 'delete',
|
||||
method: onDelete,
|
||||
},
|
||||
};
|
||||
const onMenuItemSelectHandler = ({ key }: { key: TWidgetOptions }): void => {
|
||||
keyMethodMapping[key]?.method();
|
||||
};
|
||||
[keyMethodMapping],
|
||||
);
|
||||
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||
|
||||
const [deleteWidget, editWidget] = useComponentPermission(
|
||||
@ -70,57 +99,85 @@ function WidgetHeader({
|
||||
role,
|
||||
);
|
||||
|
||||
const menu = (
|
||||
<Menu onClick={onMenuItemSelectHandler}>
|
||||
<Menu.Item key={keyMethodMapping.view.key}>
|
||||
<MenuItemContainer>
|
||||
<span>View</span> <FullscreenOutlined />
|
||||
</MenuItemContainer>
|
||||
</Menu.Item>
|
||||
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,
|
||||
],
|
||||
);
|
||||
|
||||
{editWidget && (
|
||||
<Menu.Item key={keyMethodMapping.edit.key}>
|
||||
<MenuItemContainer>
|
||||
<span>Edit</span> <EditFilled />
|
||||
</MenuItemContainer>
|
||||
</Menu.Item>
|
||||
)}
|
||||
const onClickHandler = useCallback(() => {
|
||||
setIsOpen((open) => !open);
|
||||
}, []);
|
||||
|
||||
{deleteWidget && (
|
||||
<>
|
||||
<Menu.Divider />
|
||||
<Menu.Item key={keyMethodMapping.delete.key} danger>
|
||||
<MenuItemContainer>
|
||||
<span>Delete</span> <DeleteOutlined />
|
||||
</MenuItemContainer>
|
||||
</Menu.Item>
|
||||
</>
|
||||
)}
|
||||
</Menu>
|
||||
const menu = useMemo(
|
||||
() => ({
|
||||
items: menuList,
|
||||
onClick: onMenuItemSelectHandler,
|
||||
}),
|
||||
[menuList, onMenuItemSelectHandler],
|
||||
);
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
overlay={menu}
|
||||
trigger={['click']}
|
||||
overlayStyle={{ minWidth: 100 }}
|
||||
placement="bottom"
|
||||
>
|
||||
<HeaderContainer
|
||||
onMouseOver={(): void => setLocalHover(true)}
|
||||
onMouseOut={(): void => setLocalHover(false)}
|
||||
hover={localHover}
|
||||
<div>
|
||||
<Dropdown
|
||||
destroyPopupOnHide
|
||||
open={isOpen}
|
||||
onOpenChange={setIsOpen}
|
||||
menu={menu}
|
||||
trigger={['click']}
|
||||
overlayStyle={overlayStyles}
|
||||
>
|
||||
<HeaderContentContainer onClick={(e): void => e.preventDefault()}>
|
||||
<Typography.Text style={{ maxWidth: '80%' }} ellipsis>
|
||||
{title}
|
||||
</Typography.Text>
|
||||
<ArrowContainer hover={parentHover}>
|
||||
<DownOutlined />
|
||||
</ArrowContainer>
|
||||
</HeaderContentContainer>
|
||||
</HeaderContainer>
|
||||
</Dropdown>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,12 +1,6 @@
|
||||
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;
|
||||
|
@ -8,6 +8,8 @@ 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,
|
||||
@ -63,12 +65,22 @@ function GridGraph(props: Props): JSX.Element {
|
||||
const [selectedDashboard] = dashboards;
|
||||
const { data } = selectedDashboard;
|
||||
const { widgets } = data;
|
||||
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||
const dispatch: AppDispatch = 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) {
|
||||
@ -182,13 +194,14 @@ function GridGraph(props: Props): JSX.Element {
|
||||
yAxisUnit={currentWidget?.yAxisUnit}
|
||||
layout={layout}
|
||||
setLayout={setLayout}
|
||||
onDragSelect={onDragSelect}
|
||||
/>
|
||||
),
|
||||
};
|
||||
}),
|
||||
);
|
||||
},
|
||||
[widgets],
|
||||
[widgets, onDragSelect],
|
||||
);
|
||||
|
||||
const onEmptyWidgetHandler = useCallback(async () => {
|
||||
|
@ -110,13 +110,11 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
||||
|
||||
return (
|
||||
<>
|
||||
{withOutSeverityKeys.map((e) => {
|
||||
return (
|
||||
<StyledTag key={e} color="magenta">
|
||||
{e}: {value[e]}
|
||||
</StyledTag>
|
||||
);
|
||||
})}
|
||||
{withOutSeverityKeys.map((e) => (
|
||||
<StyledTag key={e} color="magenta">
|
||||
{e}: {value[e]}
|
||||
</StyledTag>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
},
|
||||
@ -128,22 +126,20 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
||||
title: 'Action',
|
||||
dataIndex: 'id',
|
||||
key: 'action',
|
||||
render: (id: GettableAlert['id'], record): JSX.Element => {
|
||||
return (
|
||||
<>
|
||||
<ToggleAlertState disabled={record.disabled} setData={setData} id={id} />
|
||||
render: (id: GettableAlert['id'], record): JSX.Element => (
|
||||
<>
|
||||
<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} />
|
||||
</>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -40,8 +40,8 @@ function ToggleAlertState({
|
||||
});
|
||||
|
||||
if (response.statusCode === 200) {
|
||||
setData((state) => {
|
||||
return state.map((alert) => {
|
||||
setData((state) =>
|
||||
state.map((alert) => {
|
||||
if (alert.id === id) {
|
||||
return {
|
||||
...alert,
|
||||
@ -50,8 +50,8 @@ function ToggleAlertState({
|
||||
};
|
||||
}
|
||||
return alert;
|
||||
});
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
setAPIStatus((state) => ({
|
||||
...state,
|
||||
|
@ -184,16 +184,14 @@ function SearchFilter({
|
||||
{optionsData.options &&
|
||||
Array.isArray(optionsData.options) &&
|
||||
optionsData.options.map(
|
||||
(optionItem): JSX.Element => {
|
||||
return (
|
||||
<Select.Option
|
||||
key={(optionItem.value as string) || (optionItem.name as string)}
|
||||
value={optionItem.value || optionItem.name}
|
||||
>
|
||||
{optionItem.name}
|
||||
</Select.Option>
|
||||
);
|
||||
},
|
||||
(optionItem): JSX.Element => (
|
||||
<Select.Option
|
||||
key={(optionItem.value as string) || (optionItem.name as string)}
|
||||
value={optionItem.value || optionItem.name}
|
||||
>
|
||||
{optionItem.name}
|
||||
</Select.Option>
|
||||
),
|
||||
)}
|
||||
</Select>
|
||||
)}
|
||||
|
@ -19,9 +19,7 @@ export const convertQueriesToURLQuery = (
|
||||
|
||||
export const convertURLQueryStringToQuery = (
|
||||
queryString: string,
|
||||
): IQueryStructure[] => {
|
||||
return JSON.parse(decode(queryString));
|
||||
};
|
||||
): IQueryStructure[] => JSON.parse(decode(queryString));
|
||||
|
||||
export const resolveOperator = (
|
||||
result: unknown,
|
||||
|
@ -30,13 +30,11 @@ function TableView({ logData }: TableViewProps): JSX.Element | null {
|
||||
flattenLogData !== null &&
|
||||
Object.keys(flattenLogData)
|
||||
.filter((field) => fieldSearchFilter(field, fieldSearchInput))
|
||||
.map((key) => {
|
||||
return {
|
||||
key,
|
||||
field: key,
|
||||
value: JSON.stringify(flattenLogData[key]),
|
||||
};
|
||||
});
|
||||
.map((key) => ({
|
||||
key,
|
||||
field: key,
|
||||
value: JSON.stringify(flattenLogData[key]),
|
||||
}));
|
||||
|
||||
if (!dataSource) {
|
||||
return null;
|
||||
|
@ -55,22 +55,25 @@ function LogLiveTail(): JSX.Element {
|
||||
|
||||
const batchedEventsRef = useRef<Record<string, unknown>[]>([]);
|
||||
|
||||
// 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 pushLiveLog = useCallback(() => {
|
||||
dispatch({
|
||||
type: PUSH_LIVE_TAIL_EVENT,
|
||||
payload: batchedEventsRef.current.reverse(),
|
||||
});
|
||||
batchedEventsRef.current = [];
|
||||
}, [dispatch]);
|
||||
|
||||
const batchLiveLog = (e: { data: string }): void => {
|
||||
batchedEventsRef.current.push(JSON.parse(e.data as string) as never);
|
||||
pushLiveLog();
|
||||
};
|
||||
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],
|
||||
);
|
||||
|
||||
// This ref depicts thats whether the live tail is played from paused state or not.
|
||||
const liveTailSourceRef = useRef<EventSource | null>(null);
|
||||
|
@ -157,18 +157,16 @@ function Login({
|
||||
}
|
||||
};
|
||||
|
||||
const renderSAMLAction = (): JSX.Element => {
|
||||
return (
|
||||
<Button
|
||||
type="primary"
|
||||
loading={isLoading}
|
||||
disabled={isLoading}
|
||||
href={precheckResult.ssoUrl}
|
||||
>
|
||||
{t('login_with_sso')}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
const renderSAMLAction = (): JSX.Element => (
|
||||
<Button
|
||||
type="primary"
|
||||
loading={isLoading}
|
||||
disabled={isLoading}
|
||||
href={precheckResult.ssoUrl}
|
||||
>
|
||||
{t('login_with_sso')}
|
||||
</Button>
|
||||
);
|
||||
|
||||
const renderOnSsoError = (): JSX.Element | null => {
|
||||
if (!ssoerror) {
|
||||
|
@ -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(() => {
|
||||
return {
|
||||
const graphData = useMemo(
|
||||
() => ({
|
||||
labels: logsAggregate.map((s) => new Date(s.timestamp / 1000000)),
|
||||
datasets: [
|
||||
{
|
||||
@ -86,8 +86,9 @@ function LogsAggregate({ getLogsAggregate }: LogsAggregateProps): JSX.Element {
|
||||
backgroundColor: blue[4],
|
||||
},
|
||||
],
|
||||
};
|
||||
}, [logsAggregate]);
|
||||
}),
|
||||
[logsAggregate],
|
||||
);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
|
@ -25,9 +25,7 @@ export const Field = styled.div<{ isDarkMode: boolean }>`
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
&:hover {
|
||||
background: ${({ isDarkMode }): string => {
|
||||
return isDarkMode ? grey[7] : '#ddd';
|
||||
}};
|
||||
background: ${({ isDarkMode }): string => (isDarkMode ? grey[7] : '#ddd')};
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -175,6 +175,7 @@ export interface QueryBuilderProps {
|
||||
onDropDownToggleHandler: (value: boolean) => VoidFunction;
|
||||
fieldsQuery: QueryFields[][];
|
||||
setFieldsQuery: (q: QueryFields[][]) => void;
|
||||
syncKeyPrefix: () => void;
|
||||
}
|
||||
|
||||
function QueryBuilder({
|
||||
@ -182,6 +183,7 @@ function QueryBuilder({
|
||||
fieldsQuery,
|
||||
setFieldsQuery,
|
||||
onDropDownToggleHandler,
|
||||
syncKeyPrefix,
|
||||
}: QueryBuilderProps): JSX.Element {
|
||||
const handleUpdate = (query: Query, queryIndex: number): void => {
|
||||
const updated = [...fieldsQuery];
|
||||
@ -195,6 +197,9 @@ function QueryBuilder({
|
||||
else updated.splice(queryIndex, 2);
|
||||
|
||||
setFieldsQuery(updated);
|
||||
|
||||
// initiate re-render query panel
|
||||
syncKeyPrefix();
|
||||
};
|
||||
|
||||
const QueryUI = (
|
||||
|
@ -47,6 +47,14 @@ function SearchFields({
|
||||
}
|
||||
}, [parsedQuery]);
|
||||
|
||||
// syncKeyPrefix initiates re-render. useful in situations like
|
||||
// delete field (in search panel). this method allows condiitonally
|
||||
// setting keyPrefix as doing it on every update of query initiates
|
||||
// a re-render. this is a problem for text fields where input focus goes away.
|
||||
const syncKeyPrefix = (): void => {
|
||||
keyPrefixRef.current = hashCode(JSON.stringify(fieldsQuery));
|
||||
};
|
||||
|
||||
const addSuggestedField = useCallback(
|
||||
(name: string): void => {
|
||||
if (!name) {
|
||||
@ -98,6 +106,7 @@ function SearchFields({
|
||||
onDropDownToggleHandler={onDropDownToggleHandler}
|
||||
fieldsQuery={fieldsQuery}
|
||||
setFieldsQuery={setFieldsQuery}
|
||||
syncKeyPrefix={syncKeyPrefix}
|
||||
/>
|
||||
<SearchFieldsActionBar
|
||||
applyUpdate={applyUpdate}
|
||||
|
@ -68,7 +68,7 @@ function SearchFilter({
|
||||
);
|
||||
|
||||
const handleSearch = useCallback(
|
||||
(customQuery) => {
|
||||
(customQuery: string) => {
|
||||
if (liveTail === 'PLAYING') {
|
||||
dispatch({
|
||||
type: TOGGLE_LIVE_TAIL,
|
||||
|
@ -1,6 +1,7 @@
|
||||
import history from 'lib/history';
|
||||
import { parseQuery, reverseParser } from 'lib/logql';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { ILogQLParsedQueryItem } from 'lib/logql/types';
|
||||
import isEqual from 'lodash-es/isEqual';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
@ -13,8 +14,8 @@ import { ILogsReducer } from 'types/reducer/logs';
|
||||
export function useSearchParser(): {
|
||||
queryString: string;
|
||||
parsedQuery: unknown;
|
||||
updateParsedQuery: (arg0: unknown) => void;
|
||||
updateQueryString: (arg0: unknown) => void;
|
||||
updateParsedQuery: (arg0: ILogQLParsedQueryItem[]) => void;
|
||||
updateQueryString: (arg0: string) => void;
|
||||
} {
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
@ -22,7 +23,7 @@ export function useSearchParser(): {
|
||||
} = useSelector<AppState, ILogsReducer>((store) => store.logs);
|
||||
|
||||
const updateQueryString = useCallback(
|
||||
(updatedQueryString) => {
|
||||
(updatedQueryString: string) => {
|
||||
history.replace({
|
||||
pathname: history.location.pathname,
|
||||
search: updatedQueryString ? `?q=${updatedQueryString}` : '',
|
||||
@ -48,7 +49,7 @@ export function useSearchParser(): {
|
||||
}, [queryString, updateQueryString]);
|
||||
|
||||
const updateParsedQuery = useCallback(
|
||||
(updatedParsedPayload) => {
|
||||
(updatedParsedPayload: ILogQLParsedQueryItem[]) => {
|
||||
dispatch({
|
||||
type: SET_SEARCH_QUERY_PARSED_PAYLOAD,
|
||||
payload: updatedParsedPayload,
|
||||
|
@ -1,10 +1,9 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import { Typography } from 'antd';
|
||||
import LogItem from 'components/Logs/LogItem';
|
||||
import Spinner from 'components/Spinner';
|
||||
import map from 'lodash-es/map';
|
||||
import React, { memo } from 'react';
|
||||
import React, { memo, useCallback, useMemo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Virtuoso } from 'react-virtuoso';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { ILogsReducer } from 'types/reducer/logs';
|
||||
|
||||
@ -15,6 +14,24 @@ function LogsTable(): JSX.Element {
|
||||
(state) => state.logs,
|
||||
);
|
||||
|
||||
const isLiveTail = useMemo(() => logs.length === 0 && liveTail === 'PLAYING', [
|
||||
logs?.length,
|
||||
liveTail,
|
||||
]);
|
||||
|
||||
const isNoLogs = useMemo(() => logs.length === 0 && liveTail === 'STOPPED', [
|
||||
logs?.length,
|
||||
liveTail,
|
||||
]);
|
||||
|
||||
const getItemContent = useCallback(
|
||||
(index: number): JSX.Element => {
|
||||
const log = logs[index];
|
||||
return <LogItem key={log.id} logData={log} />;
|
||||
},
|
||||
[logs],
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return <Spinner height={20} tip="Getting Logs" />;
|
||||
}
|
||||
@ -24,13 +41,15 @@ function LogsTable(): JSX.Element {
|
||||
<Heading>
|
||||
<Typography.Text>Event</Typography.Text>
|
||||
</Heading>
|
||||
{Array.isArray(logs) && logs.length > 0 ? (
|
||||
map(logs, (log) => <LogItem key={log.id} logData={log} />)
|
||||
) : liveTail === 'PLAYING' ? (
|
||||
<span>Getting live logs...</span>
|
||||
) : (
|
||||
<span>No log lines found</span>
|
||||
)}
|
||||
{isLiveTail && <Typography>Getting live logs...</Typography>}
|
||||
|
||||
{isNoLogs && <Typography>No log lines found</Typography>}
|
||||
|
||||
<Virtuoso
|
||||
useWindowScroll
|
||||
totalCount={logs.length}
|
||||
itemContent={getItemContent}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
@ -0,0 +1,20 @@
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
export const getWidgetQueryBuilder = (query: Widgets['query']): Widgets => ({
|
||||
description: '',
|
||||
id: v4(),
|
||||
isStacked: false,
|
||||
nullZeroValues: '',
|
||||
opacity: '0',
|
||||
panelTypes: 'TIME_SERIES',
|
||||
query,
|
||||
queryData: {
|
||||
data: { queryData: [] },
|
||||
error: false,
|
||||
errorMessage: '',
|
||||
loading: false,
|
||||
},
|
||||
timePreferance: 'GLOBAL_TIME',
|
||||
title: '',
|
||||
});
|
@ -19,13 +19,21 @@ export const databaseCallsRPS = ({
|
||||
} => {
|
||||
const metricName = 'signoz_db_latency_count';
|
||||
const groupBy = ['db_system'];
|
||||
const itemsA = [
|
||||
{
|
||||
id: '',
|
||||
key: 'service_name',
|
||||
op: 'IN',
|
||||
value: [`${servicename}`],
|
||||
},
|
||||
...tagFilterItems,
|
||||
];
|
||||
|
||||
return getQueryBuilderQueries({
|
||||
metricName,
|
||||
legend,
|
||||
groupBy,
|
||||
servicename,
|
||||
tagFilterItems,
|
||||
legend,
|
||||
itemsA,
|
||||
});
|
||||
};
|
||||
|
||||
@ -42,14 +50,24 @@ export const databaseCallsAvgDuration = ({
|
||||
const legendFormula = '';
|
||||
const legend = '';
|
||||
const disabled = true;
|
||||
const additionalItemsA = [
|
||||
{
|
||||
id: '',
|
||||
key: 'service_name',
|
||||
op: 'IN',
|
||||
value: [`${servicename}`],
|
||||
},
|
||||
...tagFilterItems,
|
||||
];
|
||||
const additionalItemsB = additionalItemsA;
|
||||
|
||||
return getQueryBuilderQuerieswithFormula({
|
||||
servicename,
|
||||
legend,
|
||||
disabled,
|
||||
tagFilterItems,
|
||||
metricNameA,
|
||||
metricNameB,
|
||||
additionalItemsA,
|
||||
additionalItemsB,
|
||||
legend,
|
||||
disabled,
|
||||
expression,
|
||||
legendFormula,
|
||||
});
|
||||
|
@ -6,7 +6,6 @@ import {
|
||||
|
||||
import {
|
||||
getQueryBuilderQueries,
|
||||
getQueryBuilderQuerieswithAdditionalItems,
|
||||
getQueryBuilderQuerieswithFormula,
|
||||
} from './MetricsPageQueriesFactory';
|
||||
|
||||
@ -22,25 +21,41 @@ export const externalCallErrorPercent = ({
|
||||
} => {
|
||||
const metricNameA = 'signoz_external_call_latency_count';
|
||||
const metricNameB = 'signoz_external_call_latency_count';
|
||||
const additionalItems = {
|
||||
id: '',
|
||||
key: 'status_code',
|
||||
op: 'IN',
|
||||
value: ['STATUS_CODE_ERROR'],
|
||||
};
|
||||
|
||||
const additionalItemsA = [
|
||||
{
|
||||
id: '',
|
||||
key: 'service_name',
|
||||
op: 'IN',
|
||||
value: [`${servicename}`],
|
||||
},
|
||||
{
|
||||
id: '',
|
||||
key: 'status_code',
|
||||
op: 'IN',
|
||||
value: ['STATUS_CODE_ERROR'],
|
||||
},
|
||||
...tagFilterItems,
|
||||
];
|
||||
const additionalItemsB = [
|
||||
{
|
||||
id: '',
|
||||
key: 'service_name',
|
||||
op: 'IN',
|
||||
value: [`${servicename}`],
|
||||
},
|
||||
...tagFilterItems,
|
||||
];
|
||||
const legendFormula = 'External Call Error Percentage';
|
||||
const expression = 'A*100/B';
|
||||
const disabled = true;
|
||||
return getQueryBuilderQuerieswithAdditionalItems({
|
||||
return getQueryBuilderQuerieswithFormula({
|
||||
metricNameA,
|
||||
metricNameB,
|
||||
additionalItems,
|
||||
servicename,
|
||||
additionalItemsA,
|
||||
additionalItemsB,
|
||||
legend,
|
||||
groupBy,
|
||||
disabled,
|
||||
tagFilterItems,
|
||||
expression,
|
||||
legendFormula,
|
||||
});
|
||||
@ -59,14 +74,24 @@ export const externalCallDuration = ({
|
||||
const legendFormula = 'Average Duration';
|
||||
const legend = '';
|
||||
const disabled = true;
|
||||
const additionalItemsA = [
|
||||
{
|
||||
id: '',
|
||||
key: 'service_name',
|
||||
op: 'IN',
|
||||
value: [`${servicename}`],
|
||||
},
|
||||
...tagFilterItems,
|
||||
];
|
||||
const additionalItemsB = additionalItemsA;
|
||||
|
||||
return getQueryBuilderQuerieswithFormula({
|
||||
servicename,
|
||||
legend,
|
||||
disabled,
|
||||
tagFilterItems,
|
||||
metricNameA,
|
||||
metricNameB,
|
||||
additionalItemsA,
|
||||
additionalItemsB,
|
||||
legend,
|
||||
disabled,
|
||||
expression,
|
||||
legendFormula,
|
||||
});
|
||||
@ -81,12 +106,20 @@ export const externalCallRpsByAddress = ({
|
||||
queryBuilder: IMetricsBuilderQuery[];
|
||||
} => {
|
||||
const metricName = 'signoz_external_call_latency_count';
|
||||
const itemsA = [
|
||||
{
|
||||
id: '',
|
||||
key: 'service_name',
|
||||
op: 'IN',
|
||||
value: [`${servicename}`],
|
||||
},
|
||||
...tagFilterItems,
|
||||
];
|
||||
return getQueryBuilderQueries({
|
||||
servicename,
|
||||
legend,
|
||||
tagFilterItems,
|
||||
metricName,
|
||||
groupBy,
|
||||
legend,
|
||||
itemsA,
|
||||
});
|
||||
};
|
||||
|
||||
@ -103,16 +136,27 @@ export const externalCallDurationByAddress = ({
|
||||
const expression = 'A/B';
|
||||
const legendFormula = legend;
|
||||
const disabled = true;
|
||||
const additionalItemsA = [
|
||||
{
|
||||
id: '',
|
||||
key: 'service_name',
|
||||
op: 'IN',
|
||||
value: [`${servicename}`],
|
||||
},
|
||||
...tagFilterItems,
|
||||
];
|
||||
const additionalItemsB = additionalItemsA;
|
||||
|
||||
return getQueryBuilderQuerieswithFormula({
|
||||
servicename,
|
||||
legend,
|
||||
disabled,
|
||||
tagFilterItems,
|
||||
metricNameA,
|
||||
metricNameB,
|
||||
additionalItemsA,
|
||||
additionalItemsB,
|
||||
legend,
|
||||
groupBy,
|
||||
disabled,
|
||||
expression,
|
||||
legendFormula,
|
||||
groupBy,
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -4,14 +4,11 @@ import {
|
||||
IQueryBuilderTagFilterItems,
|
||||
} from 'types/api/dashboard/getAll';
|
||||
|
||||
import { ExternalCallProps } from './ExternalQueries';
|
||||
|
||||
export const getQueryBuilderQueries = ({
|
||||
metricName,
|
||||
groupBy,
|
||||
servicename,
|
||||
legend,
|
||||
tagFilterItems,
|
||||
itemsA,
|
||||
}: BuilderQueriesProps): {
|
||||
formulas: IMetricsBuilderFormula[];
|
||||
queryBuilder: IMetricsBuilderQuery[];
|
||||
@ -27,15 +24,7 @@ export const getQueryBuilderQueries = ({
|
||||
name: 'A',
|
||||
reduceTo: 1,
|
||||
tagFilters: {
|
||||
items: [
|
||||
{
|
||||
id: '',
|
||||
key: 'service_name',
|
||||
op: 'IN',
|
||||
value: [`${servicename}`],
|
||||
},
|
||||
...tagFilterItems,
|
||||
],
|
||||
items: itemsA,
|
||||
op: 'AND',
|
||||
},
|
||||
},
|
||||
@ -43,90 +32,18 @@ export const getQueryBuilderQueries = ({
|
||||
});
|
||||
|
||||
export const getQueryBuilderQuerieswithFormula = ({
|
||||
servicename,
|
||||
legend,
|
||||
disabled,
|
||||
tagFilterItems,
|
||||
metricNameA,
|
||||
metricNameB,
|
||||
additionalItemsA,
|
||||
additionalItemsB,
|
||||
legend,
|
||||
groupBy,
|
||||
disabled,
|
||||
expression,
|
||||
legendFormula,
|
||||
}: BuilderQuerieswithFormulaProps): {
|
||||
formulas: IMetricsBuilderFormula[];
|
||||
queryBuilder: IMetricsBuilderQuery[];
|
||||
} => {
|
||||
return {
|
||||
formulas: [
|
||||
{
|
||||
disabled: false,
|
||||
expression,
|
||||
name: 'F1',
|
||||
legend: legendFormula,
|
||||
},
|
||||
],
|
||||
queryBuilder: [
|
||||
{
|
||||
aggregateOperator: 18,
|
||||
disabled,
|
||||
groupBy,
|
||||
legend,
|
||||
metricName: metricNameA,
|
||||
name: 'A',
|
||||
reduceTo: 1,
|
||||
tagFilters: {
|
||||
items: [
|
||||
{
|
||||
id: '',
|
||||
key: 'service_name',
|
||||
op: 'IN',
|
||||
value: [`${servicename}`],
|
||||
},
|
||||
...tagFilterItems,
|
||||
],
|
||||
|
||||
op: 'AND',
|
||||
},
|
||||
},
|
||||
{
|
||||
aggregateOperator: 18,
|
||||
disabled,
|
||||
groupBy,
|
||||
legend,
|
||||
metricName: metricNameB,
|
||||
name: 'B',
|
||||
reduceTo: 1,
|
||||
tagFilters: {
|
||||
items: [
|
||||
{
|
||||
id: '',
|
||||
key: 'service_name',
|
||||
op: 'IN',
|
||||
value: [`${servicename}`],
|
||||
},
|
||||
...tagFilterItems,
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
export const getQueryBuilderQuerieswithAdditionalItems = ({
|
||||
servicename,
|
||||
legend,
|
||||
disabled,
|
||||
tagFilterItems,
|
||||
metricNameA,
|
||||
metricNameB,
|
||||
groupBy,
|
||||
expression,
|
||||
legendFormula,
|
||||
additionalItems,
|
||||
}: BuilderQuerieswithAdditionalItems): {
|
||||
formulas: IMetricsBuilderFormula[];
|
||||
queryBuilder: IMetricsBuilderQuery[];
|
||||
} => ({
|
||||
formulas: [
|
||||
{
|
||||
@ -146,17 +63,7 @@ export const getQueryBuilderQuerieswithAdditionalItems = ({
|
||||
name: 'A',
|
||||
reduceTo: 1,
|
||||
tagFilters: {
|
||||
items: [
|
||||
{
|
||||
id: '',
|
||||
key: 'service_name',
|
||||
op: 'IN',
|
||||
value: [`${servicename}`],
|
||||
},
|
||||
additionalItems,
|
||||
...tagFilterItems,
|
||||
],
|
||||
|
||||
items: additionalItemsA,
|
||||
op: 'AND',
|
||||
},
|
||||
},
|
||||
@ -169,28 +76,21 @@ export const getQueryBuilderQuerieswithAdditionalItems = ({
|
||||
name: 'B',
|
||||
reduceTo: 1,
|
||||
tagFilters: {
|
||||
items: [
|
||||
{
|
||||
id: '',
|
||||
key: 'service_name',
|
||||
op: 'IN',
|
||||
value: [`${servicename}`],
|
||||
},
|
||||
...tagFilterItems,
|
||||
],
|
||||
items: additionalItemsB,
|
||||
op: 'AND',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
interface BuilderQueriesProps extends ExternalCallProps {
|
||||
interface BuilderQueriesProps {
|
||||
metricName: string;
|
||||
groupBy?: string[];
|
||||
legend: string;
|
||||
itemsA: IQueryBuilderTagFilterItems[];
|
||||
}
|
||||
|
||||
interface BuilderQuerieswithFormulaProps extends ExternalCallProps {
|
||||
interface BuilderQuerieswithFormulaProps {
|
||||
metricNameA: string;
|
||||
metricNameB: string;
|
||||
legend: string;
|
||||
@ -198,9 +98,6 @@ interface BuilderQuerieswithFormulaProps extends ExternalCallProps {
|
||||
groupBy?: string[];
|
||||
expression: string;
|
||||
legendFormula: string;
|
||||
}
|
||||
|
||||
interface BuilderQuerieswithAdditionalItems
|
||||
extends BuilderQuerieswithFormulaProps {
|
||||
additionalItems: IQueryBuilderTagFilterItems;
|
||||
additionalItemsA: IQueryBuilderTagFilterItems[];
|
||||
additionalItemsB: IQueryBuilderTagFilterItems[];
|
||||
}
|
||||
|
@ -0,0 +1,112 @@
|
||||
import {
|
||||
IMetricsBuilderFormula,
|
||||
IMetricsBuilderQuery,
|
||||
IQueryBuilderTagFilterItems,
|
||||
} from 'types/api/dashboard/getAll';
|
||||
|
||||
import {
|
||||
getQueryBuilderQueries,
|
||||
getQueryBuilderQuerieswithFormula,
|
||||
} from './MetricsPageQueriesFactory';
|
||||
|
||||
export const operationPerSec = ({
|
||||
servicename,
|
||||
tagFilterItems,
|
||||
topLevelOperations,
|
||||
}: OperationPerSecProps): IOverviewQueries => {
|
||||
const metricName = 'signoz_latency_count';
|
||||
const legend = 'Operations';
|
||||
const itemsA = [
|
||||
{
|
||||
id: '',
|
||||
key: 'service_name',
|
||||
op: 'IN',
|
||||
value: [`${servicename}`],
|
||||
},
|
||||
{
|
||||
id: '',
|
||||
key: 'operation',
|
||||
op: 'MATCH',
|
||||
value: topLevelOperations,
|
||||
},
|
||||
...tagFilterItems,
|
||||
];
|
||||
|
||||
return getQueryBuilderQueries({
|
||||
metricName,
|
||||
legend,
|
||||
itemsA,
|
||||
});
|
||||
};
|
||||
|
||||
export const errorPercentage = ({
|
||||
servicename,
|
||||
tagFilterItems,
|
||||
topLevelOperations,
|
||||
}: OperationPerSecProps): IOverviewQueries => {
|
||||
const metricNameA = 'signoz_calls_total';
|
||||
const metricNameB = 'signoz_calls_total';
|
||||
const additionalItemsA = [
|
||||
{
|
||||
id: '',
|
||||
key: 'service_name',
|
||||
op: 'IN',
|
||||
value: [`${servicename}`],
|
||||
},
|
||||
{
|
||||
id: '',
|
||||
key: 'operation',
|
||||
op: 'MATCH',
|
||||
value: topLevelOperations,
|
||||
},
|
||||
{
|
||||
id: '',
|
||||
key: 'status_code',
|
||||
op: 'IN',
|
||||
value: ['STATUS_CODE_ERROR'],
|
||||
},
|
||||
...tagFilterItems,
|
||||
];
|
||||
|
||||
const additionalItemsB = [
|
||||
{
|
||||
id: '',
|
||||
key: 'service_name',
|
||||
op: 'IN',
|
||||
value: [`${servicename}`],
|
||||
},
|
||||
{
|
||||
id: '',
|
||||
key: 'operation',
|
||||
op: 'MATCH',
|
||||
value: topLevelOperations,
|
||||
},
|
||||
...tagFilterItems,
|
||||
];
|
||||
|
||||
const legendFormula = 'Error Percentage';
|
||||
const legend = legendFormula;
|
||||
const expression = 'A*100/B';
|
||||
const disabled = true;
|
||||
return getQueryBuilderQuerieswithFormula({
|
||||
metricNameA,
|
||||
metricNameB,
|
||||
additionalItemsA,
|
||||
additionalItemsB,
|
||||
legend,
|
||||
disabled,
|
||||
expression,
|
||||
legendFormula,
|
||||
});
|
||||
};
|
||||
|
||||
export interface OperationPerSecProps {
|
||||
servicename: string | undefined;
|
||||
tagFilterItems: IQueryBuilderTagFilterItems[];
|
||||
topLevelOperations: string[];
|
||||
}
|
||||
|
||||
interface IOverviewQueries {
|
||||
formulas: IMetricsBuilderFormula[];
|
||||
queryBuilder: IMetricsBuilderQuery[];
|
||||
}
|
@ -164,24 +164,20 @@ function ResourceAttributesFilter(): JSX.Element | null {
|
||||
>
|
||||
{map(
|
||||
queries,
|
||||
(query): JSX.Element => {
|
||||
return (
|
||||
<QueryChip
|
||||
disabled={disabled}
|
||||
key={query.id}
|
||||
queryData={query}
|
||||
onClose={handleClose}
|
||||
/>
|
||||
);
|
||||
},
|
||||
(query): JSX.Element => (
|
||||
<QueryChip
|
||||
disabled={disabled}
|
||||
key={query.id}
|
||||
queryData={query}
|
||||
onClose={handleClose}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
{map(staging, (item, idx) => {
|
||||
return (
|
||||
<QueryChipItem key={uuid()}>
|
||||
{idx === 0 ? convertMetricKeyToTrace(item) : item}
|
||||
</QueryChipItem>
|
||||
);
|
||||
})}
|
||||
{map(staging, (item, idx) => (
|
||||
<QueryChipItem key={uuid()}>
|
||||
{idx === 0 ? convertMetricKeyToTrace(item) : item}
|
||||
</QueryChipItem>
|
||||
))}
|
||||
</div>
|
||||
{!disabled && (
|
||||
<Select
|
||||
|
@ -25,6 +25,35 @@ function DBCall({ getWidgetQueryBuilder }: DBCallProps): JSX.Element {
|
||||
);
|
||||
const legend = '{{db_system}}';
|
||||
|
||||
const databaseCallsRPSWidget = useMemo(
|
||||
() =>
|
||||
getWidgetQueryBuilder({
|
||||
queryType: 1,
|
||||
promQL: [],
|
||||
metricsBuilder: databaseCallsRPS({
|
||||
servicename,
|
||||
legend,
|
||||
tagFilterItems,
|
||||
}),
|
||||
clickHouse: [],
|
||||
}),
|
||||
[getWidgetQueryBuilder, servicename, tagFilterItems],
|
||||
);
|
||||
|
||||
const databaseCallsAverageDurationWidget = useMemo(
|
||||
() =>
|
||||
getWidgetQueryBuilder({
|
||||
queryType: 1,
|
||||
promQL: [],
|
||||
metricsBuilder: databaseCallsAvgDuration({
|
||||
servicename,
|
||||
tagFilterItems,
|
||||
}),
|
||||
clickHouse: [],
|
||||
}),
|
||||
[getWidgetQueryBuilder, servicename, tagFilterItems],
|
||||
);
|
||||
|
||||
return (
|
||||
<Row gutter={24}>
|
||||
<Col span={12}>
|
||||
@ -34,16 +63,7 @@ function DBCall({ getWidgetQueryBuilder }: DBCallProps): JSX.Element {
|
||||
<FullView
|
||||
name="database_call_rps"
|
||||
fullViewOptions={false}
|
||||
widget={getWidgetQueryBuilder({
|
||||
queryType: 1,
|
||||
promQL: [],
|
||||
metricsBuilder: databaseCallsRPS({
|
||||
servicename,
|
||||
legend,
|
||||
tagFilterItems,
|
||||
}),
|
||||
clickHouse: [],
|
||||
})}
|
||||
widget={databaseCallsRPSWidget}
|
||||
yAxisUnit="reqps"
|
||||
/>
|
||||
</GraphContainer>
|
||||
@ -57,15 +77,7 @@ function DBCall({ getWidgetQueryBuilder }: DBCallProps): JSX.Element {
|
||||
<FullView
|
||||
name="database_call_avg_duration"
|
||||
fullViewOptions={false}
|
||||
widget={getWidgetQueryBuilder({
|
||||
queryType: 1,
|
||||
promQL: [],
|
||||
metricsBuilder: databaseCallsAvgDuration({
|
||||
servicename,
|
||||
tagFilterItems,
|
||||
}),
|
||||
clickHouse: [],
|
||||
})}
|
||||
widget={databaseCallsAverageDurationWidget}
|
||||
yAxisUnit="ms"
|
||||
/>
|
||||
</GraphContainer>
|
||||
|
@ -29,6 +29,65 @@ function External({ getWidgetQueryBuilder }: ExternalProps): JSX.Element {
|
||||
|
||||
const legend = '{{address}}';
|
||||
|
||||
const externalCallErrorWidget = useMemo(
|
||||
() =>
|
||||
getWidgetQueryBuilder({
|
||||
queryType: 1,
|
||||
promQL: [],
|
||||
metricsBuilder: externalCallErrorPercent({
|
||||
servicename,
|
||||
legend,
|
||||
tagFilterItems,
|
||||
}),
|
||||
clickHouse: [],
|
||||
}),
|
||||
[getWidgetQueryBuilder, servicename, tagFilterItems],
|
||||
);
|
||||
|
||||
const externalCallDurationWidget = useMemo(
|
||||
() =>
|
||||
getWidgetQueryBuilder({
|
||||
queryType: 1,
|
||||
promQL: [],
|
||||
metricsBuilder: externalCallDuration({
|
||||
servicename,
|
||||
tagFilterItems,
|
||||
}),
|
||||
clickHouse: [],
|
||||
}),
|
||||
[getWidgetQueryBuilder, servicename, tagFilterItems],
|
||||
);
|
||||
|
||||
const externalCallRPSWidget = useMemo(
|
||||
() =>
|
||||
getWidgetQueryBuilder({
|
||||
queryType: 1,
|
||||
promQL: [],
|
||||
metricsBuilder: externalCallRpsByAddress({
|
||||
servicename,
|
||||
legend,
|
||||
tagFilterItems,
|
||||
}),
|
||||
clickHouse: [],
|
||||
}),
|
||||
[getWidgetQueryBuilder, servicename, tagFilterItems],
|
||||
);
|
||||
|
||||
const externalCallDurationAddressWidget = useMemo(
|
||||
() =>
|
||||
getWidgetQueryBuilder({
|
||||
queryType: 1,
|
||||
promQL: [],
|
||||
metricsBuilder: externalCallDurationByAddress({
|
||||
servicename,
|
||||
legend,
|
||||
tagFilterItems,
|
||||
}),
|
||||
clickHouse: [],
|
||||
}),
|
||||
[getWidgetQueryBuilder, servicename, tagFilterItems],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Row gutter={24}>
|
||||
@ -39,16 +98,7 @@ function External({ getWidgetQueryBuilder }: ExternalProps): JSX.Element {
|
||||
<FullView
|
||||
name="external_call_error_percentage"
|
||||
fullViewOptions={false}
|
||||
widget={getWidgetQueryBuilder({
|
||||
queryType: 1,
|
||||
promQL: [],
|
||||
metricsBuilder: externalCallErrorPercent({
|
||||
servicename,
|
||||
legend,
|
||||
tagFilterItems,
|
||||
}),
|
||||
clickHouse: [],
|
||||
})}
|
||||
widget={externalCallErrorWidget}
|
||||
yAxisUnit="%"
|
||||
/>
|
||||
</GraphContainer>
|
||||
@ -62,12 +112,7 @@ function External({ getWidgetQueryBuilder }: ExternalProps): JSX.Element {
|
||||
<FullView
|
||||
name="external_call_duration"
|
||||
fullViewOptions={false}
|
||||
widget={getWidgetQueryBuilder({
|
||||
queryType: 1,
|
||||
promQL: [],
|
||||
metricsBuilder: externalCallDuration({ servicename, tagFilterItems }),
|
||||
clickHouse: [],
|
||||
})}
|
||||
widget={externalCallDurationWidget}
|
||||
yAxisUnit="ms"
|
||||
/>
|
||||
</GraphContainer>
|
||||
@ -83,16 +128,7 @@ function External({ getWidgetQueryBuilder }: ExternalProps): JSX.Element {
|
||||
<FullView
|
||||
name="external_call_rps_by_address"
|
||||
fullViewOptions={false}
|
||||
widget={getWidgetQueryBuilder({
|
||||
queryType: 1,
|
||||
promQL: [],
|
||||
metricsBuilder: externalCallRpsByAddress({
|
||||
servicename,
|
||||
legend,
|
||||
tagFilterItems,
|
||||
}),
|
||||
clickHouse: [],
|
||||
})}
|
||||
widget={externalCallRPSWidget}
|
||||
yAxisUnit="reqps"
|
||||
/>
|
||||
</GraphContainer>
|
||||
@ -106,16 +142,7 @@ function External({ getWidgetQueryBuilder }: ExternalProps): JSX.Element {
|
||||
<FullView
|
||||
name="external_call_duration_by_address"
|
||||
fullViewOptions={false}
|
||||
widget={getWidgetQueryBuilder({
|
||||
queryType: 1,
|
||||
promQL: [],
|
||||
metricsBuilder: externalCallDurationByAddress({
|
||||
servicename,
|
||||
legend,
|
||||
tagFilterItems,
|
||||
}),
|
||||
clickHouse: [],
|
||||
})}
|
||||
widget={externalCallDurationAddressWidget}
|
||||
yAxisUnit="ms"
|
||||
/>
|
||||
</GraphContainer>
|
||||
|
@ -2,44 +2,81 @@ import { ActiveElement, Chart, ChartData, ChartEvent } from 'chart.js';
|
||||
import Graph from 'components/Graph';
|
||||
import { METRICS_PAGE_QUERY_PARAM } from 'constants/query';
|
||||
import ROUTES from 'constants/routes';
|
||||
import FullView from 'container/GridGraphLayout/Graph/FullView';
|
||||
import FullView from 'container/GridGraphLayout/Graph/FullView/index.metricsBuilder';
|
||||
import convertToNanoSecondsToSecond from 'lib/convertToNanoSecondsToSecond';
|
||||
import { colors } from 'lib/getRandomColor';
|
||||
import history from 'lib/history';
|
||||
import { convertRawQueriesToTraceSelectedTags } from 'lib/resourceAttributes';
|
||||
import { escapeRegExp } from 'lodash-es';
|
||||
import React, { useMemo, useRef } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import {
|
||||
convertRawQueriesToTraceSelectedTags,
|
||||
resourceAttributesToTagFilterItems,
|
||||
} from 'lib/resourceAttributes';
|
||||
import React, { useCallback, useMemo, useRef } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { UpdateTimeInterval } from 'store/actions';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { PromQLWidgets } from 'types/api/dashboard/getAll';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import MetricReducer from 'types/reducer/metrics';
|
||||
|
||||
import {
|
||||
errorPercentage,
|
||||
operationPerSec,
|
||||
} from '../MetricsPageQueries/OverviewQueries';
|
||||
import { Card, Col, GraphContainer, GraphTitle, Row } from '../styles';
|
||||
import TopOperationsTable from '../TopOperationsTable';
|
||||
import { Button } from './styles';
|
||||
|
||||
function Application({ getWidget }: DashboardProps): JSX.Element {
|
||||
function Application({ getWidgetQueryBuilder }: DashboardProps): JSX.Element {
|
||||
const { servicename } = useParams<{ servicename?: string }>();
|
||||
const selectedTimeStamp = useRef(0);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const {
|
||||
topOperations,
|
||||
serviceOverview,
|
||||
resourceAttributePromQLQuery,
|
||||
resourceAttributeQueries,
|
||||
topLevelOperations,
|
||||
} = useSelector<AppState, MetricReducer>((state) => state.metrics);
|
||||
const operationsRegex = useMemo(() => {
|
||||
return encodeURIComponent(
|
||||
topLevelOperations.map((e) => escapeRegExp(e)).join('|'),
|
||||
);
|
||||
}, [topLevelOperations]);
|
||||
|
||||
const selectedTraceTags: string = JSON.stringify(
|
||||
convertRawQueriesToTraceSelectedTags(resourceAttributeQueries, 'array') || [],
|
||||
);
|
||||
|
||||
const tagFilterItems = useMemo(
|
||||
() => resourceAttributesToTagFilterItems(resourceAttributeQueries) || [],
|
||||
[resourceAttributeQueries],
|
||||
);
|
||||
|
||||
const operationPerSecWidget = useMemo(
|
||||
() =>
|
||||
getWidgetQueryBuilder({
|
||||
queryType: 1,
|
||||
promQL: [],
|
||||
metricsBuilder: operationPerSec({
|
||||
servicename,
|
||||
tagFilterItems,
|
||||
topLevelOperations,
|
||||
}),
|
||||
clickHouse: [],
|
||||
}),
|
||||
[getWidgetQueryBuilder, servicename, topLevelOperations, tagFilterItems],
|
||||
);
|
||||
|
||||
const errorPercentageWidget = useMemo(
|
||||
() =>
|
||||
getWidgetQueryBuilder({
|
||||
queryType: 1,
|
||||
promQL: [],
|
||||
metricsBuilder: errorPercentage({
|
||||
servicename,
|
||||
tagFilterItems,
|
||||
topLevelOperations,
|
||||
}),
|
||||
clickHouse: [],
|
||||
}),
|
||||
[servicename, topLevelOperations, tagFilterItems, getWidgetQueryBuilder],
|
||||
);
|
||||
|
||||
const onTracePopupClick = (timestamp: number): void => {
|
||||
const currentTime = timestamp;
|
||||
const tPlusOne = timestamp + 1 * 60 * 1000;
|
||||
@ -92,6 +129,16 @@ function Application({ getWidget }: DashboardProps): JSX.Element {
|
||||
}
|
||||
};
|
||||
|
||||
const onDragSelect = useCallback(
|
||||
(start: number, end: number) => {
|
||||
const startTimestamp = Math.trunc(start);
|
||||
const endTimestamp = Math.trunc(end);
|
||||
|
||||
dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp]));
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
const onErrorTrackHandler = (timestamp: number): void => {
|
||||
const currentTime = timestamp;
|
||||
const tPlusOne = timestamp + 1 * 60 * 1000;
|
||||
@ -166,13 +213,13 @@ function Application({ getWidget }: DashboardProps): JSX.Element {
|
||||
pointRadius: 1.5,
|
||||
},
|
||||
],
|
||||
labels: serviceOverview.map((e) => {
|
||||
return new Date(
|
||||
parseFloat(convertToNanoSecondsToSecond(e.timestamp)),
|
||||
);
|
||||
}),
|
||||
labels: serviceOverview.map(
|
||||
(e) =>
|
||||
new Date(parseFloat(convertToNanoSecondsToSecond(e.timestamp))),
|
||||
),
|
||||
}}
|
||||
yAxisUnit="ms"
|
||||
onDragSelect={onDragSelect}
|
||||
/>
|
||||
</GraphContainer>
|
||||
</Card>
|
||||
@ -198,13 +245,9 @@ function Application({ getWidget }: DashboardProps): JSX.Element {
|
||||
onClickHandler={(event, element, chart, data): void => {
|
||||
onClickHandler(event, element, chart, data, 'Rate');
|
||||
}}
|
||||
widget={getWidget([
|
||||
{
|
||||
query: `sum(rate(signoz_latency_count{service_name="${servicename}", operation=~\`${operationsRegex}\`${resourceAttributePromQLQuery}}[5m]))`,
|
||||
legend: 'Operations',
|
||||
},
|
||||
])}
|
||||
widget={operationPerSecWidget}
|
||||
yAxisUnit="ops"
|
||||
onDragSelect={onDragSelect}
|
||||
/>
|
||||
</GraphContainer>
|
||||
</Card>
|
||||
@ -232,13 +275,9 @@ function Application({ getWidget }: DashboardProps): JSX.Element {
|
||||
onClickHandler={(ChartEvent, activeElements, chart, data): void => {
|
||||
onClickHandler(ChartEvent, activeElements, chart, data, 'Error');
|
||||
}}
|
||||
widget={getWidget([
|
||||
{
|
||||
query: `max(sum(rate(signoz_calls_total{service_name="${servicename}", operation=~\`${operationsRegex}\`, status_code="STATUS_CODE_ERROR"${resourceAttributePromQLQuery}}[5m]) OR rate(signoz_calls_total{service_name="${servicename}", operation=~\`${operationsRegex}\`, http_status_code=~"5.."${resourceAttributePromQLQuery}}[5m]))*100/sum(rate(signoz_calls_total{service_name="${servicename}", operation=~\`${operationsRegex}\`${resourceAttributePromQLQuery}}[5m]))) < 1000 OR vector(0)`,
|
||||
legend: 'Error Percentage',
|
||||
},
|
||||
])}
|
||||
widget={errorPercentageWidget}
|
||||
yAxisUnit="%"
|
||||
onDragSelect={onDragSelect}
|
||||
/>
|
||||
</GraphContainer>
|
||||
</Card>
|
||||
@ -255,7 +294,7 @@ function Application({ getWidget }: DashboardProps): JSX.Element {
|
||||
}
|
||||
|
||||
interface DashboardProps {
|
||||
getWidget: (query: PromQLWidgets['query']) => PromQLWidgets;
|
||||
getWidgetQueryBuilder: (query: Widgets['query']) => Widgets;
|
||||
}
|
||||
|
||||
export default Application;
|
||||
|
@ -93,9 +93,7 @@ function TopOperationsTable(props: TopOperationsTableProps): JSX.Element {
|
||||
return (
|
||||
<Table
|
||||
showHeader
|
||||
title={(): string => {
|
||||
return 'Key Operations';
|
||||
}}
|
||||
title={(): string => 'Key Operations'}
|
||||
tableLayout="fixed"
|
||||
dataSource={data}
|
||||
columns={columns}
|
||||
|
@ -1,60 +1,17 @@
|
||||
import RouteTab from 'components/RouteTab';
|
||||
import ROUTES from 'constants/routes';
|
||||
import React from 'react';
|
||||
import React, { memo } from 'react';
|
||||
import { generatePath, useParams } from 'react-router-dom';
|
||||
import { useLocation } from 'react-use';
|
||||
import { PromQLWidgets, Widgets } from 'types/api/dashboard/getAll';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { getWidgetQueryBuilder } from './MetricsApplication.factory';
|
||||
import ResourceAttributesFilter from './ResourceAttributesFilter';
|
||||
import DBCall from './Tabs/DBCall';
|
||||
import External from './Tabs/External';
|
||||
import Overview from './Tabs/Overview';
|
||||
|
||||
const getWidget = (query: PromQLWidgets['query']): PromQLWidgets => {
|
||||
return {
|
||||
description: '',
|
||||
id: '',
|
||||
isStacked: false,
|
||||
nullZeroValues: '',
|
||||
opacity: '0',
|
||||
panelTypes: 'TIME_SERIES',
|
||||
query,
|
||||
queryData: {
|
||||
data: { queryData: [] },
|
||||
error: false,
|
||||
errorMessage: '',
|
||||
loading: false,
|
||||
},
|
||||
timePreferance: 'GLOBAL_TIME',
|
||||
title: '',
|
||||
stepSize: 60,
|
||||
};
|
||||
};
|
||||
|
||||
const getWidgetQueryBuilder = (query: Widgets['query']): Widgets => {
|
||||
return {
|
||||
description: '',
|
||||
id: v4(),
|
||||
isStacked: false,
|
||||
nullZeroValues: '',
|
||||
opacity: '0',
|
||||
panelTypes: 'TIME_SERIES',
|
||||
query,
|
||||
queryData: {
|
||||
data: { queryData: [] },
|
||||
error: false,
|
||||
errorMessage: '',
|
||||
loading: false,
|
||||
},
|
||||
timePreferance: 'GLOBAL_TIME',
|
||||
title: '',
|
||||
stepSize: 60,
|
||||
};
|
||||
};
|
||||
|
||||
function OverViewTab(): JSX.Element {
|
||||
return <Overview getWidget={getWidget} />;
|
||||
return <Overview getWidgetQueryBuilder={getWidgetQueryBuilder} />;
|
||||
}
|
||||
|
||||
function DbCallTab(): JSX.Element {
|
||||
@ -128,4 +85,4 @@ function ServiceMetrics(): JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(ServiceMetrics);
|
||||
export default memo(ServiceMetrics);
|
||||
|
@ -2,12 +2,15 @@ import { blue } from '@ant-design/colors';
|
||||
import { SearchOutlined } from '@ant-design/icons';
|
||||
import { Button, Card, Input, Space, Table } from 'antd';
|
||||
import type { ColumnsType, ColumnType } from 'antd/es/table';
|
||||
import type { FilterConfirmProps } from 'antd/es/table/interface';
|
||||
import type {
|
||||
FilterConfirmProps,
|
||||
FilterDropdownProps,
|
||||
} from 'antd/es/table/interface';
|
||||
import localStorageGet from 'api/browser/localstorage/get';
|
||||
import localStorageSet from 'api/browser/localstorage/set';
|
||||
import { SKIP_ONBOARDING } from 'constants/onboarding';
|
||||
import ROUTES from 'constants/routes';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { AppState } from 'store/reducers';
|
||||
@ -35,8 +38,8 @@ function Metrics(): JSX.Element {
|
||||
confirm();
|
||||
};
|
||||
|
||||
const FilterIcon = useCallback(
|
||||
({ filtered }) => (
|
||||
const FilterIcon: ColumnType<DataProps>['filterIcon'] = useCallback(
|
||||
(filtered: boolean) => (
|
||||
<SearchOutlined
|
||||
style={{
|
||||
color: filtered ? blue[6] : undefined,
|
||||
@ -47,7 +50,7 @@ function Metrics(): JSX.Element {
|
||||
);
|
||||
|
||||
const filterDropdown = useCallback(
|
||||
({ setSelectedKeys, selectedKeys, confirm }) => (
|
||||
({ setSelectedKeys, selectedKeys, confirm }: FilterDropdownProps) => (
|
||||
<Card size="small">
|
||||
<Space align="start" direction="vertical">
|
||||
<Input
|
||||
@ -73,6 +76,60 @@ function Metrics(): JSX.Element {
|
||||
[],
|
||||
);
|
||||
|
||||
type DataIndex = keyof ServicesList;
|
||||
|
||||
const getColumnSearchProps = useCallback(
|
||||
(dataIndex: DataIndex): ColumnType<DataProps> => ({
|
||||
filterDropdown,
|
||||
filterIcon: FilterIcon,
|
||||
onFilter: (value: string | number | boolean, record: DataProps): boolean =>
|
||||
record[dataIndex]
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.includes(value.toString().toLowerCase()),
|
||||
render: (text: string): JSX.Element => (
|
||||
<Link to={`${ROUTES.APPLICATION}/${text}${search}`}>
|
||||
<Name>{text}</Name>
|
||||
</Link>
|
||||
),
|
||||
}),
|
||||
[filterDropdown, FilterIcon, search],
|
||||
);
|
||||
|
||||
const columns: ColumnsType<DataProps> = useMemo(
|
||||
() => [
|
||||
{
|
||||
title: 'Application',
|
||||
dataIndex: 'serviceName',
|
||||
key: 'serviceName',
|
||||
...getColumnSearchProps('serviceName'),
|
||||
},
|
||||
{
|
||||
title: 'P99 latency (in ms)',
|
||||
dataIndex: 'p99',
|
||||
key: 'p99',
|
||||
defaultSortOrder: 'descend',
|
||||
sorter: (a: DataProps, b: DataProps): number => a.p99 - b.p99,
|
||||
render: (value: number): string => (value / 1000000).toFixed(2),
|
||||
},
|
||||
{
|
||||
title: 'Error Rate (% of total)',
|
||||
dataIndex: 'errorRate',
|
||||
key: 'errorRate',
|
||||
sorter: (a: DataProps, b: DataProps): number => a.errorRate - b.errorRate,
|
||||
render: (value: number): string => value.toFixed(2),
|
||||
},
|
||||
{
|
||||
title: 'Operations Per Second',
|
||||
dataIndex: 'callRate',
|
||||
key: 'callRate',
|
||||
sorter: (a: DataProps, b: DataProps): number => a.callRate - b.callRate,
|
||||
render: (value: number): string => value.toFixed(2),
|
||||
},
|
||||
],
|
||||
[getColumnSearchProps],
|
||||
);
|
||||
|
||||
if (
|
||||
services.length === 0 &&
|
||||
loading === false &&
|
||||
@ -82,56 +139,6 @@ function Metrics(): JSX.Element {
|
||||
return <SkipBoardModal onContinueClick={onContinueClick} />;
|
||||
}
|
||||
|
||||
type DataIndex = keyof ServicesList;
|
||||
|
||||
const getColumnSearchProps = (
|
||||
dataIndex: DataIndex,
|
||||
): ColumnType<DataProps> => ({
|
||||
filterDropdown,
|
||||
filterIcon: FilterIcon,
|
||||
onFilter: (value: string | number | boolean, record: DataProps): boolean =>
|
||||
record[dataIndex]
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.includes(value.toString().toLowerCase()),
|
||||
render: (text: string): JSX.Element => (
|
||||
<Link to={`${ROUTES.APPLICATION}/${text}${search}`}>
|
||||
<Name>{text}</Name>
|
||||
</Link>
|
||||
),
|
||||
});
|
||||
|
||||
const columns: ColumnsType<DataProps> = [
|
||||
{
|
||||
title: 'Application',
|
||||
dataIndex: 'serviceName',
|
||||
key: 'serviceName',
|
||||
...getColumnSearchProps('serviceName'),
|
||||
},
|
||||
{
|
||||
title: 'P99 latency (in ms)',
|
||||
dataIndex: 'p99',
|
||||
key: 'p99',
|
||||
defaultSortOrder: 'descend',
|
||||
sorter: (a: DataProps, b: DataProps): number => a.p99 - b.p99,
|
||||
render: (value: number): string => (value / 1000000).toFixed(2),
|
||||
},
|
||||
{
|
||||
title: 'Error Rate (% of total)',
|
||||
dataIndex: 'errorRate',
|
||||
key: 'errorRate',
|
||||
sorter: (a: DataProps, b: DataProps): number => a.errorRate - b.errorRate,
|
||||
render: (value: number): string => value.toFixed(2),
|
||||
},
|
||||
{
|
||||
title: 'Operations Per Second',
|
||||
dataIndex: 'callRate',
|
||||
key: 'callRate',
|
||||
sorter: (a: DataProps, b: DataProps): number => a.callRate - b.callRate,
|
||||
render: (value: number): string => value.toFixed(2),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Table
|
||||
|
@ -25,6 +25,7 @@ import {
|
||||
} from 'types/api/dashboard/getAll';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { variablePropsToPayloadVariables } from '../../../utils';
|
||||
import { TVariableViewMode } from '../types';
|
||||
import { LabelContainer, VariableItemRow } from './styles';
|
||||
|
||||
@ -32,6 +33,7 @@ const { Option } = Select;
|
||||
|
||||
interface VariableItemProps {
|
||||
variableData: IDashboardVariable;
|
||||
existingVariables: Record<string, IDashboardVariable>;
|
||||
onCancel: () => void;
|
||||
onSave: (name: string, arg0: IDashboardVariable, arg1: string) => void;
|
||||
validateName: (arg0: string) => boolean;
|
||||
@ -39,6 +41,7 @@ interface VariableItemProps {
|
||||
}
|
||||
function VariableItem({
|
||||
variableData,
|
||||
existingVariables,
|
||||
onCancel,
|
||||
onSave,
|
||||
validateName,
|
||||
@ -134,10 +137,16 @@ function VariableItem({
|
||||
try {
|
||||
const variableQueryResponse = await query({
|
||||
query: variableQueryValue,
|
||||
variables: variablePropsToPayloadVariables(existingVariables),
|
||||
});
|
||||
setPreviewLoading(false);
|
||||
if (variableQueryResponse.error) {
|
||||
setErrorPreview(variableQueryResponse.error);
|
||||
let message = variableQueryResponse.error;
|
||||
if (variableQueryResponse.error.includes('Syntax error:')) {
|
||||
message =
|
||||
'Please make sure query is valid and dependent variables are selected';
|
||||
}
|
||||
setErrorPreview(message);
|
||||
return;
|
||||
}
|
||||
if (variableQueryResponse.payload?.variableValues)
|
||||
|
@ -96,9 +96,7 @@ function VariablesSetting({
|
||||
setDeleteVariableModal(false);
|
||||
};
|
||||
|
||||
const validateVariableName = (name: string): boolean => {
|
||||
return !variables[name];
|
||||
};
|
||||
const validateVariableName = (name: string): boolean => !variables[name];
|
||||
|
||||
const columns = [
|
||||
{
|
||||
@ -142,6 +140,7 @@ function VariablesSetting({
|
||||
{variableViewMode ? (
|
||||
<VariableItem
|
||||
variableData={{ ...variableEditData } as IDashboardVariable}
|
||||
existingVariables={variables}
|
||||
onSave={onVariableSaveHandler}
|
||||
onCancel={onDoneVariableViewMode}
|
||||
validateName={validateVariableName}
|
||||
|
@ -8,7 +8,9 @@ import { map } from 'lodash-es';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
|
||||
import { VariableContainer, VariableName } from './styles';
|
||||
import { variablePropsToPayloadVariables } from '../utils';
|
||||
import { SelectItemStyle, VariableContainer, VariableName } from './styles';
|
||||
import { areArraysEqual } from './util';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
@ -16,18 +18,35 @@ const ALL_SELECT_VALUE = '__ALL__';
|
||||
|
||||
interface VariableItemProps {
|
||||
variableData: IDashboardVariable;
|
||||
onValueUpdate: (name: string | undefined, arg1: string | string[]) => void;
|
||||
existingVariables: Record<string, IDashboardVariable>;
|
||||
onValueUpdate: (
|
||||
name: string | undefined,
|
||||
arg1:
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| (string | number | boolean)[]
|
||||
| null
|
||||
| undefined,
|
||||
) => void;
|
||||
onAllSelectedUpdate: (name: string | undefined, arg1: boolean) => void;
|
||||
lastUpdatedVar: string;
|
||||
}
|
||||
function VariableItem({
|
||||
variableData,
|
||||
existingVariables,
|
||||
onValueUpdate,
|
||||
onAllSelectedUpdate,
|
||||
lastUpdatedVar,
|
||||
}: VariableItemProps): JSX.Element {
|
||||
const [optionsData, setOptionsData] = useState([]);
|
||||
const [optionsData, setOptionsData] = useState<(string | number | boolean)[]>(
|
||||
[],
|
||||
);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
|
||||
const [errorMessage, setErrorMessage] = useState<null | string>(null);
|
||||
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
const getOptions = useCallback(async (): Promise<void> => {
|
||||
if (variableData.type === 'QUERY') {
|
||||
try {
|
||||
@ -36,17 +55,58 @@ function VariableItem({
|
||||
|
||||
const response = await query({
|
||||
query: variableData.queryValue || '',
|
||||
variables: variablePropsToPayloadVariables(existingVariables),
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
if (response.error) {
|
||||
setErrorMessage(response.error);
|
||||
let message = response.error;
|
||||
if (response.error.includes('Syntax error:')) {
|
||||
message =
|
||||
'Please make sure query is valid and dependent variables are selected';
|
||||
}
|
||||
setErrorMessage(message);
|
||||
return;
|
||||
}
|
||||
if (response.payload?.variableValues)
|
||||
setOptionsData(
|
||||
sortValues(response.payload?.variableValues, variableData.sort) as never,
|
||||
if (response.payload?.variableValues) {
|
||||
const newOptionsData = sortValues(
|
||||
response.payload?.variableValues,
|
||||
variableData.sort,
|
||||
);
|
||||
// Since there is a chance of a variable being dependent on other
|
||||
// variables, we need to check if the optionsData has changed
|
||||
// If it has changed, we need to update the dependent variable
|
||||
// So we compare the new optionsData with the old optionsData
|
||||
const oldOptionsData = sortValues(optionsData, variableData.sort) as never;
|
||||
if (!areArraysEqual(newOptionsData, oldOptionsData)) {
|
||||
/* eslint-disable no-useless-escape */
|
||||
const re = new RegExp(`\\{\\{\\s*?\\.${lastUpdatedVar}\\s*?\\}\\}`); // regex for `{{.var}}`
|
||||
// If the variable is dependent on the last updated variable
|
||||
// and contains the last updated variable in its query (of the form `{{.var}}`)
|
||||
// then we need to update the value of the variable
|
||||
const queryValue = variableData.queryValue || '';
|
||||
const dependVarReMatch = queryValue.match(re);
|
||||
if (
|
||||
variableData.type === 'QUERY' &&
|
||||
dependVarReMatch !== null &&
|
||||
dependVarReMatch.length > 0
|
||||
) {
|
||||
let value = variableData.selectedValue;
|
||||
let allSelected = false;
|
||||
// The default value for multi-select is ALL and first value for
|
||||
// single select
|
||||
if (variableData.multiSelect) {
|
||||
value = newOptionsData;
|
||||
allSelected = true;
|
||||
} else {
|
||||
[value] = newOptionsData;
|
||||
}
|
||||
onValueUpdate(variableData.name, value);
|
||||
onAllSelectedUpdate(variableData.name, allSelected);
|
||||
}
|
||||
setOptionsData(newOptionsData);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
@ -59,10 +119,12 @@ function VariableItem({
|
||||
);
|
||||
}
|
||||
}, [
|
||||
variableData.customValue,
|
||||
variableData.queryValue,
|
||||
variableData.sort,
|
||||
variableData.type,
|
||||
variableData,
|
||||
existingVariables,
|
||||
onValueUpdate,
|
||||
onAllSelectedUpdate,
|
||||
optionsData,
|
||||
lastUpdatedVar,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
@ -72,7 +134,8 @@ function VariableItem({
|
||||
const handleChange = (value: string | string[]): void => {
|
||||
if (
|
||||
value === ALL_SELECT_VALUE ||
|
||||
(Array.isArray(value) && value.includes(ALL_SELECT_VALUE))
|
||||
(Array.isArray(value) && value.includes(ALL_SELECT_VALUE)) ||
|
||||
(Array.isArray(value) && value.length === 0)
|
||||
) {
|
||||
onValueUpdate(variableData.name, optionsData);
|
||||
onAllSelectedUpdate(variableData.name, true);
|
||||
@ -81,6 +144,15 @@ function VariableItem({
|
||||
onAllSelectedUpdate(variableData.name, false);
|
||||
}
|
||||
};
|
||||
|
||||
const selectValue = variableData.allSelected
|
||||
? 'ALL'
|
||||
: variableData.selectedValue?.toString() || '';
|
||||
const mode =
|
||||
variableData.multiSelect && !variableData.allSelected
|
||||
? 'multiple'
|
||||
: undefined;
|
||||
const enableSelectAll = variableData.multiSelect && variableData.showALLOption;
|
||||
return (
|
||||
<VariableContainer>
|
||||
<VariableName>${variableData.name}</VariableName>
|
||||
@ -93,35 +165,29 @@ function VariableItem({
|
||||
handleChange(e.target.value || '');
|
||||
}}
|
||||
style={{
|
||||
width: 50 + ((variableData.selectedValue?.length || 0) * 7 || 50),
|
||||
width:
|
||||
50 + ((variableData.selectedValue?.toString()?.length || 0) * 7 || 50),
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Select
|
||||
value={variableData.allSelected ? 'ALL' : variableData.selectedValue}
|
||||
onChange={handleChange}
|
||||
bordered={false}
|
||||
placeholder="Select value"
|
||||
mode={
|
||||
(variableData.multiSelect && !variableData.allSelected
|
||||
? 'multiple'
|
||||
: null) as never
|
||||
}
|
||||
dropdownMatchSelectWidth={false}
|
||||
style={{
|
||||
minWidth: 120,
|
||||
fontSize: '0.8rem',
|
||||
}}
|
||||
loading={isLoading}
|
||||
showArrow
|
||||
>
|
||||
{variableData.multiSelect && variableData.showALLOption && (
|
||||
<Option value={ALL_SELECT_VALUE}>ALL</Option>
|
||||
)}
|
||||
{map(optionsData, (option) => {
|
||||
return <Option value={option}>{(option as string).toString()}</Option>;
|
||||
})}
|
||||
</Select>
|
||||
!errorMessage && (
|
||||
<Select
|
||||
value={selectValue}
|
||||
onChange={handleChange}
|
||||
bordered={false}
|
||||
placeholder="Select value"
|
||||
mode={mode}
|
||||
dropdownMatchSelectWidth={false}
|
||||
style={SelectItemStyle}
|
||||
loading={isLoading}
|
||||
showArrow
|
||||
>
|
||||
{enableSelectAll && <Option value={ALL_SELECT_VALUE}>ALL</Option>}
|
||||
{map(optionsData, (option) => (
|
||||
<Option value={option}>{option.toString()}</Option>
|
||||
))}
|
||||
</Select>
|
||||
)
|
||||
)}
|
||||
{errorMessage && (
|
||||
<span style={{ margin: '0 0.5rem' }}>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Row } from 'antd';
|
||||
import { map, sortBy } from 'lodash-es';
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { connect, useSelector } from 'react-redux';
|
||||
import { bindActionCreators, Dispatch } from 'redux';
|
||||
import { ThunkDispatch } from 'redux-thunk';
|
||||
@ -23,13 +23,30 @@ function DashboardVariableSelection({
|
||||
data: { variables = {} },
|
||||
} = selectedDashboard;
|
||||
|
||||
const [update, setUpdate] = useState<boolean>(false);
|
||||
const [lastUpdatedVar, setLastUpdatedVar] = useState<string>('');
|
||||
|
||||
const onVarChanged = (name: string): void => {
|
||||
setLastUpdatedVar(name);
|
||||
setUpdate(!update);
|
||||
};
|
||||
|
||||
const onValueUpdate = (
|
||||
name: string,
|
||||
value: IDashboardVariable['selectedValue'],
|
||||
value:
|
||||
| string
|
||||
| string[]
|
||||
| number
|
||||
| number[]
|
||||
| boolean
|
||||
| boolean[]
|
||||
| null
|
||||
| undefined,
|
||||
): void => {
|
||||
const updatedVariablesData = { ...variables };
|
||||
updatedVariablesData[name].selectedValue = value;
|
||||
updateDashboardVariables(updatedVariablesData);
|
||||
onVarChanged(name);
|
||||
};
|
||||
const onAllSelectedUpdate = (
|
||||
name: string,
|
||||
@ -38,6 +55,7 @@ function DashboardVariableSelection({
|
||||
const updatedVariablesData = { ...variables };
|
||||
updatedVariablesData[name].allSelected = value;
|
||||
updateDashboardVariables(updatedVariablesData);
|
||||
onVarChanged(name);
|
||||
};
|
||||
|
||||
return (
|
||||
@ -45,9 +63,15 @@ function DashboardVariableSelection({
|
||||
{map(sortBy(Object.keys(variables)), (variableName) => (
|
||||
<VariableItem
|
||||
key={`${variableName}${variables[variableName].modificationUUID}`}
|
||||
variableData={{ name: variableName, ...variables[variableName] }}
|
||||
existingVariables={variables}
|
||||
variableData={{
|
||||
name: variableName,
|
||||
...variables[variableName],
|
||||
change: update,
|
||||
}}
|
||||
onValueUpdate={onValueUpdate as never}
|
||||
onAllSelectedUpdate={onAllSelectedUpdate as never}
|
||||
lastUpdatedVar={lastUpdatedVar}
|
||||
/>
|
||||
))}
|
||||
</Row>
|
||||
|
@ -17,3 +17,8 @@ export const VariableName = styled(Typography)`
|
||||
font-style: italic;
|
||||
color: ${grey[0]};
|
||||
`;
|
||||
|
||||
export const SelectItemStyle = {
|
||||
minWidth: 120,
|
||||
fontSize: '0.8rem',
|
||||
};
|
||||
|
@ -0,0 +1,16 @@
|
||||
export function areArraysEqual(
|
||||
a: (string | number | boolean)[],
|
||||
b: (string | number | boolean)[],
|
||||
): boolean {
|
||||
if (a.length !== b.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0; i < a.length; i += 1) {
|
||||
if (a[i] !== b[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import { DashboardData } from 'types/api/dashboard/getAll';
|
||||
|
||||
import { downloadObjectAsJson } from './util';
|
||||
import { cleardQueryData, downloadObjectAsJson } from './util';
|
||||
|
||||
function ShareModal({
|
||||
isJSONModalVisible,
|
||||
@ -51,6 +51,7 @@ function ShareModal({
|
||||
}
|
||||
}, [state.error, state.value, t]);
|
||||
|
||||
const selectedDataCleaned = cleardQueryData(selectedData);
|
||||
const GetFooterComponent = useMemo(() => {
|
||||
if (!isViewJSON) {
|
||||
return (
|
||||
@ -66,7 +67,7 @@ function ShareModal({
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={(): void => {
|
||||
downloadObjectAsJson(selectedData, selectedData.title);
|
||||
downloadObjectAsJson(selectedDataCleaned, selectedData.title);
|
||||
}}
|
||||
>
|
||||
{t('download_json')}
|
||||
@ -79,7 +80,7 @@ function ShareModal({
|
||||
{t('copy_to_clipboard')}
|
||||
</Button>
|
||||
);
|
||||
}, [isViewJSON, jsonValue, selectedData, setCopy, t]);
|
||||
}, [isViewJSON, jsonValue, selectedData, selectedDataCleaned, setCopy, t]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { DashboardData } from 'types/api/dashboard/getAll';
|
||||
|
||||
export function downloadObjectAsJson(
|
||||
exportObj: unknown,
|
||||
exportName: string,
|
||||
@ -12,3 +14,18 @@ export function downloadObjectAsJson(
|
||||
downloadAnchorNode.click();
|
||||
downloadAnchorNode.remove();
|
||||
}
|
||||
|
||||
export function cleardQueryData(param: DashboardData): DashboardData {
|
||||
return {
|
||||
...param,
|
||||
widgets: param.widgets?.map((widget) => ({
|
||||
...widget,
|
||||
queryData: {
|
||||
...widget.queryData,
|
||||
data: {
|
||||
queryData: [],
|
||||
},
|
||||
},
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
14
frontend/src/container/NewDashboard/utils.ts
Normal file
14
frontend/src/container/NewDashboard/utils.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
import { PayloadVariables } from 'types/api/dashboard/variables/query';
|
||||
|
||||
export function variablePropsToPayloadVariables(
|
||||
variables: Record<string, IDashboardVariable>,
|
||||
): PayloadVariables {
|
||||
const payloadVariables: PayloadVariables = {};
|
||||
|
||||
Object.entries(variables).forEach(([key, value]) => {
|
||||
payloadVariables[key] = value?.selectedValue;
|
||||
});
|
||||
|
||||
return payloadVariables;
|
||||
}
|
@ -2,12 +2,10 @@ import { EAggregateOperator } from 'types/common/dashboard';
|
||||
|
||||
export const AggregateFunctions = Object.keys(EAggregateOperator)
|
||||
.filter((key) => Number.isNaN(parseInt(key, 10)))
|
||||
.map((key) => {
|
||||
return {
|
||||
label: key,
|
||||
value: EAggregateOperator[key as keyof typeof EAggregateOperator],
|
||||
};
|
||||
});
|
||||
.map((key) => ({
|
||||
label: key,
|
||||
value: EAggregateOperator[key as keyof typeof EAggregateOperator],
|
||||
}));
|
||||
|
||||
export const TagKeyOperator = [
|
||||
{ label: 'In', value: 'IN' },
|
||||
|
@ -154,17 +154,15 @@ function MetricTagKeyFilter({
|
||||
{queries.length > 0 &&
|
||||
map(
|
||||
queries,
|
||||
(query): JSX.Element => {
|
||||
return (
|
||||
<QueryChip key={query.id} queryData={query} onClose={handleClose} />
|
||||
);
|
||||
},
|
||||
(query): JSX.Element => (
|
||||
<QueryChip key={query.id} queryData={query} onClose={handleClose} />
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
{map(staging, (item) => {
|
||||
return <QueryChipItem key={uuid()}>{item}</QueryChipItem>;
|
||||
})}
|
||||
{map(staging, (item) => (
|
||||
<QueryChipItem key={uuid()}>{item}</QueryChipItem>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', width: '100%' }}>
|
||||
|
@ -4,8 +4,7 @@ import { EQueryTypeToQueryKeyMapping } from '../types';
|
||||
|
||||
export const getQueryKey = (
|
||||
queryCategory: EQueryType,
|
||||
): EQueryTypeToQueryKeyMapping => {
|
||||
return EQueryTypeToQueryKeyMapping[
|
||||
): EQueryTypeToQueryKeyMapping =>
|
||||
EQueryTypeToQueryKeyMapping[
|
||||
EQueryType[queryCategory] as keyof typeof EQueryTypeToQueryKeyMapping
|
||||
];
|
||||
};
|
||||
|
@ -383,7 +383,5 @@ export const dataTypeCategories = [
|
||||
];
|
||||
|
||||
export const flattenedCategories = flattenDeep(
|
||||
dataTypeCategories.map((category) => {
|
||||
return category.formats;
|
||||
}),
|
||||
dataTypeCategories.map((category) => category.formats),
|
||||
);
|
||||
|
@ -30,9 +30,8 @@ export const TextContainer = styled.div<TextContainerProps>`
|
||||
margin-bottom: 1rem;
|
||||
|
||||
> button {
|
||||
margin-left: ${({ noButtonMargin }): string => {
|
||||
return noButtonMargin ? '0' : '0.5rem';
|
||||
}}
|
||||
margin-left: ${({ noButtonMargin }): string =>
|
||||
noButtonMargin ? '0' : '0.5rem'}
|
||||
`;
|
||||
|
||||
export const NullButtonContainer = styled.div`
|
||||
|
@ -57,9 +57,7 @@ function NewWidget({
|
||||
|
||||
const { search } = useLocation();
|
||||
|
||||
const query = useMemo(() => {
|
||||
return new URLSearchParams(search);
|
||||
}, [search]);
|
||||
const query = useMemo(() => new URLSearchParams(search), [search]);
|
||||
|
||||
const { dashboardId } = useParams<DashboardWidgetPageParams>();
|
||||
|
||||
|
@ -231,18 +231,16 @@ function AuthDomains(): JSX.Element {
|
||||
title: 'Action',
|
||||
dataIndex: 'action',
|
||||
key: 'action',
|
||||
render: (_, record): JSX.Element => {
|
||||
return (
|
||||
<Button
|
||||
disabled={!SSOFlag}
|
||||
onClick={onDeleteHandler(record)}
|
||||
danger
|
||||
type="link"
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
render: (_, record): JSX.Element => (
|
||||
<Button
|
||||
disabled={!SSOFlag}
|
||||
onClick={onDeleteHandler(record)}
|
||||
danger
|
||||
type="link"
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -26,9 +26,8 @@ function EditMembersDetails({
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [state, copyToClipboard] = useCopyToClipboard();
|
||||
|
||||
const getPasswordLink = (token: string): string => {
|
||||
return `${window.location.origin}${ROUTES.PASSWORD_RESET}?token=${token}`;
|
||||
};
|
||||
const getPasswordLink = (token: string): string =>
|
||||
`${window.location.origin}${ROUTES.PASSWORD_RESET}?token=${token}`;
|
||||
|
||||
const onChangeHandler = useCallback(
|
||||
(setFunc: React.Dispatch<React.SetStateAction<string>>, value: string) => {
|
||||
@ -51,9 +50,12 @@ function EditMembersDetails({
|
||||
}
|
||||
}, [state.error, state.value, t]);
|
||||
|
||||
const onPasswordChangeHandler = useCallback((event) => {
|
||||
setPasswordLink(event.target.value);
|
||||
}, []);
|
||||
const onPasswordChangeHandler: React.ChangeEventHandler<HTMLInputElement> = useCallback(
|
||||
(event) => {
|
||||
setPasswordLink(event.target.value);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const onGeneratePasswordHandler = async (): Promise<void> => {
|
||||
try {
|
||||
|
@ -11,8 +11,8 @@ const { Option } = Select;
|
||||
function InviteTeamMembers({ allMembers, setAllMembers }: Props): JSX.Element {
|
||||
const { t } = useTranslation('organizationsettings');
|
||||
|
||||
useEffect(() => {
|
||||
return (): void => {
|
||||
useEffect(
|
||||
() => (): void => {
|
||||
setAllMembers([
|
||||
{
|
||||
email: '',
|
||||
@ -20,8 +20,9 @@ function InviteTeamMembers({ allMembers, setAllMembers }: Props): JSX.Element {
|
||||
role: 'VIEWER',
|
||||
},
|
||||
]);
|
||||
};
|
||||
}, [setAllMembers]);
|
||||
},
|
||||
[setAllMembers],
|
||||
);
|
||||
|
||||
const onAddHandler = (): void => {
|
||||
setAllMembers((state) => [
|
||||
@ -36,16 +37,14 @@ function InviteTeamMembers({ allMembers, setAllMembers }: Props): JSX.Element {
|
||||
|
||||
const onChangeHandler = useCallback(
|
||||
(value: string, index: number, type: string): void => {
|
||||
setAllMembers((prev) => {
|
||||
return [
|
||||
...prev.slice(0, index),
|
||||
{
|
||||
...prev[index],
|
||||
[type]: value,
|
||||
},
|
||||
...prev.slice(index, prev.length - 1),
|
||||
];
|
||||
});
|
||||
setAllMembers((prev) => [
|
||||
...prev.slice(0, index),
|
||||
{
|
||||
...prev[index],
|
||||
[type]: value,
|
||||
},
|
||||
...prev.slice(index, prev.length - 1),
|
||||
]);
|
||||
},
|
||||
[setAllMembers],
|
||||
);
|
||||
|
@ -63,15 +63,17 @@ function PendingInvitesContainer(): JSX.Element {
|
||||
|
||||
const { hash } = useLocation();
|
||||
|
||||
const getParsedInviteData = useCallback((payload: PayloadProps = []) => {
|
||||
return payload?.map((data) => ({
|
||||
key: data.createdAt,
|
||||
name: data.name,
|
||||
email: data.email,
|
||||
accessLevel: data.role,
|
||||
inviteLink: `${window.location.origin}${ROUTES.SIGN_UP}?token=${data.token}`,
|
||||
}));
|
||||
}, []);
|
||||
const getParsedInviteData = useCallback(
|
||||
(payload: PayloadProps = []) =>
|
||||
payload?.map((data) => ({
|
||||
key: data.createdAt,
|
||||
name: data.name,
|
||||
email: data.email,
|
||||
accessLevel: data.role,
|
||||
inviteLink: `${window.location.origin}${ROUTES.SIGN_UP}?token=${data.token}`,
|
||||
})),
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (hash === INVITE_MEMBERS_HASH) {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user