Merge pull request #2147 from SigNoz/release/v0.15.0

Release/v0.15.0
This commit is contained in:
Ankit Nayan 2023-01-31 16:58:45 +05:30 committed by GitHub
commit 3b01bb2614
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
199 changed files with 5130 additions and 3297 deletions

View File

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

View File

@ -54,7 +54,7 @@ build-push-frontend:
@echo "--> Building and pushing frontend docker image"
@echo "------------------"
@cd $(FRONTEND_DIRECTORY) && \
docker buildx build --file Dockerfile --progress plane --push --platform linux/amd64 \
docker buildx build --file Dockerfile --progress plane --push --platform linux/arm64,linux/amd64 \
--tag $(REPONAME)/$(FRONTEND_DOCKER_IMAGE):$(DOCKER_TAG) .
# Steps to build and push docker image of query service

View File

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

View File

@ -137,7 +137,7 @@ services:
condition: on-failure
query-service:
image: signoz/query-service:0.14.0
image: signoz/query-service:0.15.0
command: ["-config=/root/config/prometheus.yml"]
# ports:
# - "6060:6060" # pprof port
@ -166,7 +166,7 @@ services:
<<: *clickhouse-depend
frontend:
image: signoz/frontend:0.14.0
image: signoz/frontend:0.15.0
deploy:
restart_policy:
condition: on-failure
@ -179,7 +179,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector:
image: signoz/signoz-otel-collector:0.66.2
image: signoz/signoz-otel-collector:0.66.3
command: ["--config=/etc/otel-collector-config.yaml"]
user: root # required for reading docker container logs
volumes:
@ -207,7 +207,7 @@ services:
<<: *clickhouse-depend
otel-collector-metrics:
image: signoz/signoz-otel-collector:0.66.2
image: signoz/signoz-otel-collector:0.66.3
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
volumes:
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml

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.2
image: signoz/signoz-otel-collector:0.66.3
command: ["--config=/etc/otel-collector-config.yaml"]
# user: root # required for reading docker container logs
volumes:
@ -67,7 +67,7 @@ services:
otel-collector-metrics:
container_name: otel-collector-metrics
image: signoz/signoz-otel-collector:0.66.2
image: signoz/signoz-otel-collector:0.66.3
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
volumes:
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml

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.14.0}
image: signoz/query-service:${DOCKER_TAG:-0.15.0}
container_name: query-service
command: ["-config=/root/config/prometheus.yml"]
# ports:
@ -181,7 +181,7 @@ services:
<<: *clickhouse-depend
frontend:
image: signoz/frontend:${DOCKER_TAG:-0.14.0}
image: signoz/frontend:${DOCKER_TAG:-0.15.0}
container_name: frontend
restart: on-failure
depends_on:
@ -193,7 +193,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector:
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.66.2}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.66.3}
command: ["--config=/etc/otel-collector-config.yaml"]
user: root # required for reading docker container logs
volumes:
@ -218,7 +218,7 @@ services:
<<: *clickhouse-depend
otel-collector-metrics:
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.66.2}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.66.3}
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
volumes:
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml

View File

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

View File

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

View File

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

View File

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

1
frontend/.yarnrc Normal file
View File

@ -0,0 +1 @@
network-timeout 600000

View File

@ -9,8 +9,9 @@ ARG TARGETARCH
WORKDIR /frontend
# Copy the package.json to install dependencies
# Copy the package.json and .yarnrc files prior to install dependencies
COPY package.json ./
COPY .yarnrc ./
# Install the dependencies and make the folder
RUN CI=1 yarn install

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.4.0",
"chart.js": "3.9.1",
"chartjs-adapter-date-fns": "^2.0.0",
"chartjs-plugin-annotation": "^1.4.0",
"color": "^4.2.1",
@ -70,16 +70,18 @@
"less-loader": "^10.2.0",
"lodash-es": "^4.17.21",
"mini-css-extract-plugin": "2.4.5",
"react": "17.0.0",
"react-dom": "17.0.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-force-graph": "^1.41.0",
"react-graph-vis": "^1.0.5",
"react-grid-layout": "^1.3.4",
"react-i18next": "^11.16.1",
"react-intersection-observer": "9.4.1",
"react-query": "^3.34.19",
"react-redux": "^7.2.2",
"react-router-dom": "^5.2.0",
"react-use": "^17.3.2",
"react-virtuoso": "4.0.3",
"react-vis": "^1.11.7",
"redux": "^4.0.5",
"redux-thunk": "^2.3.0",
@ -132,8 +134,8 @@
"@types/lodash-es": "^4.17.4",
"@types/mini-css-extract-plugin": "^2.5.1",
"@types/node": "^16.10.3",
"@types/react": "^17.0.0",
"@types/react-dom": "^16.9.9",
"@types/react": "18.0.26",
"@types/react-dom": "18.0.10",
"@types/react-grid-layout": "^1.1.2",
"@types/react-redux": "^7.1.11",
"@types/react-router-dom": "^5.1.6",
@ -186,7 +188,7 @@
]
},
"resolutions": {
"@types/react": "17.0.0",
"@types/react-dom": "17.0.0"
"@types/react": "18.0.26",
"@types/react-dom": "18.0.10"
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -29,3 +29,7 @@ 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) => {
return item.match(path, currentVersion, userFlags);
});
const c = allComponentMap.find((item) =>
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 }: SpinnerProps): JSX.Element {
function Spinner({ size, tip, height, style }: SpinnerProps): JSX.Element {
return (
<SpinerStyle height={height}>
<SpinerStyle height={height} style={style}>
<Spin spinning size={size} tip={tip} indicator={<LoadingOutlined spin />} />
</SpinerStyle>
);
@ -16,11 +16,13 @@ interface SpinnerProps {
size?: SpinProps['size'];
tip?: SpinProps['tip'];
height?: React.CSSProperties['height'];
style?: React.CSSProperties;
}
Spinner.defaultProps = {
size: undefined,
tip: undefined,
height: undefined,
style: {},
};
export default Spinner;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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(() => {
return {
const graphData = useMemo(
() => ({
labels: logsAggregate.map((s) => new Date(s.timestamp / 1000000)),
datasets: [
{
@ -86,8 +86,9 @@ function LogsAggregate({ getLogsAggregate }: LogsAggregateProps): JSX.Element {
backgroundColor: blue[4],
},
],
};
}, [logsAggregate]);
}),
[logsAggregate],
);
return (
<Container>

View File

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

View File

@ -175,6 +175,7 @@ export interface QueryBuilderProps {
onDropDownToggleHandler: (value: boolean) => VoidFunction;
fieldsQuery: QueryFields[][];
setFieldsQuery: (q: QueryFields[][]) => void;
syncKeyPrefix: () => void;
}
function QueryBuilder({
@ -182,6 +183,7 @@ function QueryBuilder({
fieldsQuery,
setFieldsQuery,
onDropDownToggleHandler,
syncKeyPrefix,
}: QueryBuilderProps): JSX.Element {
const handleUpdate = (query: Query, queryIndex: number): void => {
const updated = [...fieldsQuery];
@ -195,6 +197,9 @@ function QueryBuilder({
else updated.splice(queryIndex, 2);
setFieldsQuery(updated);
// initiate re-render query panel
syncKeyPrefix();
};
const QueryUI = (

View File

@ -47,6 +47,14 @@ function SearchFields({
}
}, [parsedQuery]);
// syncKeyPrefix initiates re-render. useful in situations like
// delete field (in search panel). this method allows condiitonally
// setting keyPrefix as doing it on every update of query initiates
// a re-render. this is a problem for text fields where input focus goes away.
const syncKeyPrefix = (): void => {
keyPrefixRef.current = hashCode(JSON.stringify(fieldsQuery));
};
const addSuggestedField = useCallback(
(name: string): void => {
if (!name) {
@ -98,6 +106,7 @@ function SearchFields({
onDropDownToggleHandler={onDropDownToggleHandler}
fieldsQuery={fieldsQuery}
setFieldsQuery={setFieldsQuery}
syncKeyPrefix={syncKeyPrefix}
/>
<SearchFieldsActionBar
applyUpdate={applyUpdate}

View File

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

View File

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

View File

@ -1,10 +1,9 @@
/* eslint-disable no-nested-ternary */
import { Typography } from 'antd';
import LogItem from 'components/Logs/LogItem';
import Spinner from 'components/Spinner';
import map from 'lodash-es/map';
import React, { memo } from 'react';
import React, { memo, useCallback, useMemo } from 'react';
import { useSelector } from 'react-redux';
import { Virtuoso } from 'react-virtuoso';
import { AppState } from 'store/reducers';
import { ILogsReducer } from 'types/reducer/logs';
@ -15,6 +14,24 @@ function LogsTable(): JSX.Element {
(state) => state.logs,
);
const isLiveTail = useMemo(() => logs.length === 0 && liveTail === 'PLAYING', [
logs?.length,
liveTail,
]);
const isNoLogs = useMemo(() => logs.length === 0 && liveTail === 'STOPPED', [
logs?.length,
liveTail,
]);
const getItemContent = useCallback(
(index: number): JSX.Element => {
const log = logs[index];
return <LogItem key={log.id} logData={log} />;
},
[logs],
);
if (isLoading) {
return <Spinner height={20} tip="Getting Logs" />;
}
@ -24,13 +41,15 @@ function LogsTable(): JSX.Element {
<Heading>
<Typography.Text>Event</Typography.Text>
</Heading>
{Array.isArray(logs) && logs.length > 0 ? (
map(logs, (log) => <LogItem key={log.id} logData={log} />)
) : liveTail === 'PLAYING' ? (
<span>Getting live logs...</span>
) : (
<span>No log lines found</span>
)}
{isLiveTail && <Typography>Getting live logs...</Typography>}
{isNoLogs && <Typography>No log lines found</Typography>}
<Virtuoso
useWindowScroll
totalCount={logs.length}
itemContent={getItemContent}
/>
</Container>
);
}

View File

@ -0,0 +1,20 @@
import { Widgets } from 'types/api/dashboard/getAll';
import { v4 } from 'uuid';
export const getWidgetQueryBuilder = (query: Widgets['query']): Widgets => ({
description: '',
id: v4(),
isStacked: false,
nullZeroValues: '',
opacity: '0',
panelTypes: 'TIME_SERIES',
query,
queryData: {
data: { queryData: [] },
error: false,
errorMessage: '',
loading: false,
},
timePreferance: 'GLOBAL_TIME',
title: '',
});

View File

@ -19,13 +19,21 @@ export const databaseCallsRPS = ({
} => {
const metricName = 'signoz_db_latency_count';
const groupBy = ['db_system'];
const itemsA = [
{
id: '',
key: 'service_name',
op: 'IN',
value: [`${servicename}`],
},
...tagFilterItems,
];
return getQueryBuilderQueries({
metricName,
legend,
groupBy,
servicename,
tagFilterItems,
legend,
itemsA,
});
};
@ -42,14 +50,24 @@ export const databaseCallsAvgDuration = ({
const legendFormula = '';
const legend = '';
const disabled = true;
const additionalItemsA = [
{
id: '',
key: 'service_name',
op: 'IN',
value: [`${servicename}`],
},
...tagFilterItems,
];
const additionalItemsB = additionalItemsA;
return getQueryBuilderQuerieswithFormula({
servicename,
legend,
disabled,
tagFilterItems,
metricNameA,
metricNameB,
additionalItemsA,
additionalItemsB,
legend,
disabled,
expression,
legendFormula,
});

View File

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

View File

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

View File

@ -0,0 +1,112 @@
import {
IMetricsBuilderFormula,
IMetricsBuilderQuery,
IQueryBuilderTagFilterItems,
} from 'types/api/dashboard/getAll';
import {
getQueryBuilderQueries,
getQueryBuilderQuerieswithFormula,
} from './MetricsPageQueriesFactory';
export const operationPerSec = ({
servicename,
tagFilterItems,
topLevelOperations,
}: OperationPerSecProps): IOverviewQueries => {
const metricName = 'signoz_latency_count';
const legend = 'Operations';
const itemsA = [
{
id: '',
key: 'service_name',
op: 'IN',
value: [`${servicename}`],
},
{
id: '',
key: 'operation',
op: 'MATCH',
value: topLevelOperations,
},
...tagFilterItems,
];
return getQueryBuilderQueries({
metricName,
legend,
itemsA,
});
};
export const errorPercentage = ({
servicename,
tagFilterItems,
topLevelOperations,
}: OperationPerSecProps): IOverviewQueries => {
const metricNameA = 'signoz_calls_total';
const metricNameB = 'signoz_calls_total';
const additionalItemsA = [
{
id: '',
key: 'service_name',
op: 'IN',
value: [`${servicename}`],
},
{
id: '',
key: 'operation',
op: 'MATCH',
value: topLevelOperations,
},
{
id: '',
key: 'status_code',
op: 'IN',
value: ['STATUS_CODE_ERROR'],
},
...tagFilterItems,
];
const additionalItemsB = [
{
id: '',
key: 'service_name',
op: 'IN',
value: [`${servicename}`],
},
{
id: '',
key: 'operation',
op: 'MATCH',
value: topLevelOperations,
},
...tagFilterItems,
];
const legendFormula = 'Error Percentage';
const legend = legendFormula;
const expression = 'A*100/B';
const disabled = true;
return getQueryBuilderQuerieswithFormula({
metricNameA,
metricNameB,
additionalItemsA,
additionalItemsB,
legend,
disabled,
expression,
legendFormula,
});
};
export interface OperationPerSecProps {
servicename: string | undefined;
tagFilterItems: IQueryBuilderTagFilterItems[];
topLevelOperations: string[];
}
interface IOverviewQueries {
formulas: IMetricsBuilderFormula[];
queryBuilder: IMetricsBuilderQuery[];
}

View File

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

View File

@ -25,6 +25,35 @@ function DBCall({ getWidgetQueryBuilder }: DBCallProps): JSX.Element {
);
const legend = '{{db_system}}';
const databaseCallsRPSWidget = useMemo(
() =>
getWidgetQueryBuilder({
queryType: 1,
promQL: [],
metricsBuilder: databaseCallsRPS({
servicename,
legend,
tagFilterItems,
}),
clickHouse: [],
}),
[getWidgetQueryBuilder, servicename, tagFilterItems],
);
const databaseCallsAverageDurationWidget = useMemo(
() =>
getWidgetQueryBuilder({
queryType: 1,
promQL: [],
metricsBuilder: databaseCallsAvgDuration({
servicename,
tagFilterItems,
}),
clickHouse: [],
}),
[getWidgetQueryBuilder, servicename, tagFilterItems],
);
return (
<Row gutter={24}>
<Col span={12}>
@ -34,16 +63,7 @@ function DBCall({ getWidgetQueryBuilder }: DBCallProps): JSX.Element {
<FullView
name="database_call_rps"
fullViewOptions={false}
widget={getWidgetQueryBuilder({
queryType: 1,
promQL: [],
metricsBuilder: databaseCallsRPS({
servicename,
legend,
tagFilterItems,
}),
clickHouse: [],
})}
widget={databaseCallsRPSWidget}
yAxisUnit="reqps"
/>
</GraphContainer>
@ -57,15 +77,7 @@ function DBCall({ getWidgetQueryBuilder }: DBCallProps): JSX.Element {
<FullView
name="database_call_avg_duration"
fullViewOptions={false}
widget={getWidgetQueryBuilder({
queryType: 1,
promQL: [],
metricsBuilder: databaseCallsAvgDuration({
servicename,
tagFilterItems,
}),
clickHouse: [],
})}
widget={databaseCallsAverageDurationWidget}
yAxisUnit="ms"
/>
</GraphContainer>

View File

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

View File

@ -2,44 +2,81 @@ import { ActiveElement, Chart, ChartData, ChartEvent } from 'chart.js';
import Graph from 'components/Graph';
import { METRICS_PAGE_QUERY_PARAM } from 'constants/query';
import ROUTES from 'constants/routes';
import FullView from 'container/GridGraphLayout/Graph/FullView';
import FullView from 'container/GridGraphLayout/Graph/FullView/index.metricsBuilder';
import convertToNanoSecondsToSecond from 'lib/convertToNanoSecondsToSecond';
import { colors } from 'lib/getRandomColor';
import history from 'lib/history';
import { convertRawQueriesToTraceSelectedTags } from 'lib/resourceAttributes';
import { escapeRegExp } from 'lodash-es';
import React, { useMemo, useRef } from 'react';
import { useSelector } from 'react-redux';
import {
convertRawQueriesToTraceSelectedTags,
resourceAttributesToTagFilterItems,
} from 'lib/resourceAttributes';
import React, { useCallback, useMemo, useRef } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';
import { UpdateTimeInterval } from 'store/actions';
import { AppState } from 'store/reducers';
import { PromQLWidgets } from 'types/api/dashboard/getAll';
import { Widgets } from 'types/api/dashboard/getAll';
import MetricReducer from 'types/reducer/metrics';
import {
errorPercentage,
operationPerSec,
} from '../MetricsPageQueries/OverviewQueries';
import { Card, Col, GraphContainer, GraphTitle, Row } from '../styles';
import TopOperationsTable from '../TopOperationsTable';
import { Button } from './styles';
function Application({ getWidget }: DashboardProps): JSX.Element {
function Application({ getWidgetQueryBuilder }: DashboardProps): JSX.Element {
const { servicename } = useParams<{ servicename?: string }>();
const selectedTimeStamp = useRef(0);
const dispatch = useDispatch();
const {
topOperations,
serviceOverview,
resourceAttributePromQLQuery,
resourceAttributeQueries,
topLevelOperations,
} = useSelector<AppState, MetricReducer>((state) => state.metrics);
const operationsRegex = useMemo(() => {
return encodeURIComponent(
topLevelOperations.map((e) => escapeRegExp(e)).join('|'),
);
}, [topLevelOperations]);
const selectedTraceTags: string = JSON.stringify(
convertRawQueriesToTraceSelectedTags(resourceAttributeQueries, 'array') || [],
);
const tagFilterItems = useMemo(
() => resourceAttributesToTagFilterItems(resourceAttributeQueries) || [],
[resourceAttributeQueries],
);
const operationPerSecWidget = useMemo(
() =>
getWidgetQueryBuilder({
queryType: 1,
promQL: [],
metricsBuilder: operationPerSec({
servicename,
tagFilterItems,
topLevelOperations,
}),
clickHouse: [],
}),
[getWidgetQueryBuilder, servicename, topLevelOperations, tagFilterItems],
);
const errorPercentageWidget = useMemo(
() =>
getWidgetQueryBuilder({
queryType: 1,
promQL: [],
metricsBuilder: errorPercentage({
servicename,
tagFilterItems,
topLevelOperations,
}),
clickHouse: [],
}),
[servicename, topLevelOperations, tagFilterItems, getWidgetQueryBuilder],
);
const onTracePopupClick = (timestamp: number): void => {
const currentTime = timestamp;
const tPlusOne = timestamp + 1 * 60 * 1000;
@ -92,6 +129,16 @@ function Application({ getWidget }: DashboardProps): JSX.Element {
}
};
const onDragSelect = useCallback(
(start: number, end: number) => {
const startTimestamp = Math.trunc(start);
const endTimestamp = Math.trunc(end);
dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp]));
},
[dispatch],
);
const onErrorTrackHandler = (timestamp: number): void => {
const currentTime = timestamp;
const tPlusOne = timestamp + 1 * 60 * 1000;
@ -166,13 +213,13 @@ function Application({ getWidget }: DashboardProps): JSX.Element {
pointRadius: 1.5,
},
],
labels: serviceOverview.map((e) => {
return new Date(
parseFloat(convertToNanoSecondsToSecond(e.timestamp)),
);
}),
labels: serviceOverview.map(
(e) =>
new Date(parseFloat(convertToNanoSecondsToSecond(e.timestamp))),
),
}}
yAxisUnit="ms"
onDragSelect={onDragSelect}
/>
</GraphContainer>
</Card>
@ -198,13 +245,9 @@ function Application({ getWidget }: DashboardProps): JSX.Element {
onClickHandler={(event, element, chart, data): void => {
onClickHandler(event, element, chart, data, 'Rate');
}}
widget={getWidget([
{
query: `sum(rate(signoz_latency_count{service_name="${servicename}", operation=~\`${operationsRegex}\`${resourceAttributePromQLQuery}}[5m]))`,
legend: 'Operations',
},
])}
widget={operationPerSecWidget}
yAxisUnit="ops"
onDragSelect={onDragSelect}
/>
</GraphContainer>
</Card>
@ -232,13 +275,9 @@ function Application({ getWidget }: DashboardProps): JSX.Element {
onClickHandler={(ChartEvent, activeElements, chart, data): void => {
onClickHandler(ChartEvent, activeElements, chart, data, 'Error');
}}
widget={getWidget([
{
query: `max(sum(rate(signoz_calls_total{service_name="${servicename}", operation=~\`${operationsRegex}\`, status_code="STATUS_CODE_ERROR"${resourceAttributePromQLQuery}}[5m]) OR rate(signoz_calls_total{service_name="${servicename}", operation=~\`${operationsRegex}\`, http_status_code=~"5.."${resourceAttributePromQLQuery}}[5m]))*100/sum(rate(signoz_calls_total{service_name="${servicename}", operation=~\`${operationsRegex}\`${resourceAttributePromQLQuery}}[5m]))) < 1000 OR vector(0)`,
legend: 'Error Percentage',
},
])}
widget={errorPercentageWidget}
yAxisUnit="%"
onDragSelect={onDragSelect}
/>
</GraphContainer>
</Card>
@ -255,7 +294,7 @@ function Application({ getWidget }: DashboardProps): JSX.Element {
}
interface DashboardProps {
getWidget: (query: PromQLWidgets['query']) => PromQLWidgets;
getWidgetQueryBuilder: (query: Widgets['query']) => Widgets;
}
export default Application;

View File

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

View File

@ -1,60 +1,17 @@
import RouteTab from 'components/RouteTab';
import ROUTES from 'constants/routes';
import React from 'react';
import React, { memo } from 'react';
import { generatePath, useParams } from 'react-router-dom';
import { useLocation } from 'react-use';
import { PromQLWidgets, Widgets } from 'types/api/dashboard/getAll';
import { v4 } from 'uuid';
import { getWidgetQueryBuilder } from './MetricsApplication.factory';
import ResourceAttributesFilter from './ResourceAttributesFilter';
import DBCall from './Tabs/DBCall';
import External from './Tabs/External';
import Overview from './Tabs/Overview';
const getWidget = (query: PromQLWidgets['query']): PromQLWidgets => {
return {
description: '',
id: '',
isStacked: false,
nullZeroValues: '',
opacity: '0',
panelTypes: 'TIME_SERIES',
query,
queryData: {
data: { queryData: [] },
error: false,
errorMessage: '',
loading: false,
},
timePreferance: 'GLOBAL_TIME',
title: '',
stepSize: 60,
};
};
const getWidgetQueryBuilder = (query: Widgets['query']): Widgets => {
return {
description: '',
id: v4(),
isStacked: false,
nullZeroValues: '',
opacity: '0',
panelTypes: 'TIME_SERIES',
query,
queryData: {
data: { queryData: [] },
error: false,
errorMessage: '',
loading: false,
},
timePreferance: 'GLOBAL_TIME',
title: '',
stepSize: 60,
};
};
function OverViewTab(): JSX.Element {
return <Overview getWidget={getWidget} />;
return <Overview getWidgetQueryBuilder={getWidgetQueryBuilder} />;
}
function DbCallTab(): JSX.Element {
@ -128,4 +85,4 @@ function ServiceMetrics(): JSX.Element {
);
}
export default React.memo(ServiceMetrics);
export default memo(ServiceMetrics);

View File

@ -2,12 +2,15 @@ import { blue } from '@ant-design/colors';
import { SearchOutlined } from '@ant-design/icons';
import { Button, Card, Input, Space, Table } from 'antd';
import type { ColumnsType, ColumnType } from 'antd/es/table';
import type { FilterConfirmProps } from 'antd/es/table/interface';
import type {
FilterConfirmProps,
FilterDropdownProps,
} from 'antd/es/table/interface';
import localStorageGet from 'api/browser/localstorage/get';
import localStorageSet from 'api/browser/localstorage/set';
import { SKIP_ONBOARDING } from 'constants/onboarding';
import ROUTES from 'constants/routes';
import React, { useCallback, useState } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { Link, useLocation } from 'react-router-dom';
import { AppState } from 'store/reducers';
@ -35,8 +38,8 @@ function Metrics(): JSX.Element {
confirm();
};
const FilterIcon = useCallback(
({ filtered }) => (
const FilterIcon: ColumnType<DataProps>['filterIcon'] = useCallback(
(filtered: boolean) => (
<SearchOutlined
style={{
color: filtered ? blue[6] : undefined,
@ -47,7 +50,7 @@ function Metrics(): JSX.Element {
);
const filterDropdown = useCallback(
({ setSelectedKeys, selectedKeys, confirm }) => (
({ setSelectedKeys, selectedKeys, confirm }: FilterDropdownProps) => (
<Card size="small">
<Space align="start" direction="vertical">
<Input
@ -73,6 +76,60 @@ function Metrics(): JSX.Element {
[],
);
type DataIndex = keyof ServicesList;
const getColumnSearchProps = useCallback(
(dataIndex: DataIndex): ColumnType<DataProps> => ({
filterDropdown,
filterIcon: FilterIcon,
onFilter: (value: string | number | boolean, record: DataProps): boolean =>
record[dataIndex]
.toString()
.toLowerCase()
.includes(value.toString().toLowerCase()),
render: (text: string): JSX.Element => (
<Link to={`${ROUTES.APPLICATION}/${text}${search}`}>
<Name>{text}</Name>
</Link>
),
}),
[filterDropdown, FilterIcon, search],
);
const columns: ColumnsType<DataProps> = useMemo(
() => [
{
title: 'Application',
dataIndex: 'serviceName',
key: 'serviceName',
...getColumnSearchProps('serviceName'),
},
{
title: 'P99 latency (in ms)',
dataIndex: 'p99',
key: 'p99',
defaultSortOrder: 'descend',
sorter: (a: DataProps, b: DataProps): number => a.p99 - b.p99,
render: (value: number): string => (value / 1000000).toFixed(2),
},
{
title: 'Error Rate (% of total)',
dataIndex: 'errorRate',
key: 'errorRate',
sorter: (a: DataProps, b: DataProps): number => a.errorRate - b.errorRate,
render: (value: number): string => value.toFixed(2),
},
{
title: 'Operations Per Second',
dataIndex: 'callRate',
key: 'callRate',
sorter: (a: DataProps, b: DataProps): number => a.callRate - b.callRate,
render: (value: number): string => value.toFixed(2),
},
],
[getColumnSearchProps],
);
if (
services.length === 0 &&
loading === false &&
@ -82,56 +139,6 @@ function Metrics(): JSX.Element {
return <SkipBoardModal onContinueClick={onContinueClick} />;
}
type DataIndex = keyof ServicesList;
const getColumnSearchProps = (
dataIndex: DataIndex,
): ColumnType<DataProps> => ({
filterDropdown,
filterIcon: FilterIcon,
onFilter: (value: string | number | boolean, record: DataProps): boolean =>
record[dataIndex]
.toString()
.toLowerCase()
.includes(value.toString().toLowerCase()),
render: (text: string): JSX.Element => (
<Link to={`${ROUTES.APPLICATION}/${text}${search}`}>
<Name>{text}</Name>
</Link>
),
});
const columns: ColumnsType<DataProps> = [
{
title: 'Application',
dataIndex: 'serviceName',
key: 'serviceName',
...getColumnSearchProps('serviceName'),
},
{
title: 'P99 latency (in ms)',
dataIndex: 'p99',
key: 'p99',
defaultSortOrder: 'descend',
sorter: (a: DataProps, b: DataProps): number => a.p99 - b.p99,
render: (value: number): string => (value / 1000000).toFixed(2),
},
{
title: 'Error Rate (% of total)',
dataIndex: 'errorRate',
key: 'errorRate',
sorter: (a: DataProps, b: DataProps): number => a.errorRate - b.errorRate,
render: (value: number): string => value.toFixed(2),
},
{
title: 'Operations Per Second',
dataIndex: 'callRate',
key: 'callRate',
sorter: (a: DataProps, b: DataProps): number => a.callRate - b.callRate,
render: (value: number): string => value.toFixed(2),
},
];
return (
<Container>
<Table

View File

@ -25,6 +25,7 @@ import {
} from 'types/api/dashboard/getAll';
import { v4 } from 'uuid';
import { variablePropsToPayloadVariables } from '../../../utils';
import { TVariableViewMode } from '../types';
import { LabelContainer, VariableItemRow } from './styles';
@ -32,6 +33,7 @@ const { Option } = Select;
interface VariableItemProps {
variableData: IDashboardVariable;
existingVariables: Record<string, IDashboardVariable>;
onCancel: () => void;
onSave: (name: string, arg0: IDashboardVariable, arg1: string) => void;
validateName: (arg0: string) => boolean;
@ -39,6 +41,7 @@ interface VariableItemProps {
}
function VariableItem({
variableData,
existingVariables,
onCancel,
onSave,
validateName,
@ -134,10 +137,16 @@ function VariableItem({
try {
const variableQueryResponse = await query({
query: variableQueryValue,
variables: variablePropsToPayloadVariables(existingVariables),
});
setPreviewLoading(false);
if (variableQueryResponse.error) {
setErrorPreview(variableQueryResponse.error);
let message = variableQueryResponse.error;
if (variableQueryResponse.error.includes('Syntax error:')) {
message =
'Please make sure query is valid and dependent variables are selected';
}
setErrorPreview(message);
return;
}
if (variableQueryResponse.payload?.variableValues)

View File

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

View File

@ -8,7 +8,9 @@ import { map } from 'lodash-es';
import React, { useCallback, useEffect, useState } from 'react';
import { IDashboardVariable } from 'types/api/dashboard/getAll';
import { VariableContainer, VariableName } from './styles';
import { variablePropsToPayloadVariables } from '../utils';
import { SelectItemStyle, VariableContainer, VariableName } from './styles';
import { areArraysEqual } from './util';
const { Option } = Select;
@ -16,18 +18,35 @@ const ALL_SELECT_VALUE = '__ALL__';
interface VariableItemProps {
variableData: IDashboardVariable;
onValueUpdate: (name: string | undefined, arg1: string | string[]) => void;
existingVariables: Record<string, IDashboardVariable>;
onValueUpdate: (
name: string | undefined,
arg1:
| string
| number
| boolean
| (string | number | boolean)[]
| null
| undefined,
) => void;
onAllSelectedUpdate: (name: string | undefined, arg1: boolean) => void;
lastUpdatedVar: string;
}
function VariableItem({
variableData,
existingVariables,
onValueUpdate,
onAllSelectedUpdate,
lastUpdatedVar,
}: VariableItemProps): JSX.Element {
const [optionsData, setOptionsData] = useState([]);
const [optionsData, setOptionsData] = useState<(string | number | boolean)[]>(
[],
);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [errorMessage, setErrorMessage] = useState<null | string>(null);
/* eslint-disable sonarjs/cognitive-complexity */
const getOptions = useCallback(async (): Promise<void> => {
if (variableData.type === 'QUERY') {
try {
@ -36,17 +55,58 @@ function VariableItem({
const response = await query({
query: variableData.queryValue || '',
variables: variablePropsToPayloadVariables(existingVariables),
});
setIsLoading(false);
if (response.error) {
setErrorMessage(response.error);
let message = response.error;
if (response.error.includes('Syntax error:')) {
message =
'Please make sure query is valid and dependent variables are selected';
}
setErrorMessage(message);
return;
}
if (response.payload?.variableValues)
setOptionsData(
sortValues(response.payload?.variableValues, variableData.sort) as never,
if (response.payload?.variableValues) {
const newOptionsData = sortValues(
response.payload?.variableValues,
variableData.sort,
);
// Since there is a chance of a variable being dependent on other
// variables, we need to check if the optionsData has changed
// If it has changed, we need to update the dependent variable
// So we compare the new optionsData with the old optionsData
const oldOptionsData = sortValues(optionsData, variableData.sort) as never;
if (!areArraysEqual(newOptionsData, oldOptionsData)) {
/* eslint-disable no-useless-escape */
const re = new RegExp(`\\{\\{\\s*?\\.${lastUpdatedVar}\\s*?\\}\\}`); // regex for `{{.var}}`
// If the variable is dependent on the last updated variable
// and contains the last updated variable in its query (of the form `{{.var}}`)
// then we need to update the value of the variable
const queryValue = variableData.queryValue || '';
const dependVarReMatch = queryValue.match(re);
if (
variableData.type === 'QUERY' &&
dependVarReMatch !== null &&
dependVarReMatch.length > 0
) {
let value = variableData.selectedValue;
let allSelected = false;
// The default value for multi-select is ALL and first value for
// single select
if (variableData.multiSelect) {
value = newOptionsData;
allSelected = true;
} else {
[value] = newOptionsData;
}
onValueUpdate(variableData.name, value);
onAllSelectedUpdate(variableData.name, allSelected);
}
setOptionsData(newOptionsData);
}
}
} catch (e) {
console.error(e);
}
@ -59,10 +119,12 @@ function VariableItem({
);
}
}, [
variableData.customValue,
variableData.queryValue,
variableData.sort,
variableData.type,
variableData,
existingVariables,
onValueUpdate,
onAllSelectedUpdate,
optionsData,
lastUpdatedVar,
]);
useEffect(() => {
@ -72,7 +134,8 @@ function VariableItem({
const handleChange = (value: string | string[]): void => {
if (
value === ALL_SELECT_VALUE ||
(Array.isArray(value) && value.includes(ALL_SELECT_VALUE))
(Array.isArray(value) && value.includes(ALL_SELECT_VALUE)) ||
(Array.isArray(value) && value.length === 0)
) {
onValueUpdate(variableData.name, optionsData);
onAllSelectedUpdate(variableData.name, true);
@ -81,6 +144,15 @@ function VariableItem({
onAllSelectedUpdate(variableData.name, false);
}
};
const selectValue = variableData.allSelected
? 'ALL'
: variableData.selectedValue?.toString() || '';
const mode =
variableData.multiSelect && !variableData.allSelected
? 'multiple'
: undefined;
const enableSelectAll = variableData.multiSelect && variableData.showALLOption;
return (
<VariableContainer>
<VariableName>${variableData.name}</VariableName>
@ -93,35 +165,29 @@ function VariableItem({
handleChange(e.target.value || '');
}}
style={{
width: 50 + ((variableData.selectedValue?.length || 0) * 7 || 50),
width:
50 + ((variableData.selectedValue?.toString()?.length || 0) * 7 || 50),
}}
/>
) : (
<Select
value={variableData.allSelected ? 'ALL' : variableData.selectedValue}
onChange={handleChange}
bordered={false}
placeholder="Select value"
mode={
(variableData.multiSelect && !variableData.allSelected
? 'multiple'
: null) as never
}
dropdownMatchSelectWidth={false}
style={{
minWidth: 120,
fontSize: '0.8rem',
}}
loading={isLoading}
showArrow
>
{variableData.multiSelect && variableData.showALLOption && (
<Option value={ALL_SELECT_VALUE}>ALL</Option>
)}
{map(optionsData, (option) => {
return <Option value={option}>{(option as string).toString()}</Option>;
})}
</Select>
!errorMessage && (
<Select
value={selectValue}
onChange={handleChange}
bordered={false}
placeholder="Select value"
mode={mode}
dropdownMatchSelectWidth={false}
style={SelectItemStyle}
loading={isLoading}
showArrow
>
{enableSelectAll && <Option value={ALL_SELECT_VALUE}>ALL</Option>}
{map(optionsData, (option) => (
<Option value={option}>{option.toString()}</Option>
))}
</Select>
)
)}
{errorMessage && (
<span style={{ margin: '0 0.5rem' }}>

View File

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

View File

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

View File

@ -0,0 +1,16 @@
export function areArraysEqual(
a: (string | number | boolean)[],
b: (string | number | boolean)[],
): boolean {
if (a.length !== b.length) {
return false;
}
for (let i = 0; i < a.length; i += 1) {
if (a[i] !== b[i]) {
return false;
}
}
return true;
}

View File

@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next';
import { useCopyToClipboard } from 'react-use';
import { DashboardData } from 'types/api/dashboard/getAll';
import { downloadObjectAsJson } from './util';
import { cleardQueryData, downloadObjectAsJson } from './util';
function ShareModal({
isJSONModalVisible,
@ -51,6 +51,7 @@ function ShareModal({
}
}, [state.error, state.value, t]);
const selectedDataCleaned = cleardQueryData(selectedData);
const GetFooterComponent = useMemo(() => {
if (!isViewJSON) {
return (
@ -66,7 +67,7 @@ function ShareModal({
<Button
type="primary"
onClick={(): void => {
downloadObjectAsJson(selectedData, selectedData.title);
downloadObjectAsJson(selectedDataCleaned, selectedData.title);
}}
>
{t('download_json')}
@ -79,7 +80,7 @@ function ShareModal({
{t('copy_to_clipboard')}
</Button>
);
}, [isViewJSON, jsonValue, selectedData, setCopy, t]);
}, [isViewJSON, jsonValue, selectedData, selectedDataCleaned, setCopy, t]);
return (
<Modal

View File

@ -1,3 +1,5 @@
import { DashboardData } from 'types/api/dashboard/getAll';
export function downloadObjectAsJson(
exportObj: unknown,
exportName: string,
@ -12,3 +14,18 @@ export function downloadObjectAsJson(
downloadAnchorNode.click();
downloadAnchorNode.remove();
}
export function cleardQueryData(param: DashboardData): DashboardData {
return {
...param,
widgets: param.widgets?.map((widget) => ({
...widget,
queryData: {
...widget.queryData,
data: {
queryData: [],
},
},
})),
};
}

View File

@ -0,0 +1,14 @@
import { IDashboardVariable } from 'types/api/dashboard/getAll';
import { PayloadVariables } from 'types/api/dashboard/variables/query';
export function variablePropsToPayloadVariables(
variables: Record<string, IDashboardVariable>,
): PayloadVariables {
const payloadVariables: PayloadVariables = {};
Object.entries(variables).forEach(([key, value]) => {
payloadVariables[key] = value?.selectedValue;
});
return payloadVariables;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -63,15 +63,17 @@ function PendingInvitesContainer(): JSX.Element {
const { hash } = useLocation();
const getParsedInviteData = useCallback((payload: PayloadProps = []) => {
return payload?.map((data) => ({
key: data.createdAt,
name: data.name,
email: data.email,
accessLevel: data.role,
inviteLink: `${window.location.origin}${ROUTES.SIGN_UP}?token=${data.token}`,
}));
}, []);
const getParsedInviteData = useCallback(
(payload: PayloadProps = []) =>
payload?.map((data) => ({
key: data.createdAt,
name: data.name,
email: data.email,
accessLevel: data.role,
inviteLink: `${window.location.origin}${ROUTES.SIGN_UP}?token=${data.token}`,
})),
[],
);
useEffect(() => {
if (hash === INVITE_MEMBERS_HASH) {

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