Compare commits

..

3 Commits

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

Signed-off-by: Prashant Shahi <prashant@signoz.io>
Co-authored-by: Palash Gupta <palashgdev@gmail.com>
(cherry picked from commit 13f9922c53)
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2023-01-26 13:51:48 +05:30
Palash Gupta
c48f72781e fix: yarnrc is added in the root of the frontend (#2114)
Co-authored-by: Prashant Shahi <prashant@signoz.io>
(cherry picked from commit ba8f804b26)
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2023-01-26 13:51:40 +05:30
Prashant Shahi
51b583480b chore: 📌 pin versions: SigNoz 0.14.0, SigNoz OtelCollector 0.66.2
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2023-01-12 17:56:29 +05:30
189 changed files with 3242 additions and 4971 deletions

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

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

View File

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

View File

@@ -137,7 +137,7 @@ services:
condition: on-failure
query-service:
image: signoz/query-service:0.13.1
image: signoz/query-service:0.14.0
command: ["-config=/root/config/prometheus.yml"]
# ports:
# - "6060:6060" # pprof port
@@ -166,7 +166,7 @@ services:
<<: *clickhouse-depend
frontend:
image: signoz/frontend:0.13.1
image: signoz/frontend:0.14.0
deploy:
restart_policy:
condition: on-failure
@@ -179,7 +179,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector:
image: signoz/signoz-otel-collector:0.66.3
image: signoz/signoz-otel-collector:0.66.2
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.3
image: signoz/signoz-otel-collector:0.66.2
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
volumes:
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml

View File

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

View File

@@ -114,7 +114,7 @@ services:
# volumes:
# - ./clickhouse-config.xml:/etc/clickhouse-server/config.xml
# - ./clickhouse-users.xml:/etc/clickhouse-server/users.xml
# - ./custom-function.xml:/etc/clickhouse-server/custom-function.xml
# - ./custom-function.xml:/etc/clickhouse-server/custom-function.xml
# - ./clickhouse-cluster.xml:/etc/clickhouse-server/config.d/cluster.xml
# # - ./clickhouse-storage.xml:/etc/clickhouse-server/config.d/storage.xml
# - ./data/clickhouse-2/:/var/lib/clickhouse/
@@ -132,7 +132,7 @@ services:
# volumes:
# - ./clickhouse-config.xml:/etc/clickhouse-server/config.xml
# - ./clickhouse-users.xml:/etc/clickhouse-server/users.xml
# - ./custom-function.xml:/etc/clickhouse-server/custom-function.xml
# - ./custom-function.xml:/etc/clickhouse-server/custom-function.xml
# - ./clickhouse-cluster.xml:/etc/clickhouse-server/config.d/cluster.xml
# # - ./clickhouse-storage.xml:/etc/clickhouse-server/config.d/storage.xml
# - ./data/clickhouse-3/:/var/lib/clickhouse/
@@ -153,7 +153,7 @@ services:
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
query-service:
image: signoz/query-service:${DOCKER_TAG:-0.13.1}
image: signoz/query-service:${DOCKER_TAG:-0.14.0}
container_name: query-service
command: ["-config=/root/config/prometheus.yml"]
# ports:
@@ -181,7 +181,7 @@ services:
<<: *clickhouse-depend
frontend:
image: signoz/frontend:${DOCKER_TAG:-0.13.1}
image: signoz/frontend:${DOCKER_TAG:-0.14.0}
container_name: frontend
restart: on-failure
depends_on:
@@ -193,7 +193,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector:
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.66.1}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.66.2}
command: ["--config=/etc/otel-collector-config.yaml"]
user: root # required for reading docker container logs
volumes:
@@ -218,7 +218,7 @@ services:
<<: *clickhouse-depend
otel-collector-metrics:
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.66.1}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.66.2}
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
volumes:
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml

View File

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

View File

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

View File

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

View File

@@ -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.9.1",
"chart.js": "^3.4.0",
"chartjs-adapter-date-fns": "^2.0.0",
"chartjs-plugin-annotation": "^1.4.0",
"color": "^4.2.1",
@@ -70,8 +70,8 @@
"less-loader": "^10.2.0",
"lodash-es": "^4.17.21",
"mini-css-extract-plugin": "2.4.5",
"react": "18.2.0",
"react-dom": "18.2.0",
"react": "17.0.0",
"react-dom": "17.0.0",
"react-force-graph": "^1.41.0",
"react-graph-vis": "^1.0.5",
"react-grid-layout": "^1.3.4",
@@ -80,7 +80,6 @@
"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",
@@ -133,8 +132,8 @@
"@types/lodash-es": "^4.17.4",
"@types/mini-css-extract-plugin": "^2.5.1",
"@types/node": "^16.10.3",
"@types/react": "18.0.26",
"@types/react-dom": "18.0.10",
"@types/react": "^17.0.0",
"@types/react-dom": "^16.9.9",
"@types/react-grid-layout": "^1.1.2",
"@types/react-redux": "^7.1.11",
"@types/react-router-dom": "^5.1.6",
@@ -187,7 +186,7 @@
]
},
"resolutions": {
"@types/react": "18.0.26",
"@types/react-dom": "18.0.10"
"@types/react": "17.0.0",
"@types/react-dom": "17.0.0"
}
}

View File

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

View File

@@ -12,9 +12,7 @@ const getSpans = async (
const updatedSelectedTags = props.selectedTags.map((e) => ({
Key: e.Key[0],
Operator: e.Operator,
StringValues: e.StringValues,
NumberValues: e.NumberValues,
BoolValues: e.BoolValues,
Values: e.Values,
}));
const exclude: string[] = [];

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -27,21 +27,8 @@ 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';
@@ -77,8 +64,6 @@ function Graph({
forceReRender,
staticLine,
containerHeight,
onDragSelect,
dragSelectColor,
}: GraphProps): JSX.Element {
const chartRef = useRef<HTMLCanvasElement>(null);
const isDarkMode = useIsDarkMode();
@@ -106,7 +91,7 @@ function Graph({
}
if (chartRef.current !== null) {
const options: CustomChartOptions = {
const options: ChartOptions = {
animation: {
duration: animate ? 200 : 0,
},
@@ -163,15 +148,6 @@ function Graph({
},
},
},
[dragSelectPluginId]: createDragSelectPluginOptions(
!!onDragSelect,
onDragSelect,
dragSelectColor,
),
[intersectionCursorPluginId]: createIntersectionCursorPluginOptions(
!!onDragSelect,
currentTheme === 'dark' ? 'white' : 'black',
),
},
layout: {
padding: 0,
@@ -201,7 +177,6 @@ function Graph({
},
},
type: 'time',
ticks: { color: getAxisLabelColor(currentTheme) },
},
y: {
display: true,
@@ -210,7 +185,6 @@ function Graph({
color: getGridColor(),
},
ticks: {
color: getAxisLabelColor(currentTheme),
// Include a dollar sign in the ticks
callback(value) {
return getYAxisFormattedValue(value.toString(), yAxisUnit);
@@ -237,13 +211,7 @@ function Graph({
const chartHasData = hasData(data);
const chartPlugins = [];
if (chartHasData) {
chartPlugins.push(createIntersectionCursorPlugin());
chartPlugins.push(createDragSelectPlugin());
} else {
chartPlugins.push(emptyGraph);
}
if (!chartHasData) chartPlugins.push(emptyGraph);
chartPlugins.push(legend(name, data.datasets.length > 3));
lineChartRef.current = new Chart(chartRef.current, {
@@ -266,9 +234,6 @@ function Graph({
yAxisUnit,
onClickHandler,
staticLine,
onDragSelect,
dragSelectColor,
currentTheme,
]);
useEffect(() => {
@@ -283,13 +248,6 @@ function Graph({
);
}
type CustomChartOptions = ChartOptions & {
plugins: {
[dragSelectPluginId]: DragSelectPluginOptions | false;
[intersectionCursorPluginId]: IntersectionCursorPluginOptions | false;
};
};
interface GraphProps {
animate?: boolean;
type: ChartType;
@@ -302,8 +260,6 @@ interface GraphProps {
forceReRender?: boolean | null | number;
staticLine?: StaticLineProps | undefined;
containerHeight?: string | number;
onDragSelect?: (start: number, end: number) => void;
dragSelectColor?: string;
}
export interface StaticLineProps {
@@ -330,8 +286,6 @@ Graph.defaultProps = {
yAxisUnit: undefined,
forceReRender: undefined,
staticLine: undefined,
containerHeight: '90%',
onDragSelect: undefined,
dragSelectColor: undefined,
containerHeight: '85%',
};
export default Graph;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,8 +10,7 @@ import {
Tooltip,
Typography,
} from 'antd';
import { ColumnType, TablePaginationConfig } from 'antd/es/table';
import { FilterValue, SorterResult } from 'antd/es/table/interface';
import { ColumnType } from 'antd/es/table';
import { ColumnsType } from 'antd/lib/table';
import { FilterConfirmProps } from 'antd/lib/table/interface';
import getAll from 'api/errors/getAll';
@@ -31,7 +30,6 @@ import { ErrorResponse, SuccessResponse } from 'types/api';
import { Exception, PayloadProps } from 'types/api/errors/getAll';
import { GlobalReducer } from 'types/reducer/globalTime';
import { FilterDropdownExtendsProps } from './types';
import {
extractFilterValues,
getDefaultFilterValue,
@@ -178,45 +176,41 @@ function AllErrors(): JSX.Element {
);
const filterDropdownWrapper = useCallback(
({
setSelectedKeys,
selectedKeys,
confirm,
placeholder,
filterKey,
}: FilterDropdownExtendsProps) => (
<Card size="small">
<Space align="start" direction="vertical">
<Input
placeholder={placeholder}
value={selectedKeys[0]}
onChange={(e): void =>
setSelectedKeys(e.target.value ? [e.target.value] : [])
}
allowClear
defaultValue={getDefaultFilterValue(
filterKey,
getUpdatedServiceName,
getUpdatedExceptionType,
)}
onPressEnter={handleSearch(confirm, String(selectedKeys[0]), filterKey)}
/>
<Button
type="primary"
onClick={handleSearch(confirm, String(selectedKeys[0]), filterKey)}
icon={<SearchOutlined />}
size="small"
>
Search
</Button>
</Space>
</Card>
),
({ setSelectedKeys, selectedKeys, confirm, placeholder, filterKey }) => {
return (
<Card size="small">
<Space align="start" direction="vertical">
<Input
placeholder={placeholder}
value={selectedKeys[0]}
onChange={(e): void =>
setSelectedKeys(e.target.value ? [e.target.value] : [])
}
allowClear
defaultValue={getDefaultFilterValue(
filterKey,
getUpdatedServiceName,
getUpdatedExceptionType,
)}
onPressEnter={handleSearch(confirm, selectedKeys[0], filterKey)}
/>
<Button
type="primary"
onClick={handleSearch(confirm, selectedKeys[0], filterKey)}
icon={<SearchOutlined />}
size="small"
>
Search
</Button>
</Space>
</Card>
);
},
[getUpdatedExceptionType, getUpdatedServiceName, handleSearch],
);
const onExceptionTypeFilter: ColumnType<Exception>['onFilter'] = useCallback(
(value: unknown, record: Exception): boolean => {
const onExceptionTypeFilter = useCallback(
(value, record: Exception): boolean => {
if (record.exceptionType && typeof value === 'string') {
return record.exceptionType.toLowerCase().includes(value.toLowerCase());
}
@@ -226,7 +220,7 @@ function AllErrors(): JSX.Element {
);
const onApplicationTypeFilter = useCallback(
(value: unknown, record: Exception): boolean => {
(value, record: Exception): boolean => {
if (record.serviceName && typeof value === 'string') {
return record.serviceName.toLowerCase().includes(value.toLowerCase());
}
@@ -349,11 +343,7 @@ function AllErrors(): JSX.Element {
];
const onChangeHandler: TableProps<Exception>['onChange'] = useCallback(
(
paginations: TablePaginationConfig,
filters: Record<string, FilterValue | null>,
sorter: SorterResult<Exception>[] | SorterResult<Exception>,
) => {
(paginations, filters, sorter) => {
if (!Array.isArray(sorter)) {
const { pageSize = 0, current = 0 } = paginations;
const { columnKey = '', order } = sorter;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -211,70 +211,78 @@ function QuerySection({
setFormulaQueries({ ...formulas });
}, [formulaQueries, setFormulaQueries]);
const renderPromqlUI = (): JSX.Element => (
<PromqlSection promQueries={promQueries} setPromQueries={setPromQueries} />
);
const renderPromqlUI = (): JSX.Element => {
return (
<PromqlSection promQueries={promQueries} setPromQueries={setPromQueries} />
);
};
const renderChQueryUI = (): JSX.Element => (
<ChQuerySection chQueries={chQueries} setChQueries={setChQueries} />
);
const renderChQueryUI = (): JSX.Element => {
return <ChQuerySection chQueries={chQueries} setChQueries={setChQueries} />;
};
const renderFormulaButton = (): JSX.Element => (
<QueryButton onClick={addFormula} icon={<PlusOutlined />}>
{t('button_formula')}
</QueryButton>
);
const renderFormulaButton = (): JSX.Element => {
return (
<QueryButton onClick={addFormula} icon={<PlusOutlined />}>
{t('button_formula')}
</QueryButton>
);
};
const renderQueryButton = (): JSX.Element => (
<QueryButton onClick={addMetricQuery} icon={<PlusOutlined />}>
{t('button_query')}
</QueryButton>
);
const renderQueryButton = (): JSX.Element => {
return (
<QueryButton onClick={addMetricQuery} icon={<PlusOutlined />}>
{t('button_query')}
</QueryButton>
);
};
const renderMetricUI = (): JSX.Element => (
<div>
{metricQueries &&
Object.keys(metricQueries).map((key: string) => {
// todo(amol): need to handle this in fetch
const current = metricQueries[key];
current.name = key;
return (
<MetricsBuilder
key={key}
queryIndex={key}
queryData={toIMetricsBuilderQuery(current)}
selectedGraph="TIME_SERIES"
handleQueryChange={handleMetricQueryChange}
/>
);
})}
{queryCategory !== EQueryType.PROM && renderQueryButton()}
<div style={{ marginTop: '1rem' }}>
{formulaQueries &&
Object.keys(formulaQueries).map((key: string) => {
const renderMetricUI = (): JSX.Element => {
return (
<div>
{metricQueries &&
Object.keys(metricQueries).map((key: string) => {
// todo(amol): need to handle this in fetch
const current = formulaQueries[key];
const current = metricQueries[key];
current.name = key;
return (
<MetricsBuilderFormula
<MetricsBuilder
key={key}
formulaIndex={key}
formulaData={current}
handleFormulaChange={handleFormulaChange}
queryIndex={key}
queryData={toIMetricsBuilderQuery(current)}
selectedGraph="TIME_SERIES"
handleQueryChange={handleMetricQueryChange}
/>
);
})}
{queryCategory === EQueryType.QUERY_BUILDER &&
(!formulaQueries || Object.keys(formulaQueries).length === 0) &&
metricQueries &&
Object.keys(metricQueries).length > 0 &&
renderFormulaButton()}
{queryCategory !== EQueryType.PROM && renderQueryButton()}
<div style={{ marginTop: '1rem' }}>
{formulaQueries &&
Object.keys(formulaQueries).map((key: string) => {
// todo(amol): need to handle this in fetch
const current = formulaQueries[key];
current.name = key;
return (
<MetricsBuilderFormula
key={key}
formulaIndex={key}
formulaData={current}
handleFormulaChange={handleFormulaChange}
/>
);
})}
{queryCategory === EQueryType.QUERY_BUILDER &&
(!formulaQueries || Object.keys(formulaQueries).length === 0) &&
metricQueries &&
Object.keys(metricQueries).length > 0 &&
renderFormulaButton()}
</div>
</div>
</div>
);
);
};
const handleRunQuery = (): void => {
runQuery();

View File

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

View File

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

View File

@@ -436,35 +436,41 @@ function FormAlertRules({
<BasicInfo alertDef={alertDef} setAlertDef={setAlertDef} />
);
const renderQBChartPreview = (): JSX.Element => (
<ChartPreview
headline={<PlotTag queryType={queryCategory} />}
name=""
threshold={alertDef.condition?.target}
query={debouncedStagedQuery}
selectedInterval={toChartInterval(alertDef.evalWindow)}
/>
);
const renderQBChartPreview = (): JSX.Element => {
return (
<ChartPreview
headline={<PlotTag queryType={queryCategory} />}
name=""
threshold={alertDef.condition?.target}
query={debouncedStagedQuery}
selectedInterval={toChartInterval(alertDef.evalWindow)}
/>
);
};
const renderPromChartPreview = (): JSX.Element => (
<ChartPreview
headline={<PlotTag queryType={queryCategory} />}
name="Chart Preview"
threshold={alertDef.condition?.target}
query={debouncedStagedQuery}
/>
);
const renderPromChartPreview = (): JSX.Element => {
return (
<ChartPreview
headline={<PlotTag queryType={queryCategory} />}
name="Chart Preview"
threshold={alertDef.condition?.target}
query={debouncedStagedQuery}
/>
);
};
const renderChQueryChartPreview = (): JSX.Element => (
<ChartPreview
headline={<PlotTag queryType={queryCategory} />}
name="Chart Preview"
threshold={alertDef.condition?.target}
query={manualStagedQuery}
userQueryKey={runQueryId}
selectedInterval={toChartInterval(alertDef.evalWindow)}
/>
);
const renderChQueryChartPreview = (): JSX.Element => {
return (
<ChartPreview
headline={<PlotTag queryType={queryCategory} />}
name="Chart Preview"
threshold={alertDef.condition?.target}
query={manualStagedQuery}
userQueryKey={runQueryId}
selectedInterval={toChartInterval(alertDef.evalWindow)}
/>
);
};
return (
<>
{Element}

View File

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

View File

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

View File

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

View File

@@ -590,22 +590,20 @@ function GeneralSettings({
});
return (
<>
<Col xs={24} md={22} xl={20} xxl={18} style={{ margin: 'auto' }}>
{Element}
<Col xs={24} md={22} xl={20} xxl={18} style={{ margin: 'auto' }}>
<ErrorTextContainer>
<TextToolTip
{...{
text: `More details on how to set retention period`,
url: 'https://signoz.io/docs/userguide/retention-period/',
}}
/>
{errorText && <ErrorText>{errorText}</ErrorText>}
</ErrorTextContainer>
<ErrorTextContainer>
<TextToolTip
{...{
text: `More details on how to set retention period`,
url: 'https://signoz.io/docs/userguide/retention-period/',
}}
/>
{errorText && <ErrorText>{errorText}</ErrorText>}
</ErrorTextContainer>
<Row justify="start">{renderConfig}</Row>
</Col>
</>
<Row justify="start">{renderConfig}</Row>
</Col>
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,13 +0,0 @@
import { themeColors } from 'constants/theme';
const positionCss: React.CSSProperties['position'] = 'fixed';
export const spinnerStyles = { position: positionCss, right: '0.5rem' };
export const tooltipStyles = {
fontSize: '1rem',
top: '0.313rem',
position: positionCss,
right: '0.313rem',
color: themeColors.errorColor,
};
export const errorTooltipPosition = 'top';

View File

@@ -2,23 +2,17 @@ import {
DeleteOutlined,
DownOutlined,
EditFilled,
ExclamationCircleOutlined,
FullscreenOutlined,
} from '@ant-design/icons';
import { Dropdown, Menu, Tooltip, Typography } from 'antd';
import Spinner from 'components/Spinner';
import { Dropdown, Menu, Typography } from 'antd';
import useComponentPermission from 'hooks/useComponentPermission';
import history from 'lib/history';
import React, { 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, spinnerStyles, tooltipStyles } from './config';
import {
ArrowContainer,
HeaderContainer,
@@ -33,10 +27,6 @@ interface IWidgetHeaderProps {
onView: VoidFunction;
onDelete: VoidFunction;
parentHover: boolean;
queryResponse: UseQueryResult<
SuccessResponse<MetricRangePayloadProps> | ErrorResponse
>;
errorMessage: string | undefined;
}
function WidgetHeader({
title,
@@ -44,8 +34,6 @@ function WidgetHeader({
onView,
onDelete,
parentHover,
queryResponse,
errorMessage,
}: IWidgetHeaderProps): JSX.Element {
const [localHover, setLocalHover] = useState(false);
@@ -118,30 +106,20 @@ function WidgetHeader({
overlayStyle={{ minWidth: 100 }}
placement="bottom"
>
<>
<HeaderContainer
onMouseOver={(): void => setLocalHover(true)}
onMouseOut={(): void => setLocalHover(false)}
hover={localHover}
>
<HeaderContentContainer onClick={(e): void => e.preventDefault()}>
<Typography.Text style={{ maxWidth: '80%' }} ellipsis>
{title}
</Typography.Text>
<ArrowContainer hover={parentHover}>
<DownOutlined />
</ArrowContainer>
</HeaderContentContainer>
</HeaderContainer>
{queryResponse.isFetching && !queryResponse.isError && (
<Spinner height="5vh" style={spinnerStyles} />
)}
{queryResponse.isError && (
<Tooltip title={errorMessage} placement={errorTooltipPosition}>
<ExclamationCircleOutlined style={tooltipStyles} />
</Tooltip>
)}
</>
<HeaderContainer
onMouseOver={(): void => setLocalHover(true)}
onMouseOut={(): void => setLocalHover(false)}
hover={localHover}
>
<HeaderContentContainer onClick={(e): void => e.preventDefault()}>
<Typography.Text style={{ maxWidth: '80%' }} ellipsis>
{title}
</Typography.Text>
<ArrowContainer hover={parentHover}>
<DownOutlined />
</ArrowContainer>
</HeaderContentContainer>
</HeaderContainer>
</Dropdown>
);
}

View File

@@ -8,8 +8,6 @@ import { useTranslation } from 'react-i18next';
import { connect, useDispatch, useSelector } from 'react-redux';
import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import { AppDispatch } from 'store';
import { UpdateTimeInterval } from 'store/actions';
import {
ToggleAddWidget,
ToggleAddWidgetProps,
@@ -65,22 +63,12 @@ function GridGraph(props: Props): JSX.Element {
const [selectedDashboard] = dashboards;
const { data } = selectedDashboard;
const { widgets } = data;
const dispatch: AppDispatch = useDispatch<Dispatch<AppActions>>();
const dispatch = useDispatch<Dispatch<AppActions>>();
const [layouts, setLayout] = useState<LayoutProps[]>(
getPreLayouts(widgets, selectedDashboard.data.layout || []),
);
const onDragSelect = useCallback(
(start: number, end: number) => {
const startTimestamp = Math.trunc(start);
const endTimestamp = Math.trunc(end);
dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp]));
},
[dispatch],
);
useEffect(() => {
(async (): Promise<void> => {
if (!isAddWidget) {
@@ -194,14 +182,13 @@ function GridGraph(props: Props): JSX.Element {
yAxisUnit={currentWidget?.yAxisUnit}
layout={layout}
setLayout={setLayout}
onDragSelect={onDragSelect}
/>
),
};
}),
);
},
[widgets, onDragSelect],
[widgets],
);
const onEmptyWidgetHandler = useCallback(async () => {

View File

@@ -110,11 +110,13 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
return (
<>
{withOutSeverityKeys.map((e) => (
<StyledTag key={e} color="magenta">
{e}: {value[e]}
</StyledTag>
))}
{withOutSeverityKeys.map((e) => {
return (
<StyledTag key={e} color="magenta">
{e}: {value[e]}
</StyledTag>
);
})}
</>
);
},
@@ -126,20 +128,22 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
title: 'Action',
dataIndex: 'id',
key: 'action',
render: (id: GettableAlert['id'], record): JSX.Element => (
<>
<ToggleAlertState disabled={record.disabled} setData={setData} id={id} />
render: (id: GettableAlert['id'], record): JSX.Element => {
return (
<>
<ToggleAlertState disabled={record.disabled} setData={setData} id={id} />
<ColumnButton
onClick={(): void => onEditHandler(id.toString())}
type="link"
>
Edit
</ColumnButton>
<ColumnButton
onClick={(): void => onEditHandler(id.toString())}
type="link"
>
Edit
</ColumnButton>
<DeleteAlert notifications={notifications} setData={setData} id={id} />
</>
),
<DeleteAlert notifications={notifications} setData={setData} id={id} />
</>
);
},
});
}

View File

@@ -40,8 +40,8 @@ function ToggleAlertState({
});
if (response.statusCode === 200) {
setData((state) =>
state.map((alert) => {
setData((state) => {
return state.map((alert) => {
if (alert.id === id) {
return {
...alert,
@@ -50,8 +50,8 @@ function ToggleAlertState({
};
}
return alert;
}),
);
});
});
setAPIStatus((state) => ({
...state,

View File

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

View File

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

View File

@@ -30,11 +30,13 @@ function TableView({ logData }: TableViewProps): JSX.Element | null {
flattenLogData !== null &&
Object.keys(flattenLogData)
.filter((field) => fieldSearchFilter(field, fieldSearchInput))
.map((key) => ({
key,
field: key,
value: JSON.stringify(flattenLogData[key]),
}));
.map((key) => {
return {
key,
field: key,
value: JSON.stringify(flattenLogData[key]),
};
});
if (!dataSource) {
return null;

View File

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

View File

@@ -157,16 +157,18 @@ function Login({
}
};
const renderSAMLAction = (): JSX.Element => (
<Button
type="primary"
loading={isLoading}
disabled={isLoading}
href={precheckResult.ssoUrl}
>
{t('login_with_sso')}
</Button>
);
const renderSAMLAction = (): JSX.Element => {
return (
<Button
type="primary"
loading={isLoading}
disabled={isLoading}
href={precheckResult.ssoUrl}
>
{t('login_with_sso')}
</Button>
);
};
const renderOnSsoError = (): JSX.Element | null => {
if (!ssoerror) {

View File

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

View File

@@ -25,7 +25,9 @@ export const Field = styled.div<{ isDarkMode: boolean }>`
justify-content: space-between;
align-items: center;
&:hover {
background: ${({ isDarkMode }): string => (isDarkMode ? grey[7] : '#ddd')};
background: ${({ isDarkMode }): string => {
return isDarkMode ? grey[7] : '#ddd';
}};
}
`;

View File

@@ -68,7 +68,7 @@ function SearchFilter({
);
const handleSearch = useCallback(
(customQuery: string) => {
(customQuery) => {
if (liveTail === 'PLAYING') {
dispatch({
type: TOGGLE_LIVE_TAIL,

View File

@@ -1,7 +1,6 @@
import history from 'lib/history';
import { parseQuery, reverseParser } from 'lib/logql';
import { ILogQLParsedQueryItem } from 'lib/logql/types';
import isEqual from 'lodash-es/isEqual';
import { isEqual } from 'lodash-es';
import { useCallback, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
@@ -14,8 +13,8 @@ import { ILogsReducer } from 'types/reducer/logs';
export function useSearchParser(): {
queryString: string;
parsedQuery: unknown;
updateParsedQuery: (arg0: ILogQLParsedQueryItem[]) => void;
updateQueryString: (arg0: string) => void;
updateParsedQuery: (arg0: unknown) => void;
updateQueryString: (arg0: unknown) => void;
} {
const dispatch = useDispatch();
const {
@@ -23,7 +22,7 @@ export function useSearchParser(): {
} = useSelector<AppState, ILogsReducer>((store) => store.logs);
const updateQueryString = useCallback(
(updatedQueryString: string) => {
(updatedQueryString) => {
history.replace({
pathname: history.location.pathname,
search: updatedQueryString ? `?q=${updatedQueryString}` : '',
@@ -49,7 +48,7 @@ export function useSearchParser(): {
}, [queryString, updateQueryString]);
const updateParsedQuery = useCallback(
(updatedParsedPayload: ILogQLParsedQueryItem[]) => {
(updatedParsedPayload) => {
dispatch({
type: SET_SEARCH_QUERY_PARSED_PAYLOAD,
payload: updatedParsedPayload,

View File

@@ -1,9 +1,10 @@
/* eslint-disable no-nested-ternary */
import { Typography } from 'antd';
import LogItem from 'components/Logs/LogItem';
import Spinner from 'components/Spinner';
import React, { memo, useCallback, useMemo } from 'react';
import map from 'lodash-es/map';
import React, { memo } from 'react';
import { useSelector } from 'react-redux';
import { Virtuoso } from 'react-virtuoso';
import { AppState } from 'store/reducers';
import { ILogsReducer } from 'types/reducer/logs';
@@ -14,24 +15,6 @@ 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" />;
}
@@ -41,15 +24,13 @@ function LogsTable(): JSX.Element {
<Heading>
<Typography.Text>Event</Typography.Text>
</Heading>
{isLiveTail && <Typography>Getting live logs...</Typography>}
{isNoLogs && <Typography>No log lines found</Typography>}
<Virtuoso
useWindowScroll
totalCount={logs.length}
itemContent={getItemContent}
/>
{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>
)}
</Container>
);
}

View File

@@ -1,20 +0,0 @@
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: '',
});

View File

@@ -19,21 +19,13 @@ 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,
groupBy,
legend,
itemsA,
groupBy,
servicename,
tagFilterItems,
});
};
@@ -50,24 +42,14 @@ 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({
metricNameA,
metricNameB,
additionalItemsA,
additionalItemsB,
servicename,
legend,
disabled,
tagFilterItems,
metricNameA,
metricNameB,
expression,
legendFormula,
});

View File

@@ -6,6 +6,7 @@ import {
import {
getQueryBuilderQueries,
getQueryBuilderQuerieswithAdditionalItems,
getQueryBuilderQuerieswithFormula,
} from './MetricsPageQueriesFactory';
@@ -21,41 +22,25 @@ export const externalCallErrorPercent = ({
} => {
const metricNameA = 'signoz_external_call_latency_count';
const metricNameB = 'signoz_external_call_latency_count';
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 additionalItems = {
id: '',
key: 'status_code',
op: 'IN',
value: ['STATUS_CODE_ERROR'],
};
const legendFormula = 'External Call Error Percentage';
const expression = 'A*100/B';
const disabled = true;
return getQueryBuilderQuerieswithFormula({
return getQueryBuilderQuerieswithAdditionalItems({
metricNameA,
metricNameB,
additionalItemsA,
additionalItemsB,
additionalItems,
servicename,
legend,
groupBy,
disabled,
tagFilterItems,
expression,
legendFormula,
});
@@ -74,24 +59,14 @@ 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({
metricNameA,
metricNameB,
additionalItemsA,
additionalItemsB,
servicename,
legend,
disabled,
tagFilterItems,
metricNameA,
metricNameB,
expression,
legendFormula,
});
@@ -106,20 +81,12 @@ 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,
});
};
@@ -136,27 +103,16 @@ 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,
});
};

View File

@@ -4,11 +4,14 @@ import {
IQueryBuilderTagFilterItems,
} from 'types/api/dashboard/getAll';
import { ExternalCallProps } from './ExternalQueries';
export const getQueryBuilderQueries = ({
metricName,
groupBy,
servicename,
legend,
itemsA,
tagFilterItems,
}: BuilderQueriesProps): {
formulas: IMetricsBuilderFormula[];
queryBuilder: IMetricsBuilderQuery[];
@@ -24,7 +27,15 @@ export const getQueryBuilderQueries = ({
name: 'A',
reduceTo: 1,
tagFilters: {
items: itemsA,
items: [
{
id: '',
key: 'service_name',
op: 'IN',
value: [`${servicename}`],
},
...tagFilterItems,
],
op: 'AND',
},
},
@@ -32,18 +43,90 @@ 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: [
{
@@ -63,7 +146,17 @@ export const getQueryBuilderQuerieswithFormula = ({
name: 'A',
reduceTo: 1,
tagFilters: {
items: additionalItemsA,
items: [
{
id: '',
key: 'service_name',
op: 'IN',
value: [`${servicename}`],
},
additionalItems,
...tagFilterItems,
],
op: 'AND',
},
},
@@ -76,21 +169,28 @@ export const getQueryBuilderQuerieswithFormula = ({
name: 'B',
reduceTo: 1,
tagFilters: {
items: additionalItemsB,
items: [
{
id: '',
key: 'service_name',
op: 'IN',
value: [`${servicename}`],
},
...tagFilterItems,
],
op: 'AND',
},
},
],
});
interface BuilderQueriesProps {
interface BuilderQueriesProps extends ExternalCallProps {
metricName: string;
groupBy?: string[];
legend: string;
itemsA: IQueryBuilderTagFilterItems[];
}
interface BuilderQuerieswithFormulaProps {
interface BuilderQuerieswithFormulaProps extends ExternalCallProps {
metricNameA: string;
metricNameB: string;
legend: string;
@@ -98,6 +198,9 @@ interface BuilderQuerieswithFormulaProps {
groupBy?: string[];
expression: string;
legendFormula: string;
additionalItemsA: IQueryBuilderTagFilterItems[];
additionalItemsB: IQueryBuilderTagFilterItems[];
}
interface BuilderQuerieswithAdditionalItems
extends BuilderQuerieswithFormulaProps {
additionalItems: IQueryBuilderTagFilterItems;
}

View File

@@ -1,112 +0,0 @@
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[];
}

View File

@@ -164,20 +164,24 @@ function ResourceAttributesFilter(): JSX.Element | null {
>
{map(
queries,
(query): JSX.Element => (
<QueryChip
disabled={disabled}
key={query.id}
queryData={query}
onClose={handleClose}
/>
),
(query): JSX.Element => {
return (
<QueryChip
disabled={disabled}
key={query.id}
queryData={query}
onClose={handleClose}
/>
);
},
)}
{map(staging, (item, idx) => (
<QueryChipItem key={uuid()}>
{idx === 0 ? convertMetricKeyToTrace(item) : item}
</QueryChipItem>
))}
{map(staging, (item, idx) => {
return (
<QueryChipItem key={uuid()}>
{idx === 0 ? convertMetricKeyToTrace(item) : item}
</QueryChipItem>
);
})}
</div>
{!disabled && (
<Select

View File

@@ -25,35 +25,6 @@ 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}>
@@ -63,7 +34,16 @@ function DBCall({ getWidgetQueryBuilder }: DBCallProps): JSX.Element {
<FullView
name="database_call_rps"
fullViewOptions={false}
widget={databaseCallsRPSWidget}
widget={getWidgetQueryBuilder({
queryType: 1,
promQL: [],
metricsBuilder: databaseCallsRPS({
servicename,
legend,
tagFilterItems,
}),
clickHouse: [],
})}
yAxisUnit="reqps"
/>
</GraphContainer>
@@ -77,7 +57,15 @@ function DBCall({ getWidgetQueryBuilder }: DBCallProps): JSX.Element {
<FullView
name="database_call_avg_duration"
fullViewOptions={false}
widget={databaseCallsAverageDurationWidget}
widget={getWidgetQueryBuilder({
queryType: 1,
promQL: [],
metricsBuilder: databaseCallsAvgDuration({
servicename,
tagFilterItems,
}),
clickHouse: [],
})}
yAxisUnit="ms"
/>
</GraphContainer>

View File

@@ -29,65 +29,6 @@ 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}>
@@ -98,7 +39,16 @@ function External({ getWidgetQueryBuilder }: ExternalProps): JSX.Element {
<FullView
name="external_call_error_percentage"
fullViewOptions={false}
widget={externalCallErrorWidget}
widget={getWidgetQueryBuilder({
queryType: 1,
promQL: [],
metricsBuilder: externalCallErrorPercent({
servicename,
legend,
tagFilterItems,
}),
clickHouse: [],
})}
yAxisUnit="%"
/>
</GraphContainer>
@@ -112,7 +62,12 @@ function External({ getWidgetQueryBuilder }: ExternalProps): JSX.Element {
<FullView
name="external_call_duration"
fullViewOptions={false}
widget={externalCallDurationWidget}
widget={getWidgetQueryBuilder({
queryType: 1,
promQL: [],
metricsBuilder: externalCallDuration({ servicename, tagFilterItems }),
clickHouse: [],
})}
yAxisUnit="ms"
/>
</GraphContainer>
@@ -128,7 +83,16 @@ function External({ getWidgetQueryBuilder }: ExternalProps): JSX.Element {
<FullView
name="external_call_rps_by_address"
fullViewOptions={false}
widget={externalCallRPSWidget}
widget={getWidgetQueryBuilder({
queryType: 1,
promQL: [],
metricsBuilder: externalCallRpsByAddress({
servicename,
legend,
tagFilterItems,
}),
clickHouse: [],
})}
yAxisUnit="reqps"
/>
</GraphContainer>
@@ -142,7 +106,16 @@ function External({ getWidgetQueryBuilder }: ExternalProps): JSX.Element {
<FullView
name="external_call_duration_by_address"
fullViewOptions={false}
widget={externalCallDurationAddressWidget}
widget={getWidgetQueryBuilder({
queryType: 1,
promQL: [],
metricsBuilder: externalCallDurationByAddress({
servicename,
legend,
tagFilterItems,
}),
clickHouse: [],
})}
yAxisUnit="ms"
/>
</GraphContainer>

View File

@@ -2,81 +2,44 @@ 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/index.metricsBuilder';
import FullView from 'container/GridGraphLayout/Graph/FullView';
import convertToNanoSecondsToSecond from 'lib/convertToNanoSecondsToSecond';
import { colors } from 'lib/getRandomColor';
import history from 'lib/history';
import {
convertRawQueriesToTraceSelectedTags,
resourceAttributesToTagFilterItems,
} from 'lib/resourceAttributes';
import React, { useCallback, useMemo, useRef } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { convertRawQueriesToTraceSelectedTags } from 'lib/resourceAttributes';
import { escapeRegExp } from 'lodash-es';
import React, { useMemo, useRef } from 'react';
import { useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';
import { UpdateTimeInterval } from 'store/actions';
import { AppState } from 'store/reducers';
import { Widgets } from 'types/api/dashboard/getAll';
import { PromQLWidgets } 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({ getWidgetQueryBuilder }: DashboardProps): JSX.Element {
function Application({ getWidget }: 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;
@@ -129,16 +92,6 @@ function Application({ getWidgetQueryBuilder }: 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;
@@ -213,13 +166,13 @@ function Application({ getWidgetQueryBuilder }: DashboardProps): JSX.Element {
pointRadius: 1.5,
},
],
labels: serviceOverview.map(
(e) =>
new Date(parseFloat(convertToNanoSecondsToSecond(e.timestamp))),
),
labels: serviceOverview.map((e) => {
return new Date(
parseFloat(convertToNanoSecondsToSecond(e.timestamp)),
);
}),
}}
yAxisUnit="ms"
onDragSelect={onDragSelect}
/>
</GraphContainer>
</Card>
@@ -245,9 +198,13 @@ function Application({ getWidgetQueryBuilder }: DashboardProps): JSX.Element {
onClickHandler={(event, element, chart, data): void => {
onClickHandler(event, element, chart, data, 'Rate');
}}
widget={operationPerSecWidget}
widget={getWidget([
{
query: `sum(rate(signoz_latency_count{service_name="${servicename}", operation=~\`${operationsRegex}\`${resourceAttributePromQLQuery}}[5m]))`,
legend: 'Operations',
},
])}
yAxisUnit="ops"
onDragSelect={onDragSelect}
/>
</GraphContainer>
</Card>
@@ -275,9 +232,13 @@ function Application({ getWidgetQueryBuilder }: DashboardProps): JSX.Element {
onClickHandler={(ChartEvent, activeElements, chart, data): void => {
onClickHandler(ChartEvent, activeElements, chart, data, 'Error');
}}
widget={errorPercentageWidget}
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',
},
])}
yAxisUnit="%"
onDragSelect={onDragSelect}
/>
</GraphContainer>
</Card>
@@ -294,7 +255,7 @@ function Application({ getWidgetQueryBuilder }: DashboardProps): JSX.Element {
}
interface DashboardProps {
getWidgetQueryBuilder: (query: Widgets['query']) => Widgets;
getWidget: (query: PromQLWidgets['query']) => PromQLWidgets;
}
export default Application;

View File

@@ -93,7 +93,9 @@ function TopOperationsTable(props: TopOperationsTableProps): JSX.Element {
return (
<Table
showHeader
title={(): string => 'Key Operations'}
title={(): string => {
return 'Key Operations';
}}
tableLayout="fixed"
dataSource={data}
columns={columns}

View File

@@ -1,17 +1,60 @@
import RouteTab from 'components/RouteTab';
import ROUTES from 'constants/routes';
import React, { memo } from 'react';
import React 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 getWidgetQueryBuilder={getWidgetQueryBuilder} />;
return <Overview getWidget={getWidget} />;
}
function DbCallTab(): JSX.Element {
@@ -85,4 +128,4 @@ function ServiceMetrics(): JSX.Element {
);
}
export default memo(ServiceMetrics);
export default React.memo(ServiceMetrics);

View File

@@ -2,15 +2,12 @@ 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,
FilterDropdownProps,
} from 'antd/es/table/interface';
import type { FilterConfirmProps } 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, useMemo, useState } from 'react';
import React, { useCallback, useState } from 'react';
import { useSelector } from 'react-redux';
import { Link, useLocation } from 'react-router-dom';
import { AppState } from 'store/reducers';
@@ -38,8 +35,8 @@ function Metrics(): JSX.Element {
confirm();
};
const FilterIcon: ColumnType<DataProps>['filterIcon'] = useCallback(
(filtered: boolean) => (
const FilterIcon = useCallback(
({ filtered }) => (
<SearchOutlined
style={{
color: filtered ? blue[6] : undefined,
@@ -50,7 +47,7 @@ function Metrics(): JSX.Element {
);
const filterDropdown = useCallback(
({ setSelectedKeys, selectedKeys, confirm }: FilterDropdownProps) => (
({ setSelectedKeys, selectedKeys, confirm }) => (
<Card size="small">
<Space align="start" direction="vertical">
<Input
@@ -76,60 +73,6 @@ 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 &&
@@ -139,6 +82,56 @@ 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

View File

@@ -25,7 +25,6 @@ import {
} from 'types/api/dashboard/getAll';
import { v4 } from 'uuid';
import { variablePropsToPayloadVariables } from '../../../utils';
import { TVariableViewMode } from '../types';
import { LabelContainer, VariableItemRow } from './styles';
@@ -33,7 +32,6 @@ 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;
@@ -41,7 +39,6 @@ interface VariableItemProps {
}
function VariableItem({
variableData,
existingVariables,
onCancel,
onSave,
validateName,
@@ -137,16 +134,10 @@ function VariableItem({
try {
const variableQueryResponse = await query({
query: variableQueryValue,
variables: variablePropsToPayloadVariables(existingVariables),
});
setPreviewLoading(false);
if (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);
setErrorPreview(variableQueryResponse.error);
return;
}
if (variableQueryResponse.payload?.variableValues)

View File

@@ -96,7 +96,9 @@ function VariablesSetting({
setDeleteVariableModal(false);
};
const validateVariableName = (name: string): boolean => !variables[name];
const validateVariableName = (name: string): boolean => {
return !variables[name];
};
const columns = [
{
@@ -140,7 +142,6 @@ function VariablesSetting({
{variableViewMode ? (
<VariableItem
variableData={{ ...variableEditData } as IDashboardVariable}
existingVariables={variables}
onSave={onVariableSaveHandler}
onCancel={onDoneVariableViewMode}
validateName={validateVariableName}

View File

@@ -8,9 +8,7 @@ import { map } from 'lodash-es';
import React, { useCallback, useEffect, useState } from 'react';
import { IDashboardVariable } from 'types/api/dashboard/getAll';
import { variablePropsToPayloadVariables } from '../utils';
import { SelectItemStyle, VariableContainer, VariableName } from './styles';
import { areArraysEqual } from './util';
import { VariableContainer, VariableName } from './styles';
const { Option } = Select;
@@ -18,35 +16,18 @@ const ALL_SELECT_VALUE = '__ALL__';
interface VariableItemProps {
variableData: IDashboardVariable;
existingVariables: Record<string, IDashboardVariable>;
onValueUpdate: (
name: string | undefined,
arg1:
| string
| number
| boolean
| (string | number | boolean)[]
| null
| undefined,
) => void;
onValueUpdate: (name: string | undefined, arg1: string | string[]) => void;
onAllSelectedUpdate: (name: string | undefined, arg1: boolean) => void;
lastUpdatedVar: string;
}
function VariableItem({
variableData,
existingVariables,
onValueUpdate,
onAllSelectedUpdate,
lastUpdatedVar,
}: VariableItemProps): JSX.Element {
const [optionsData, setOptionsData] = useState<(string | number | boolean)[]>(
[],
);
const [optionsData, setOptionsData] = useState([]);
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 {
@@ -55,58 +36,17 @@ function VariableItem({
const response = await query({
query: variableData.queryValue || '',
variables: variablePropsToPayloadVariables(existingVariables),
});
setIsLoading(false);
if (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);
setErrorMessage(response.error);
return;
}
if (response.payload?.variableValues) {
const newOptionsData = sortValues(
response.payload?.variableValues,
variableData.sort,
if (response.payload?.variableValues)
setOptionsData(
sortValues(response.payload?.variableValues, variableData.sort) as never,
);
// 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);
}
@@ -119,12 +59,10 @@ function VariableItem({
);
}
}, [
variableData,
existingVariables,
onValueUpdate,
onAllSelectedUpdate,
optionsData,
lastUpdatedVar,
variableData.customValue,
variableData.queryValue,
variableData.sort,
variableData.type,
]);
useEffect(() => {
@@ -134,8 +72,7 @@ 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.length === 0)
(Array.isArray(value) && value.includes(ALL_SELECT_VALUE))
) {
onValueUpdate(variableData.name, optionsData);
onAllSelectedUpdate(variableData.name, true);
@@ -144,15 +81,6 @@ 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>
@@ -165,29 +93,35 @@ function VariableItem({
handleChange(e.target.value || '');
}}
style={{
width:
50 + ((variableData.selectedValue?.toString()?.length || 0) * 7 || 50),
width: 50 + ((variableData.selectedValue?.length || 0) * 7 || 50),
}}
/>
) : (
!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>
)
<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 && (
<span style={{ margin: '0 0.5rem' }}>

View File

@@ -1,6 +1,6 @@
import { Row } from 'antd';
import { map, sortBy } from 'lodash-es';
import React, { useState } from 'react';
import React from 'react';
import { connect, useSelector } from 'react-redux';
import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
@@ -23,30 +23,13 @@ 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:
| string
| string[]
| number
| number[]
| boolean
| boolean[]
| null
| undefined,
value: IDashboardVariable['selectedValue'],
): void => {
const updatedVariablesData = { ...variables };
updatedVariablesData[name].selectedValue = value;
updateDashboardVariables(updatedVariablesData);
onVarChanged(name);
};
const onAllSelectedUpdate = (
name: string,
@@ -55,7 +38,6 @@ function DashboardVariableSelection({
const updatedVariablesData = { ...variables };
updatedVariablesData[name].allSelected = value;
updateDashboardVariables(updatedVariablesData);
onVarChanged(name);
};
return (
@@ -63,15 +45,9 @@ function DashboardVariableSelection({
{map(sortBy(Object.keys(variables)), (variableName) => (
<VariableItem
key={`${variableName}${variables[variableName].modificationUUID}`}
existingVariables={variables}
variableData={{
name: variableName,
...variables[variableName],
change: update,
}}
variableData={{ name: variableName, ...variables[variableName] }}
onValueUpdate={onValueUpdate as never}
onAllSelectedUpdate={onAllSelectedUpdate as never}
lastUpdatedVar={lastUpdatedVar}
/>
))}
</Row>

View File

@@ -17,8 +17,3 @@ export const VariableName = styled(Typography)`
font-style: italic;
color: ${grey[0]};
`;
export const SelectItemStyle = {
minWidth: 120,
fontSize: '0.8rem',
};

View File

@@ -1,16 +0,0 @@
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;
}

View File

@@ -1,14 +0,0 @@
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;
}

View File

@@ -2,10 +2,12 @@ import { EAggregateOperator } from 'types/common/dashboard';
export const AggregateFunctions = Object.keys(EAggregateOperator)
.filter((key) => Number.isNaN(parseInt(key, 10)))
.map((key) => ({
label: key,
value: EAggregateOperator[key as keyof typeof EAggregateOperator],
}));
.map((key) => {
return {
label: key,
value: EAggregateOperator[key as keyof typeof EAggregateOperator],
};
});
export const TagKeyOperator = [
{ label: 'In', value: 'IN' },

View File

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

View File

@@ -4,7 +4,8 @@ import { EQueryTypeToQueryKeyMapping } from '../types';
export const getQueryKey = (
queryCategory: EQueryType,
): EQueryTypeToQueryKeyMapping =>
EQueryTypeToQueryKeyMapping[
): EQueryTypeToQueryKeyMapping => {
return EQueryTypeToQueryKeyMapping[
EQueryType[queryCategory] as keyof typeof EQueryTypeToQueryKeyMapping
];
};

View File

@@ -383,5 +383,7 @@ export const dataTypeCategories = [
];
export const flattenedCategories = flattenDeep(
dataTypeCategories.map((category) => category.formats),
dataTypeCategories.map((category) => {
return category.formats;
}),
);

View File

@@ -30,8 +30,9 @@ export const TextContainer = styled.div<TextContainerProps>`
margin-bottom: 1rem;
> button {
margin-left: ${({ noButtonMargin }): string =>
noButtonMargin ? '0' : '0.5rem'}
margin-left: ${({ noButtonMargin }): string => {
return noButtonMargin ? '0' : '0.5rem';
}}
`;
export const NullButtonContainer = styled.div`

View File

@@ -57,7 +57,9 @@ function NewWidget({
const { search } = useLocation();
const query = useMemo(() => new URLSearchParams(search), [search]);
const query = useMemo(() => {
return new URLSearchParams(search);
}, [search]);
const { dashboardId } = useParams<DashboardWidgetPageParams>();

View File

@@ -231,16 +231,18 @@ function AuthDomains(): JSX.Element {
title: 'Action',
dataIndex: 'action',
key: 'action',
render: (_, record): JSX.Element => (
<Button
disabled={!SSOFlag}
onClick={onDeleteHandler(record)}
danger
type="link"
>
Delete
</Button>
),
render: (_, record): JSX.Element => {
return (
<Button
disabled={!SSOFlag}
onClick={onDeleteHandler(record)}
danger
type="link"
>
Delete
</Button>
);
},
},
];

View File

@@ -26,8 +26,9 @@ function EditMembersDetails({
const [isLoading, setIsLoading] = useState<boolean>(false);
const [state, copyToClipboard] = useCopyToClipboard();
const getPasswordLink = (token: string): string =>
`${window.location.origin}${ROUTES.PASSWORD_RESET}?token=${token}`;
const getPasswordLink = (token: string): string => {
return `${window.location.origin}${ROUTES.PASSWORD_RESET}?token=${token}`;
};
const onChangeHandler = useCallback(
(setFunc: React.Dispatch<React.SetStateAction<string>>, value: string) => {
@@ -50,12 +51,9 @@ function EditMembersDetails({
}
}, [state.error, state.value, t]);
const onPasswordChangeHandler: React.ChangeEventHandler<HTMLInputElement> = useCallback(
(event) => {
setPasswordLink(event.target.value);
},
[],
);
const onPasswordChangeHandler = useCallback((event) => {
setPasswordLink(event.target.value);
}, []);
const onGeneratePasswordHandler = async (): Promise<void> => {
try {

View File

@@ -11,8 +11,8 @@ const { Option } = Select;
function InviteTeamMembers({ allMembers, setAllMembers }: Props): JSX.Element {
const { t } = useTranslation('organizationsettings');
useEffect(
() => (): void => {
useEffect(() => {
return (): void => {
setAllMembers([
{
email: '',
@@ -20,9 +20,8 @@ function InviteTeamMembers({ allMembers, setAllMembers }: Props): JSX.Element {
role: 'VIEWER',
},
]);
},
[setAllMembers],
);
};
}, [setAllMembers]);
const onAddHandler = (): void => {
setAllMembers((state) => [
@@ -37,14 +36,16 @@ function InviteTeamMembers({ allMembers, setAllMembers }: Props): JSX.Element {
const onChangeHandler = useCallback(
(value: string, index: number, type: string): void => {
setAllMembers((prev) => [
...prev.slice(0, index),
{
...prev[index],
[type]: value,
},
...prev.slice(index, prev.length - 1),
]);
setAllMembers((prev) => {
return [
...prev.slice(0, index),
{
...prev[index],
[type]: value,
},
...prev.slice(index, prev.length - 1),
];
});
},
[setAllMembers],
);

View File

@@ -63,17 +63,15 @@ function PendingInvitesContainer(): JSX.Element {
const { hash } = useLocation();
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}`,
})),
[],
);
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}`,
}));
}, []);
useEffect(() => {
if (hash === INVITE_MEMBERS_HASH) {

View File

@@ -60,6 +60,9 @@ function DateTimeSelection({
searchStartTime,
]);
const [startTime, setStartTime] = useState<Dayjs>();
const [endTime, setEndTime] = useState<Dayjs>();
const [options, setOptions] = useState(getOptions(location.pathname));
const [refreshButtonHidden, setRefreshButtonHidden] = useState<boolean>(false);
const [customDateTimeVisible, setCustomDTPickerVisible] = useState<boolean>(
@@ -105,6 +108,10 @@ function DateTimeSelection({
return defaultSelectedOption;
};
const [selectedTimeInterval, setSelectedTimeInterval] = useState<Time>(
getDefaultTime(location.pathname),
);
const updateLocalStorageForRoutes = (value: Time): void => {
const preRoutes = getLocalStorageKey(LOCALSTORAGE.METRICS_TIME_IN_DURATION);
if (preRoutes !== null) {
@@ -126,7 +133,7 @@ function DateTimeSelection({
const currentTime = dayjs();
const lastRefresh = dayjs(
selectedTime === 'custom' ? minTime / 1000000 : maxTime / 1000000,
selectedTimeInterval === 'custom' ? minTime / 1000000 : maxTime / 1000000,
);
const secondsDiff = currentTime.diff(lastRefresh, 'seconds');
@@ -153,11 +160,13 @@ function DateTimeSelection({
}
return `Last refresh - ${secondsDiff} sec ago`;
}, [maxTime, minTime, selectedTime]);
}, [maxTime, minTime, selectedTimeInterval]);
const onSelectHandler = (value: Time): void => {
if (value !== 'custom') {
updateTimeInterval(value);
const selectedLabel = getInputLabel(undefined, undefined, value);
setSelectedTimeInterval(selectedLabel as Time);
updateLocalStorageForRoutes(value);
if (refreshButtonHidden) {
setRefreshButtonHidden(false);
@@ -169,7 +178,7 @@ function DateTimeSelection({
};
const onRefreshHandler = (): void => {
onSelectHandler(selectedTime);
onSelectHandler(selectedTimeInterval);
onLastRefreshHandler();
};
@@ -177,6 +186,9 @@ function DateTimeSelection({
if (dateTimeRange !== null) {
const [startTimeMoment, endTimeMoment] = dateTimeRange;
if (startTimeMoment && endTimeMoment) {
setSelectedTimeInterval('custom');
setStartTime(startTimeMoment);
setEndTime(endTimeMoment);
setCustomDTPickerVisible(false);
updateTimeInterval('custom', [
startTimeMoment?.toDate().getTime() || 0,
@@ -227,6 +239,9 @@ function DateTimeSelection({
const [preStartTime = 0, preEndTime = 0] = getTime() || [];
setStartTime(dayjs(preStartTime));
setEndTime(dayjs(preEndTime));
setRefreshButtonHidden(updatedTime === 'custom');
updateTimeInterval(updatedTime, [preStartTime, preEndTime]);
@@ -251,11 +266,7 @@ function DateTimeSelection({
<FormContainer>
<DefaultSelect
onSelect={(value: unknown): void => onSelectHandler(value as Time)}
value={getInputLabel(
dayjs(minTime / 1000000),
dayjs(maxTime / 1000000),
selectedTime,
)}
value={getInputLabel(startTime, endTime, selectedTime)}
data-testid="dropDown"
>
{options.map(({ value, label }) => (

View File

@@ -171,7 +171,7 @@ function Duration(): JSX.Element {
updatedUrl(min, max);
};
const TipComponent = useCallback((value: undefined | number) => {
const TipComponent = useCallback((value) => {
if (value === undefined) {
return <div />;
}

View File

@@ -20,13 +20,11 @@ function TraceGraph(): JSX.Element {
const { loading, error, errorMessage, payload } = spansGraph;
const ChartData = useMemo(
() =>
selectedGroupBy.length === 0
? getChartData(payload)
: getChartDataforGroupBy(payload),
[payload, selectedGroupBy],
);
const ChartData = useMemo(() => {
return selectedGroupBy.length === 0
? getChartData(payload)
: getChartDataforGroupBy(payload);
}, [payload, selectedGroupBy]);
if (error) {
return (

View File

@@ -1,15 +1,13 @@
import { AutoComplete, Input } from 'antd';
import { AutoComplete, AutoCompleteProps, Input, notification } from 'antd';
import getTagFilters from 'api/trace/getTagFilter';
import React, { useCallback, useMemo, useState } from 'react';
import { useQuery } from 'react-query';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { TraceReducer } from 'types/reducer/trace';
import { getTagKeyOptions, onTagKeySelect } from './utils';
function TagsKey(props: TagsKeysProps): JSX.Element {
const [selectLoading, setSelectLoading] = useState<boolean>(false);
const globalTime = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
@@ -20,48 +18,64 @@ function TagsKey(props: TagsKeysProps): JSX.Element {
const traces = useSelector<AppState, TraceReducer>((state) => state.traces);
const { isLoading, data } = useQuery(
[
'getTagKeys',
globalTime.minTime,
globalTime.maxTime,
traces.selectedFilter,
traces.isFilterExclude,
],
{
queryFn: () =>
getTagFilters({
start: globalTime.minTime,
end: globalTime.maxTime,
other: Object.fromEntries(traces.selectedFilter),
isFilterExclude: traces.isFilterExclude,
}),
cacheTime: 120000,
},
);
const [options, setOptions] = useState<AutoCompleteProps['options']>([]);
const options = useMemo(() => getTagKeyOptions(data?.payload), [data]);
const onSearchHandler = useCallback(async () => {
try {
setSelectLoading(true);
const response = await getTagFilters({
start: globalTime.minTime,
end: globalTime.maxTime,
other: Object.fromEntries(traces.selectedFilter),
isFilterExclude: traces.isFilterExclude,
});
const onSelectHandler = useCallback(
(value: unknown) =>
onTagKeySelect(
value,
options,
setSelectedKey,
setLocalSelectedTags,
index,
tag,
),
[index, options, setLocalSelectedTags, tag],
);
if (response.statusCode === 200) {
if (response.payload === null) {
setOptions([
{
value: '',
label: 'No tags available',
},
]);
} else {
setOptions(
response.payload.map((e) => ({
value: e.tagKeys,
label: e.tagKeys,
})),
);
}
} else {
notification.error({
message: response.error || 'Something went wrong',
});
}
setSelectLoading(false);
} catch (error) {
notification.error({
message: 'Something went wrong',
});
setSelectLoading(false);
}
}, [globalTime, traces]);
const counter = useRef(0);
useEffect(() => {
if (counter.current === 0 && selectedKey.length === 0) {
counter.current = 1;
onSearchHandler();
}
}, [onSearchHandler, selectedKey.length]);
return (
<AutoComplete
dropdownClassName="certain-category-search-dropdown"
dropdownMatchSelectWidth={500}
style={{ width: '100%' }}
value={selectedKey}
allowClear
disabled={isLoading}
notFoundContent="No tags available"
showSearch
options={options?.map((e) => ({
label: e.label?.toString(),
@@ -71,9 +85,27 @@ function TagsKey(props: TagsKeysProps): JSX.Element {
option?.label?.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1
}
onChange={(e): void => setSelectedKey(e)}
onSelect={onSelectHandler}
onSelect={(value: unknown): void => {
if (
typeof value === 'string' &&
options &&
options.find((option) => option.value === value)
) {
setSelectedKey(value);
setLocalSelectedTags((tags) => [
...tags.slice(0, index),
{
Key: [value],
Operator: tag.Operator,
Values: tag.Values,
},
...tags.slice(index + 1, tags.length),
]);
}
}}
>
<Input placeholder="Please select" />
<Input disabled={selectLoading} placeholder="Please select" />
</AutoComplete>
);
}

View File

@@ -1,169 +1,81 @@
import { Select } from 'antd';
import { BaseOptionType } from 'antd/es/select';
import getTagValue from 'api/trace/getTagValue';
import React, { useCallback, useMemo, useState } from 'react';
import React, { useState } from 'react';
import { useQuery } from 'react-query';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { TraceReducer } from 'types/reducer/trace';
import { SelectComponent } from './styles';
import {
disableTagValue,
extractTagKey,
extractTagType,
getInitialLocalValue,
getTagValueOptions,
onTagValueChange,
selectOptions,
TagValueTypes,
} from './utils';
import { AutoCompleteComponent } from './styles';
function TagValue(props: TagValueProps): JSX.Element {
const { tag, setLocalSelectedTags, index, tagKey } = props;
const {
Key: selectedKey,
Operator: selectedOperator,
StringValues: selectedStringValues,
NumberValues: selectedNumberValues,
BoolValues: selectedBoolValues,
Values: selectedValues,
} = tag;
const [localTagValue, setLocalTagValue] = useState<TagValueTypes[]>(
getInitialLocalValue(
selectedNumberValues,
selectedBoolValues,
selectedStringValues,
),
);
const [localValue, setLocalValue] = useState<string>(selectedValues[0]);
const globalReducer = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const tagType = useMemo(() => extractTagType(tagKey), [tagKey]);
const { isLoading, data } = useQuery(
['tagKey', globalReducer.minTime, globalReducer.maxTime, tagKey, tagType],
['tagKey', globalReducer.minTime, globalReducer.maxTime, tagKey],
{
queryFn: () =>
getTagValue({
end: globalReducer.maxTime,
start: globalReducer.minTime,
tagKey: {
Key: extractTagKey(tagKey),
Type: tagType,
},
tagKey,
}),
},
);
const tagValueDisabled = useMemo(
() =>
disableTagValue(
selectedOperator,
setLocalTagValue,
selectedKey,
setLocalSelectedTags,
index,
),
[index, selectedKey, selectedOperator, setLocalSelectedTags],
);
const onSetLocalValue = useCallback(() => {
setLocalTagValue([]);
}, []);
const onSelectedHandler = useCallback(
(value: unknown) => {
if (
typeof value === 'number' ||
(typeof value === 'string' && !Number.isNaN(Number(value)) && value !== ' ')
) {
setLocalTagValue([value]);
setLocalSelectedTags((tags) => [
...tags.slice(0, index),
{
Key: selectedKey,
Operator: selectedOperator,
StringValues: [],
NumberValues: [Number(value)],
BoolValues: [],
},
...tags.slice(index + 1, tags.length),
]);
} else if (
typeof value === 'boolean' ||
value === 'true' ||
value === 'false'
) {
setLocalTagValue([value]);
setLocalSelectedTags((tags) => [
...tags.slice(0, index),
{
Key: selectedKey,
Operator: selectedOperator,
StringValues: [],
NumberValues: [],
BoolValues: [value === 'true' || value === true],
},
...tags.slice(index + 1, tags.length),
]);
} else if (typeof value === 'string') {
setLocalTagValue([value]);
setLocalSelectedTags((tags) => [
...tags.slice(0, index),
{
Key: selectedKey,
Operator: selectedOperator,
StringValues: [value],
NumberValues: [],
BoolValues: [],
},
...tags.slice(index + 1, tags.length),
]);
}
},
[index, selectedKey, selectedOperator, setLocalSelectedTags],
);
const onChangeHandler = useCallback(
(value: unknown) => onTagValueChange(value, setLocalTagValue),
[],
);
const getFilterOptions = useCallback(
(inputValue: string, option?: BaseOptionType): boolean => {
if (typeof option?.label === 'string') {
return option?.label.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1;
}
return false;
},
[],
);
return (
<SelectComponent
loading={isLoading}
options={getTagValueOptions(data?.payload, tagType)}
mode="tags"
<AutoCompleteComponent
options={data?.payload?.map((e) => ({
label: e.tagValues,
value: e.tagValues,
}))}
allowClear
onClear={onSetLocalValue}
onDeselect={onSetLocalValue}
defaultOpen
showSearch
filterOption={getFilterOptions}
disabled={isLoading || tagValueDisabled}
value={localTagValue}
onChange={onChangeHandler}
onSelect={onSelectedHandler}
filterOption={(inputValue, option): boolean =>
option?.label.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1
}
disabled={isLoading}
value={localValue}
onChange={(values): void => {
if (typeof values === 'string') {
setLocalValue(values);
}
}}
onSelect={(value: unknown): void => {
if (typeof value === 'string') {
setLocalValue(value);
setLocalSelectedTags((tags) => [
...tags.slice(0, index),
{
Key: selectedKey,
Operator: selectedOperator,
Values: [value],
},
...tags.slice(index + 1, tags.length),
]);
}
}}
>
{selectOptions(data?.payload, tagType)?.map((suggestion) => (
<Select.Option key={suggestion.toString()} value={suggestion}>
{suggestion}
</Select.Option>
))}
</SelectComponent>
{data &&
data.payload &&
data.payload.map((suggestion) => (
<Select.Option key={suggestion.tagValues} value={suggestion.tagValues}>
{suggestion.tagValues}
</Select.Option>
))}
</AutoCompleteComponent>
);
}

View File

@@ -1,112 +1,42 @@
import { CloseOutlined } from '@ant-design/icons';
import { Select } from 'antd';
import React from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { TraceReducer } from 'types/reducer/trace';
import { Container, IconContainer, SelectComponent } from './styles';
import TagsKey from './TagKey';
import TagValue from './TagValue';
import { mapOperators } from './utils';
const { Option } = Select;
type Tags = FlatArray<TraceReducer['selectedTags'], 1>['Operator'];
const StringBoolNumber = ['string', 'number', 'bool'];
const Number = ['number'];
const String = ['string'];
export interface AllMenuProps {
interface AllMenuProps {
key: Tags | '';
value: string;
supportedTypes: string[];
}
export const AllMenu: AllMenuProps[] = [
const AllMenu: AllMenuProps[] = [
{
key: 'Equals',
value: 'EQUALS',
supportedTypes: StringBoolNumber,
},
{
key: 'NotEquals',
value: 'NOT EQUALS',
supportedTypes: StringBoolNumber,
},
{
key: 'In',
key: 'in',
value: 'IN',
supportedTypes: String,
},
{
key: 'NotIn',
key: 'not in',
value: 'NOT IN',
supportedTypes: String,
},
{
key: 'Exists',
value: 'EXISTS',
supportedTypes: StringBoolNumber,
},
{
key: 'NotExists',
value: 'NOT EXISTS',
supportedTypes: StringBoolNumber,
},
{
key: 'GreaterThan',
value: 'GREATER THAN',
supportedTypes: Number,
},
{
key: 'LessThan',
value: 'LESS THAN',
supportedTypes: Number,
},
{
key: 'GreaterThanEquals',
value: 'GREATER THAN OR EQUALS',
supportedTypes: Number,
},
{
key: 'LessThanEquals',
value: 'LESS THAN OR EQUALS',
supportedTypes: Number,
},
{
key: 'StartsWith',
value: 'STARTS WITH',
supportedTypes: String,
},
{
key: 'NotStartsWith',
value: 'NOT STARTS WITH',
supportedTypes: String,
},
{
key: 'Contains',
value: 'CONTAINS',
supportedTypes: String,
},
{
key: 'NotContains',
value: 'NOT CONTAINS',
supportedTypes: String,
},
];
function SingleTags(props: AllTagsProps): JSX.Element {
const {
tag,
onCloseHandler,
setLocalSelectedTags,
index,
localSelectedTags,
} = props;
const traces = useSelector<AppState, TraceReducer>((state) => state.traces);
const { tag, onCloseHandler, setLocalSelectedTags, index } = props;
const {
Key: selectedKey,
Operator: selectedOperator,
StringValues: selectedStringValues,
NumberValues: selectedNumberValues,
BoolValues: selectedBoolValues,
Values: selectedValues,
} = tag;
const onDeleteTagHandler = (index: number): void => {
@@ -116,15 +46,13 @@ function SingleTags(props: AllTagsProps): JSX.Element {
const onChangeOperatorHandler = (key: unknown): void => {
if (typeof key === 'string') {
setLocalSelectedTags([
...localSelectedTags.slice(0, index),
...traces.selectedTags.slice(0, index),
{
Key: selectedKey,
StringValues: selectedStringValues,
NumberValues: selectedNumberValues,
BoolValues: selectedBoolValues,
Values: selectedValues,
Operator: key as Tags,
},
...localSelectedTags.slice(index + 1, localSelectedTags.length),
...traces.selectedTags.slice(index + 1, traces.selectedTags.length),
]);
}
};
@@ -140,14 +68,11 @@ function SingleTags(props: AllTagsProps): JSX.Element {
onChange={onChangeOperatorHandler}
value={AllMenu.find((e) => e.key === selectedOperator)?.value || ''}
>
{
// filter out the operator that does not include supported type of the selected key
mapOperators(selectedKey).map((e) => (
<Option key={e.value} value={e.key}>
{e.value}
</Option>
))
}
{AllMenu.map((e) => (
<Option key={e.value} value={e.key}>
{e.value}
</Option>
))}
</SelectComponent>
{selectedKey[0] ? (
@@ -175,7 +100,6 @@ interface AllTagsProps {
setLocalSelectedTags: React.Dispatch<
React.SetStateAction<TraceReducer['selectedTags']>
>;
localSelectedTags: TraceReducer['selectedTags'];
}
export interface Value {

View File

@@ -1,4 +1,4 @@
import { Select, Space } from 'antd';
import { AutoComplete, Select, Space } from 'antd';
import styled from 'styled-components';
export const SpaceComponent = styled(Space)`
@@ -7,6 +7,12 @@ export const SpaceComponent = styled(Space)`
}
`;
export const SelectComponent = styled(Select)`
&&& {
width: 100%;
}
`;
export const Container = styled(Space)`
&&& {
display: flex;
@@ -31,7 +37,7 @@ export const IconContainer = styled.div`
margin-left: 1.125rem;
`;
export const SelectComponent = styled(Select)`
export const AutoCompleteComponent = styled(AutoComplete)`
&&& {
width: 100%;
}

View File

@@ -1,204 +0,0 @@
import { AutoCompleteProps } from 'antd';
import { DefaultOptionType } from 'antd/es/select';
import { PayloadProps as TagKeyPayload } from 'types/api/trace/getTagFilters';
import { PayloadProps as TagValuePayload } from 'types/api/trace/getTagValue';
import { OperatorValues, Tags } from 'types/reducer/trace';
import { AllMenu, AllMenuProps } from '.';
export type TagValueTypes = string | number | boolean;
/**
* @description extract tag filters from payload
*/
export const extractTagFilters = (
payload: TagKeyPayload,
): DefaultOptionType[] => {
const tagFilters: string[] = [];
payload.stringTagKeys.forEach((element) => {
tagFilters.push(`${element}.(string)`);
});
payload.numberTagKeys.forEach((element) => {
tagFilters.push(`${element}.(number)`);
});
payload.boolTagKeys.forEach((element) => {
tagFilters.push(`${element}.(bool)`);
});
return tagFilters.map((e) => ({
value: e,
label: e,
}));
};
export const extractTagType = (tagKey: string): string => {
if (tagKey?.includes('.(string)')) {
return 'string';
}
if (tagKey?.includes('.(number)')) {
return 'number';
}
if (tagKey?.includes('.(bool)')) {
return 'bool';
}
return 'string';
};
export const extractTagKey = (tagKey: string): string => {
const tag = tagKey.split('.(');
if (tag && tag.length > 0) {
return tag[0];
}
return '';
};
export function onTagValueChange(
values: unknown,
setLocalValue: React.Dispatch<React.SetStateAction<TagValueTypes[]>>,
): void {
if (Array.isArray(values) && values.length > 0) {
if (typeof values[0] === 'number' || typeof values[0] === 'boolean') {
setLocalValue(values);
} else if (typeof values[0] === 'string') {
if (values[0] === 'true' || values[0] === 'false') {
setLocalValue([values[0] === 'true']);
} else if (values[0] !== ' ' && !Number.isNaN(Number(values[0]))) {
setLocalValue([Number(values[0])]);
} else {
setLocalValue([values[0]]);
}
}
}
}
export function disableTagValue(
selectedOperator: OperatorValues,
setLocalValue: React.Dispatch<React.SetStateAction<TagValueTypes[]>>,
selectedKeys: string[],
setLocalSelectedTags: React.Dispatch<React.SetStateAction<Tags[]>>,
index: number,
): boolean {
if (selectedOperator === 'Exists' || selectedOperator === 'NotExists') {
setLocalValue([]);
setLocalSelectedTags((tags) => [
...tags.slice(0, index),
{
Key: selectedKeys,
Operator: selectedOperator,
StringValues: [],
NumberValues: [],
BoolValues: [],
},
...tags.slice(index + 1, tags.length),
]);
return true;
}
return false;
}
export function getInitialLocalValue(
selectedNumberValues: number[],
selectedBoolValues: boolean[],
selectedStringValues: string[],
): TagValueTypes[] {
if (selectedStringValues && selectedStringValues.length > 0) {
return selectedStringValues;
}
if (selectedNumberValues && selectedNumberValues.length > 0) {
return selectedNumberValues;
}
if (selectedBoolValues && selectedBoolValues.length > 0) {
return selectedBoolValues;
}
return selectedStringValues;
}
export function getTagValueOptions(
payload: TagValuePayload | null | undefined,
tagType: string,
): Array<{ label: string; value: TagValueTypes }> | undefined {
if (tagType === 'string') {
return payload?.stringTagValues?.map((e) => ({
label: e,
value: e,
}));
}
if (tagType === 'number') {
return payload?.numberTagValues?.map((e) => ({
label: e.toString(),
value: e,
}));
}
if (tagType === 'bool') {
return payload?.boolTagValues?.map((e) => ({
label: e.toString(),
value: e,
}));
}
return [];
}
export function getTagKeyOptions(
payload: TagKeyPayload | null | undefined,
): DefaultOptionType[] {
if (payload === null) {
return [
{
value: '',
label: 'No tags available',
},
];
}
if (payload != null) {
return extractTagFilters(payload);
}
return [];
}
export function selectOptions(
payload: TagValuePayload | null | undefined,
tagType: string,
): string[] | boolean[] | number[] | undefined {
if (tagType === 'string') {
return payload?.stringTagValues;
}
if (tagType === 'number') {
return payload?.numberTagValues;
}
if (tagType === 'bool') {
return payload?.boolTagValues;
}
return [];
}
export function mapOperators(selectedKey: string[]): AllMenuProps[] {
return AllMenu.filter((e) =>
e?.supportedTypes?.includes(extractTagType(selectedKey[0])),
);
}
export function onTagKeySelect(
value: unknown,
options: AutoCompleteProps['options'],
setSelectedKey: React.Dispatch<React.SetStateAction<string>>,
setLocalSelectedTags: React.Dispatch<React.SetStateAction<Tags[]>>,
index: number,
tag: Tags,
): void {
if (
typeof value === 'string' &&
options &&
options.find((option) => option.value === value)
) {
setSelectedKey(value);
setLocalSelectedTags((tags) => [
...tags.slice(0, index),
{
Key: [value],
Operator: tag.Operator,
StringValues: tag.StringValues,
NumberValues: tag.NumberValues,
BoolValues: tag.BoolValues,
},
...tags.slice(index + 1, tags.length),
]);
}
}

View File

@@ -16,6 +16,7 @@ import {
Container,
CurrentTagsContainer,
ErrorContainer,
Wrapper,
} from './styles';
import Tags from './Tag';
@@ -40,10 +41,8 @@ function AllTags({
...tags,
{
Key: [],
Operator: 'Equals',
StringValues: [],
NumberValues: [],
BoolValues: [],
Operator: 'in',
Values: [],
},
]);
};
@@ -89,30 +88,32 @@ function AllTags({
return (
<Container>
<Typography>Tags</Typography>
<Wrapper>
<Typography>Tags</Typography>
<CurrentTagsContainer>
{localSelectedTags.map((tags, index) => (
<Tags
key={tags.Key.join(',')}
tag={tags}
index={index}
onCloseHandler={(): void => onCloseHandler(index)}
setLocalSelectedTags={setLocalSelectedTags}
localSelectedTags={localSelectedTags}
/>
))}
</CurrentTagsContainer>
<CurrentTagsContainer>
{localSelectedTags.map((tags, index) => (
<Tags
key={tags.Key.join(',')}
tag={tags}
index={index}
onCloseHandler={(): void => onCloseHandler(index)}
setLocalSelectedTags={setLocalSelectedTags}
/>
))}
</CurrentTagsContainer>
<Space wrap direction="horizontal">
<Button type="primary" onClick={onTagAddHandler} icon={<PlusOutlined />}>
Add Tags Filter
</Button>
<Space wrap direction="horizontal">
<Button type="primary" onClick={onTagAddHandler} icon={<PlusOutlined />}>
Add Tags Filter
</Button>
<Text ellipsis>
Results will include spans with ALL the specified tags ( Rows are `ANDed` )
</Text>
</Space>
<Text ellipsis>
Results will include spans with ALL the specified tags ( Rows are `anded`
)
</Text>
</Space>
</Wrapper>
<ButtonContainer>
<Space align="start">

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