@ -40,7 +40,7 @@ services:
|
|||||||
condition: on-failure
|
condition: on-failure
|
||||||
|
|
||||||
query-service:
|
query-service:
|
||||||
image: signoz/query-service:0.11.0
|
image: signoz/query-service:0.11.1
|
||||||
command: ["-config=/root/config/prometheus.yml"]
|
command: ["-config=/root/config/prometheus.yml"]
|
||||||
# ports:
|
# ports:
|
||||||
# - "6060:6060" # pprof port
|
# - "6060:6060" # pprof port
|
||||||
@ -70,7 +70,7 @@ services:
|
|||||||
- clickhouse
|
- clickhouse
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
image: signoz/frontend:0.11.0
|
image: signoz/frontend:0.11.1
|
||||||
deploy:
|
deploy:
|
||||||
restart_policy:
|
restart_policy:
|
||||||
condition: on-failure
|
condition: on-failure
|
||||||
@ -83,7 +83,7 @@ services:
|
|||||||
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
otel-collector:
|
otel-collector:
|
||||||
image: signoz-otel-collector:0.55.0
|
image: signoz-otel-collector:0.55.1
|
||||||
command: ["--config=/etc/otel-collector-config.yaml"]
|
command: ["--config=/etc/otel-collector-config.yaml"]
|
||||||
user: root # required for reading docker container logs
|
user: root # required for reading docker container logs
|
||||||
volumes:
|
volumes:
|
||||||
@ -111,7 +111,7 @@ services:
|
|||||||
- clickhouse
|
- clickhouse
|
||||||
|
|
||||||
otel-collector-metrics:
|
otel-collector-metrics:
|
||||||
image: signoz-otel-collector:0.55.0
|
image: signoz-otel-collector:0.55.1
|
||||||
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
|
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
|
||||||
volumes:
|
volumes:
|
||||||
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
|
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
|
||||||
|
@ -19,8 +19,7 @@ rule_files:
|
|||||||
|
|
||||||
# A scrape configuration containing exactly one endpoint to scrape:
|
# A scrape configuration containing exactly one endpoint to scrape:
|
||||||
# Here it's Prometheus itself.
|
# Here it's Prometheus itself.
|
||||||
scrape_configs:
|
scrape_configs: []
|
||||||
|
|
||||||
|
|
||||||
remote_read:
|
remote_read:
|
||||||
- url: tcp://clickhouse:9000/?database=signoz_metrics
|
- url: tcp://clickhouse:9000/?database=signoz_metrics
|
||||||
|
@ -3,13 +3,17 @@ server {
|
|||||||
server_name _;
|
server_name _;
|
||||||
|
|
||||||
gzip on;
|
gzip on;
|
||||||
gzip_static on;
|
gzip_static on;
|
||||||
gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;
|
gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;
|
||||||
gzip_proxied any;
|
gzip_proxied any;
|
||||||
gzip_vary on;
|
gzip_vary on;
|
||||||
gzip_comp_level 6;
|
gzip_comp_level 6;
|
||||||
gzip_buffers 16 8k;
|
gzip_buffers 16 8k;
|
||||||
gzip_http_version 1.1;
|
gzip_http_version 1.1;
|
||||||
|
|
||||||
|
# to handle uri issue 414 from nginx
|
||||||
|
client_max_body_size 24M;
|
||||||
|
large_client_header_buffers 8 16k;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
if ( $uri = '/index.html' ) {
|
if ( $uri = '/index.html' ) {
|
||||||
|
@ -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`
|
# 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:
|
otel-collector:
|
||||||
container_name: otel-collector
|
container_name: otel-collector
|
||||||
image: signoz/signoz-otel-collector:0.55.0
|
image: signoz/signoz-otel-collector:0.55.1
|
||||||
command: ["--config=/etc/otel-collector-config.yaml"]
|
command: ["--config=/etc/otel-collector-config.yaml"]
|
||||||
# user: root # required for reading docker container logs
|
# user: root # required for reading docker container logs
|
||||||
volumes:
|
volumes:
|
||||||
@ -67,7 +67,7 @@ services:
|
|||||||
|
|
||||||
otel-collector-metrics:
|
otel-collector-metrics:
|
||||||
container_name: otel-collector-metrics
|
container_name: otel-collector-metrics
|
||||||
image: signoz/signoz-otel-collector:0.55.0
|
image: signoz/signoz-otel-collector:0.55.1
|
||||||
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
|
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
|
||||||
volumes:
|
volumes:
|
||||||
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
|
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
|
||||||
|
@ -2,7 +2,7 @@ version: "2.4"
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
query-service:
|
query-service:
|
||||||
image: signoz/query-service:0.11.0
|
image: signoz/query-service:0.11.1
|
||||||
container_name: query-service
|
container_name: query-service
|
||||||
command: ["-config=/root/config/prometheus.yml"]
|
command: ["-config=/root/config/prometheus.yml"]
|
||||||
# ports:
|
# ports:
|
||||||
@ -31,7 +31,7 @@ services:
|
|||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
image: signoz/frontend:0.11.0
|
image: signoz/frontend:0.11.1
|
||||||
container_name: frontend
|
container_name: frontend
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
depends_on:
|
depends_on:
|
||||||
|
@ -39,7 +39,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`
|
# 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:
|
query-service:
|
||||||
image: signoz/query-service:0.11.0
|
image: signoz/query-service:0.11.1
|
||||||
container_name: query-service
|
container_name: query-service
|
||||||
command: ["-config=/root/config/prometheus.yml"]
|
command: ["-config=/root/config/prometheus.yml"]
|
||||||
# ports:
|
# ports:
|
||||||
@ -68,7 +68,7 @@ services:
|
|||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
image: signoz/frontend:0.11.0
|
image: signoz/frontend:0.11.1
|
||||||
container_name: frontend
|
container_name: frontend
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
depends_on:
|
depends_on:
|
||||||
@ -80,7 +80,7 @@ services:
|
|||||||
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
otel-collector:
|
otel-collector:
|
||||||
image: signoz/signoz-otel-collector:0.55.0
|
image: signoz/signoz-otel-collector:0.55.1
|
||||||
command: ["--config=/etc/otel-collector-config.yaml"]
|
command: ["--config=/etc/otel-collector-config.yaml"]
|
||||||
user: root # required for reading docker container logs
|
user: root # required for reading docker container logs
|
||||||
volumes:
|
volumes:
|
||||||
@ -106,7 +106,7 @@ services:
|
|||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|
||||||
otel-collector-metrics:
|
otel-collector-metrics:
|
||||||
image: signoz/signoz-otel-collector:0.55.0
|
image: signoz/signoz-otel-collector:0.55.1
|
||||||
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
|
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
|
||||||
volumes:
|
volumes:
|
||||||
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
|
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
|
||||||
|
@ -19,8 +19,7 @@ rule_files:
|
|||||||
|
|
||||||
# A scrape configuration containing exactly one endpoint to scrape:
|
# A scrape configuration containing exactly one endpoint to scrape:
|
||||||
# Here it's Prometheus itself.
|
# Here it's Prometheus itself.
|
||||||
scrape_configs:
|
scrape_configs: []
|
||||||
|
|
||||||
|
|
||||||
remote_read:
|
remote_read:
|
||||||
- url: tcp://clickhouse:9000/?database=signoz_metrics
|
- url: tcp://clickhouse:9000/?database=signoz_metrics
|
||||||
|
@ -3,7 +3,7 @@ server {
|
|||||||
server_name _;
|
server_name _;
|
||||||
|
|
||||||
gzip on;
|
gzip on;
|
||||||
gzip_static on;
|
gzip_static on;
|
||||||
gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;
|
gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;
|
||||||
gzip_proxied any;
|
gzip_proxied any;
|
||||||
gzip_vary on;
|
gzip_vary on;
|
||||||
@ -13,7 +13,6 @@ server {
|
|||||||
|
|
||||||
# to handle uri issue 414 from nginx
|
# to handle uri issue 414 from nginx
|
||||||
client_max_body_size 24M;
|
client_max_body_size 24M;
|
||||||
|
|
||||||
large_client_header_buffers 8 16k;
|
large_client_header_buffers 8 16k;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
|
@ -13,7 +13,7 @@ WORKDIR /frontend
|
|||||||
COPY package.json ./
|
COPY package.json ./
|
||||||
|
|
||||||
# Install the dependencies and make the folder
|
# Install the dependencies and make the folder
|
||||||
RUN CI=1 yarn install
|
RUN CI=1 yarn install
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
|
@ -1,15 +1,19 @@
|
|||||||
server {
|
server {
|
||||||
listen 3301;
|
listen 3301;
|
||||||
server_name _;
|
server_name _;
|
||||||
|
|
||||||
gzip on;
|
gzip on;
|
||||||
gzip_static on;
|
gzip_static on;
|
||||||
gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;
|
gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;
|
||||||
gzip_proxied any;
|
gzip_proxied any;
|
||||||
gzip_vary on;
|
gzip_vary on;
|
||||||
gzip_comp_level 6;
|
gzip_comp_level 6;
|
||||||
gzip_buffers 16 8k;
|
gzip_buffers 16 8k;
|
||||||
gzip_http_version 1.1;
|
gzip_http_version 1.1;
|
||||||
|
|
||||||
|
# to handle uri issue 414 from nginx
|
||||||
|
client_max_body_size 24M;
|
||||||
|
large_client_header_buffers 8 16k;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
root /usr/share/nginx/html;
|
root /usr/share/nginx/html;
|
||||||
|
BIN
frontend/public/Logos/elixir.png
Normal file
After Width: | Height: | Size: 4.4 KiB |
BIN
frontend/public/Logos/go.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
frontend/public/Logos/java.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
frontend/public/Logos/javascript.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
frontend/public/Logos/ms-net-framework.png
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
frontend/public/Logos/php.png
Normal file
After Width: | Height: | Size: 3.4 KiB |
BIN
frontend/public/Logos/python.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
frontend/public/Logos/rails.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
frontend/public/Logos/rust.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
@ -35,11 +35,8 @@ export const SettingsPage = Loadable(
|
|||||||
() => import(/* webpackChunkName: "SettingsPage" */ 'pages/Settings'),
|
() => import(/* webpackChunkName: "SettingsPage" */ 'pages/Settings'),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const InstrumentationPage = Loadable(
|
export const GettingStarted = Loadable(
|
||||||
() =>
|
() => import(/* webpackChunkName: "GettingStarted" */ 'pages/GettingStarted'),
|
||||||
import(
|
|
||||||
/* webpackChunkName: "InstrumentationPage" */ 'pages/AddInstrumentation'
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
export const DashboardPage = Loadable(
|
export const DashboardPage = Loadable(
|
||||||
|
@ -11,7 +11,7 @@ import {
|
|||||||
EditAlertChannelsAlerts,
|
EditAlertChannelsAlerts,
|
||||||
EditRulesPage,
|
EditRulesPage,
|
||||||
ErrorDetails,
|
ErrorDetails,
|
||||||
InstrumentationPage,
|
GettingStarted,
|
||||||
ListAllALertsPage,
|
ListAllALertsPage,
|
||||||
Login,
|
Login,
|
||||||
Logs,
|
Logs,
|
||||||
@ -85,7 +85,7 @@ const routes: AppRoutes[] = [
|
|||||||
{
|
{
|
||||||
path: ROUTES.INSTRUMENTATION,
|
path: ROUTES.INSTRUMENTATION,
|
||||||
exact: true,
|
exact: true,
|
||||||
component: InstrumentationPage,
|
component: GettingStarted,
|
||||||
isPrivate: true,
|
isPrivate: true,
|
||||||
key: 'INSTRUMENTATION',
|
key: 'INSTRUMENTATION',
|
||||||
},
|
},
|
||||||
|
26
frontend/src/api/dashboard/variables/query.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import axios from 'api';
|
||||||
|
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
|
import { PayloadProps, Props } from 'types/api/dashboard/variables/query';
|
||||||
|
|
||||||
|
const query = async (
|
||||||
|
props: Props,
|
||||||
|
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(
|
||||||
|
`/variables/query?query=${encodeURIComponent(props.query)}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
error: null,
|
||||||
|
message: response.data.status,
|
||||||
|
payload: response.data.data,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return ErrorResponseHandler(error as AxiosError);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default query;
|
@ -6,7 +6,7 @@ const ROUTES = {
|
|||||||
TRACE: '/trace',
|
TRACE: '/trace',
|
||||||
TRACE_DETAIL: '/trace/:id',
|
TRACE_DETAIL: '/trace/:id',
|
||||||
SETTINGS: '/settings',
|
SETTINGS: '/settings',
|
||||||
INSTRUMENTATION: '/add-instrumentation',
|
INSTRUMENTATION: '/get-started',
|
||||||
USAGE_EXPLORER: '/usage-explorer',
|
USAGE_EXPLORER: '/usage-explorer',
|
||||||
APPLICATION: '/application',
|
APPLICATION: '/application',
|
||||||
ALL_DASHBOARD: '/dashboard',
|
ALL_DASHBOARD: '/dashboard',
|
||||||
|
@ -3,7 +3,7 @@ import styled from 'styled-components';
|
|||||||
|
|
||||||
export const Layout = styled(LayoutComponent)`
|
export const Layout = styled(LayoutComponent)`
|
||||||
&&& {
|
&&& {
|
||||||
min-height: 91vh;
|
min-height: 92vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import {
|
|||||||
timeItems,
|
timeItems,
|
||||||
timePreferance,
|
timePreferance,
|
||||||
} from 'container/NewWidget/RightContainer/timeItems';
|
} from 'container/NewWidget/RightContainer/timeItems';
|
||||||
|
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
|
||||||
import getChartData from 'lib/getChartData';
|
import getChartData from 'lib/getChartData';
|
||||||
import React, { useCallback, useState } from 'react';
|
import React, { useCallback, useState } from 'react';
|
||||||
import { useQuery } from 'react-query';
|
import { useQuery } from 'react-query';
|
||||||
@ -52,6 +53,7 @@ function FullView({
|
|||||||
graphType: widget.panelTypes,
|
graphType: widget.panelTypes,
|
||||||
query: widget.query,
|
query: widget.query,
|
||||||
globalSelectedInterval: globalSelectedTime,
|
globalSelectedInterval: globalSelectedTime,
|
||||||
|
variables: getDashboardVariables(),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ import { AxiosError } from 'axios';
|
|||||||
import { ChartData } from 'chart.js';
|
import { ChartData } from 'chart.js';
|
||||||
import Spinner from 'components/Spinner';
|
import Spinner from 'components/Spinner';
|
||||||
import GridGraphComponent from 'container/GridGraphComponent';
|
import GridGraphComponent from 'container/GridGraphComponent';
|
||||||
|
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
|
||||||
import getChartData from 'lib/getChartData';
|
import getChartData from 'lib/getChartData';
|
||||||
import isEmpty from 'lodash-es/isEmpty';
|
import isEmpty from 'lodash-es/isEmpty';
|
||||||
import React, { memo, useCallback, useEffect, useState } from 'react';
|
import React, { memo, useCallback, useEffect, useState } from 'react';
|
||||||
@ -104,11 +105,18 @@ function GridCardGraph({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async (): Promise<void> => {
|
(async (): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
|
setState((state) => ({
|
||||||
|
...state,
|
||||||
|
error: false,
|
||||||
|
errorMessage: '',
|
||||||
|
loading: true,
|
||||||
|
}));
|
||||||
const response = await GetMetricQueryRange({
|
const response = await GetMetricQueryRange({
|
||||||
selectedTime: widget.timePreferance,
|
selectedTime: widget.timePreferance,
|
||||||
graphType: widget.panelTypes,
|
graphType: widget.panelTypes,
|
||||||
query: widget.query,
|
query: widget.query,
|
||||||
globalSelectedInterval,
|
globalSelectedInterval,
|
||||||
|
variables: getDashboardVariables(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const isError = response.error;
|
const isError = response.error;
|
||||||
@ -144,6 +152,11 @@ function GridCardGraph({
|
|||||||
errorMessage: (error as AxiosError).toString(),
|
errorMessage: (error as AxiosError).toString(),
|
||||||
loading: false,
|
loading: false,
|
||||||
}));
|
}));
|
||||||
|
} finally {
|
||||||
|
setState((state) => ({
|
||||||
|
...state,
|
||||||
|
loading: false,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}, [widget, maxTime, minTime, globalSelectedInterval]);
|
}, [widget, maxTime, minTime, globalSelectedInterval]);
|
||||||
|
@ -121,6 +121,7 @@ function GridGraph(props: Props): JSX.Element {
|
|||||||
name: data.name,
|
name: data.name,
|
||||||
tags: data.tags,
|
tags: data.tags,
|
||||||
widgets: data.widgets,
|
widgets: data.widgets,
|
||||||
|
variables: data.variables,
|
||||||
layout,
|
layout,
|
||||||
},
|
},
|
||||||
uuid: selectedDashboard.uuid,
|
uuid: selectedDashboard.uuid,
|
||||||
@ -157,6 +158,7 @@ function GridGraph(props: Props): JSX.Element {
|
|||||||
data.name,
|
data.name,
|
||||||
data.tags,
|
data.tags,
|
||||||
data.title,
|
data.title,
|
||||||
|
data.variables,
|
||||||
data.widgets,
|
data.widgets,
|
||||||
dispatch,
|
dispatch,
|
||||||
saveLayoutPermission,
|
saveLayoutPermission,
|
||||||
|
@ -27,6 +27,7 @@ export const UpdateDashboard = async ({
|
|||||||
description: data.description,
|
description: data.description,
|
||||||
name: data.name,
|
name: data.name,
|
||||||
tags: data.tags,
|
tags: data.tags,
|
||||||
|
variables: data.variables,
|
||||||
widgets: [
|
widgets: [
|
||||||
...(data.widgets || []),
|
...(data.widgets || []),
|
||||||
{
|
{
|
||||||
|
@ -12,6 +12,7 @@ describe('executeSearchQueries', () => {
|
|||||||
updated_at: '',
|
updated_at: '',
|
||||||
data: {
|
data: {
|
||||||
title: 'first dashboard',
|
title: 'first dashboard',
|
||||||
|
variables: {},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const secondDashboard: Dashboard = {
|
const secondDashboard: Dashboard = {
|
||||||
@ -21,6 +22,7 @@ describe('executeSearchQueries', () => {
|
|||||||
updated_at: '',
|
updated_at: '',
|
||||||
data: {
|
data: {
|
||||||
title: 'second dashboard',
|
title: 'second dashboard',
|
||||||
|
variables: {},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const thirdDashboard: Dashboard = {
|
const thirdDashboard: Dashboard = {
|
||||||
@ -30,6 +32,7 @@ describe('executeSearchQueries', () => {
|
|||||||
updated_at: '',
|
updated_at: '',
|
||||||
data: {
|
data: {
|
||||||
title: 'third dashboard (with special characters +?\\)',
|
title: 'third dashboard (with special characters +?\\)',
|
||||||
|
variables: {},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const dashboards = [firstDashboard, secondDashboard, thirdDashboard];
|
const dashboards = [firstDashboard, secondDashboard, thirdDashboard];
|
||||||
|
@ -0,0 +1,112 @@
|
|||||||
|
import { SaveOutlined } from '@ant-design/icons';
|
||||||
|
import { Col, Divider, Input, Space, Typography } from 'antd';
|
||||||
|
import AddTags from 'container/NewDashboard/DashboardSettings/General/AddTags';
|
||||||
|
import React, { useCallback, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { connect, useSelector } from 'react-redux';
|
||||||
|
import { bindActionCreators, Dispatch } from 'redux';
|
||||||
|
import { ThunkDispatch } from 'redux-thunk';
|
||||||
|
import {
|
||||||
|
UpdateDashboardTitleDescriptionTags,
|
||||||
|
UpdateDashboardTitleDescriptionTagsProps,
|
||||||
|
} from 'store/actions';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
|
import AppActions from 'types/actions';
|
||||||
|
import DashboardReducer from 'types/reducer/dashboards';
|
||||||
|
|
||||||
|
import { Button } from './styles';
|
||||||
|
|
||||||
|
function GeneralDashboardSettings({
|
||||||
|
updateDashboardTitleDescriptionTags,
|
||||||
|
}: DescriptionOfDashboardProps): JSX.Element {
|
||||||
|
const { dashboards } = useSelector<AppState, DashboardReducer>(
|
||||||
|
(state) => state.dashboards,
|
||||||
|
);
|
||||||
|
|
||||||
|
const [selectedDashboard] = dashboards;
|
||||||
|
const selectedData = selectedDashboard.data;
|
||||||
|
const { title } = selectedData;
|
||||||
|
const { tags } = selectedData;
|
||||||
|
const { description } = selectedData;
|
||||||
|
|
||||||
|
const [updatedTitle, setUpdatedTitle] = useState<string>(title);
|
||||||
|
const [updatedTags, setUpdatedTags] = useState<string[]>(tags || []);
|
||||||
|
const [updatedDescription, setUpdatedDescription] = useState(
|
||||||
|
description || '',
|
||||||
|
);
|
||||||
|
|
||||||
|
const { t } = useTranslation('common');
|
||||||
|
|
||||||
|
const onSaveHandler = useCallback(() => {
|
||||||
|
const dashboard = selectedDashboard;
|
||||||
|
// @TODO need to update this function to take title,description,tags only
|
||||||
|
updateDashboardTitleDescriptionTags({
|
||||||
|
dashboard: {
|
||||||
|
...dashboard,
|
||||||
|
data: {
|
||||||
|
...dashboard.data,
|
||||||
|
description: updatedDescription,
|
||||||
|
tags: updatedTags,
|
||||||
|
title: updatedTitle,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, [
|
||||||
|
updatedTitle,
|
||||||
|
updatedTags,
|
||||||
|
updatedDescription,
|
||||||
|
selectedDashboard,
|
||||||
|
updateDashboardTitleDescriptionTags,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Col>
|
||||||
|
<Space direction="vertical" style={{ width: '100%' }}>
|
||||||
|
<div>
|
||||||
|
<Typography style={{ marginBottom: '0.5rem' }}>Name</Typography>
|
||||||
|
<Input
|
||||||
|
value={updatedTitle}
|
||||||
|
onChange={(e): void => setUpdatedTitle(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Typography style={{ marginBottom: '0.5rem' }}>Description</Typography>
|
||||||
|
<Input.TextArea
|
||||||
|
value={updatedDescription}
|
||||||
|
onChange={(e): void => setUpdatedDescription(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Typography style={{ marginBottom: '0.5rem' }}>Tags</Typography>
|
||||||
|
<AddTags tags={updatedTags} setTags={setUpdatedTags} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Divider />
|
||||||
|
<Button icon={<SaveOutlined />} onClick={onSaveHandler} type="primary">
|
||||||
|
{t('save')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Space>
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DispatchProps {
|
||||||
|
updateDashboardTitleDescriptionTags: (
|
||||||
|
props: UpdateDashboardTitleDescriptionTagsProps,
|
||||||
|
) => (dispatch: Dispatch<AppActions>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = (
|
||||||
|
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
|
||||||
|
): DispatchProps => ({
|
||||||
|
updateDashboardTitleDescriptionTags: bindActionCreators(
|
||||||
|
UpdateDashboardTitleDescriptionTags,
|
||||||
|
dispatch,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
type DescriptionOfDashboardProps = DispatchProps;
|
||||||
|
|
||||||
|
export default connect(null, mapDispatchToProps)(GeneralDashboardSettings);
|
@ -0,0 +1,20 @@
|
|||||||
|
import { Button as ButtonComponent, Drawer } from 'antd';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
export const Container = styled.div`
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const Button = styled(ButtonComponent)`
|
||||||
|
&&& {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const DrawerContainer = styled(Drawer)`
|
||||||
|
.ant-drawer-header {
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
`;
|
@ -0,0 +1,354 @@
|
|||||||
|
/* eslint-disable sonarjs/cognitive-complexity */
|
||||||
|
import { orange } from '@ant-design/colors';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Col,
|
||||||
|
Divider,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
Switch,
|
||||||
|
Tag,
|
||||||
|
Typography,
|
||||||
|
} from 'antd';
|
||||||
|
import query from 'api/dashboard/variables/query';
|
||||||
|
import Editor from 'components/Editor';
|
||||||
|
import { commaValuesParser } from 'lib/dashbaordVariables/customCommaValuesParser';
|
||||||
|
import sortValues from 'lib/dashbaordVariables/sortVariableValues';
|
||||||
|
import { map } from 'lodash-es';
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import {
|
||||||
|
IDashboardVariable,
|
||||||
|
TSortVariableValuesType,
|
||||||
|
TVariableQueryType,
|
||||||
|
VariableQueryTypeArr,
|
||||||
|
VariableSortTypeArr,
|
||||||
|
} from 'types/api/dashboard/getAll';
|
||||||
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
|
import { TVariableViewMode } from '../types';
|
||||||
|
import { LabelContainer, VariableItemRow } from './styles';
|
||||||
|
|
||||||
|
const { Option } = Select;
|
||||||
|
|
||||||
|
interface VariableItemProps {
|
||||||
|
variableData: IDashboardVariable;
|
||||||
|
onCancel: () => void;
|
||||||
|
onSave: (name: string, arg0: IDashboardVariable, arg1: string) => void;
|
||||||
|
validateName: (arg0: string) => boolean;
|
||||||
|
variableViewMode: TVariableViewMode;
|
||||||
|
}
|
||||||
|
function VariableItem({
|
||||||
|
variableData,
|
||||||
|
onCancel,
|
||||||
|
onSave,
|
||||||
|
validateName,
|
||||||
|
variableViewMode,
|
||||||
|
}: VariableItemProps): JSX.Element {
|
||||||
|
const [variableName, setVariableName] = useState<string>(
|
||||||
|
variableData.name || '',
|
||||||
|
);
|
||||||
|
const [variableDescription, setVariableDescription] = useState<string>(
|
||||||
|
variableData.description || '',
|
||||||
|
);
|
||||||
|
const [queryType, setQueryType] = useState<TVariableQueryType>(
|
||||||
|
variableData.type || 'QUERY',
|
||||||
|
);
|
||||||
|
const [variableQueryValue, setVariableQueryValue] = useState<string>(
|
||||||
|
variableData.queryValue || '',
|
||||||
|
);
|
||||||
|
const [variableCustomValue, setVariableCustomValue] = useState<string>(
|
||||||
|
variableData.customValue || '',
|
||||||
|
);
|
||||||
|
const [variableTextboxValue, setVariableTextboxValue] = useState<string>(
|
||||||
|
variableData.textboxValue || '',
|
||||||
|
);
|
||||||
|
const [
|
||||||
|
variableSortType,
|
||||||
|
setVariableSortType,
|
||||||
|
] = useState<TSortVariableValuesType>(
|
||||||
|
variableData.sort || VariableSortTypeArr[0],
|
||||||
|
);
|
||||||
|
const [variableMultiSelect, setVariableMultiSelect] = useState<boolean>(
|
||||||
|
variableData.multiSelect || false,
|
||||||
|
);
|
||||||
|
const [variableShowALLOption, setVariableShowALLOption] = useState<boolean>(
|
||||||
|
variableData.showALLOption || false,
|
||||||
|
);
|
||||||
|
const [previewValues, setPreviewValues] = useState<string[]>([]);
|
||||||
|
|
||||||
|
// Internal states
|
||||||
|
const [previewLoading, setPreviewLoading] = useState<boolean>(false);
|
||||||
|
// Error messages
|
||||||
|
const [errorName, setErrorName] = useState<boolean>(false);
|
||||||
|
const [errorPreview, setErrorPreview] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setPreviewValues([]);
|
||||||
|
if (queryType === 'CUSTOM') {
|
||||||
|
setPreviewValues(
|
||||||
|
sortValues(
|
||||||
|
commaValuesParser(variableCustomValue),
|
||||||
|
variableSortType,
|
||||||
|
) as never,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
queryType,
|
||||||
|
variableCustomValue,
|
||||||
|
variableData.customValue,
|
||||||
|
variableData.type,
|
||||||
|
variableSortType,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const handleSave = (): void => {
|
||||||
|
const newVariableData: IDashboardVariable = {
|
||||||
|
name: variableName,
|
||||||
|
description: variableDescription,
|
||||||
|
type: queryType,
|
||||||
|
queryValue: variableQueryValue,
|
||||||
|
customValue: variableCustomValue,
|
||||||
|
textboxValue: variableTextboxValue,
|
||||||
|
multiSelect: variableMultiSelect,
|
||||||
|
showALLOption: variableShowALLOption,
|
||||||
|
sort: variableSortType,
|
||||||
|
...(queryType === 'TEXTBOX' && {
|
||||||
|
selectedValue: (variableData.selectedValue ||
|
||||||
|
variableTextboxValue) as never,
|
||||||
|
}),
|
||||||
|
modificationUUID: v4(),
|
||||||
|
};
|
||||||
|
onSave(
|
||||||
|
variableName,
|
||||||
|
newVariableData,
|
||||||
|
(variableViewMode === 'EDIT' && variableName !== variableData.name
|
||||||
|
? variableData.name
|
||||||
|
: '') as string,
|
||||||
|
);
|
||||||
|
onCancel();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fetches the preview values for the SQL variable query
|
||||||
|
const handleQueryResult = async (): Promise<void> => {
|
||||||
|
setPreviewLoading(true);
|
||||||
|
setErrorPreview(null);
|
||||||
|
try {
|
||||||
|
const variableQueryResponse = await query({
|
||||||
|
query: variableQueryValue,
|
||||||
|
});
|
||||||
|
setPreviewLoading(false);
|
||||||
|
if (variableQueryResponse.error) {
|
||||||
|
setErrorPreview(variableQueryResponse.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (variableQueryResponse.payload?.variableValues)
|
||||||
|
setPreviewValues(
|
||||||
|
sortValues(
|
||||||
|
variableQueryResponse.payload?.variableValues || [],
|
||||||
|
variableSortType,
|
||||||
|
) as never,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Col>
|
||||||
|
{/* <Typography.Title level={3}>Add Variable</Typography.Title> */}
|
||||||
|
<VariableItemRow>
|
||||||
|
<LabelContainer>
|
||||||
|
<Typography>Name</Typography>
|
||||||
|
</LabelContainer>
|
||||||
|
<div>
|
||||||
|
<Input
|
||||||
|
placeholder="Unique name of the variable"
|
||||||
|
style={{ width: 400 }}
|
||||||
|
value={variableName}
|
||||||
|
onChange={(e): void => {
|
||||||
|
setVariableName(e.target.value);
|
||||||
|
setErrorName(
|
||||||
|
!validateName(e.target.value) && e.target.value !== variableData.name,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<Typography.Text type="warning">
|
||||||
|
{errorName ? 'Variable name already exists' : ''}
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</VariableItemRow>
|
||||||
|
<VariableItemRow>
|
||||||
|
<LabelContainer>
|
||||||
|
<Typography>Description</Typography>
|
||||||
|
</LabelContainer>
|
||||||
|
|
||||||
|
<Input.TextArea
|
||||||
|
value={variableDescription}
|
||||||
|
placeholder="Write description of the variable"
|
||||||
|
style={{ width: 400 }}
|
||||||
|
onChange={(e): void => setVariableDescription(e.target.value)}
|
||||||
|
/>
|
||||||
|
</VariableItemRow>
|
||||||
|
<VariableItemRow>
|
||||||
|
<LabelContainer>
|
||||||
|
<Typography>Type</Typography>
|
||||||
|
</LabelContainer>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
defaultActiveFirstOption
|
||||||
|
style={{ width: 400 }}
|
||||||
|
onChange={(e: TVariableQueryType): void => {
|
||||||
|
setQueryType(e);
|
||||||
|
}}
|
||||||
|
value={queryType}
|
||||||
|
>
|
||||||
|
<Option value={VariableQueryTypeArr[0]}>Query</Option>
|
||||||
|
<Option value={VariableQueryTypeArr[1]}>Textbox</Option>
|
||||||
|
<Option value={VariableQueryTypeArr[2]}>Custom</Option>
|
||||||
|
</Select>
|
||||||
|
</VariableItemRow>
|
||||||
|
<Typography.Title
|
||||||
|
level={5}
|
||||||
|
style={{ marginTop: '1rem', marginBottom: '1rem' }}
|
||||||
|
>
|
||||||
|
Options
|
||||||
|
</Typography.Title>
|
||||||
|
{queryType === 'QUERY' && (
|
||||||
|
<VariableItemRow>
|
||||||
|
<LabelContainer>
|
||||||
|
<Typography>Query</Typography>
|
||||||
|
</LabelContainer>
|
||||||
|
|
||||||
|
<div style={{ flex: 1, position: 'relative' }}>
|
||||||
|
<Editor
|
||||||
|
language="sql"
|
||||||
|
value={variableQueryValue}
|
||||||
|
onChange={(e): void => setVariableQueryValue(e)}
|
||||||
|
height="300px"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={handleQueryResult}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: 0,
|
||||||
|
}}
|
||||||
|
loading={previewLoading}
|
||||||
|
>
|
||||||
|
Test Run Query
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</VariableItemRow>
|
||||||
|
)}
|
||||||
|
{queryType === 'CUSTOM' && (
|
||||||
|
<VariableItemRow>
|
||||||
|
<LabelContainer>
|
||||||
|
<Typography>Values separated by comma</Typography>
|
||||||
|
</LabelContainer>
|
||||||
|
<Input.TextArea
|
||||||
|
value={variableCustomValue}
|
||||||
|
placeholder="1, 10, mykey, mykey:myvalue"
|
||||||
|
style={{ width: 400 }}
|
||||||
|
onChange={(e): void => {
|
||||||
|
setVariableCustomValue(e.target.value);
|
||||||
|
setPreviewValues(
|
||||||
|
sortValues(
|
||||||
|
commaValuesParser(e.target.value),
|
||||||
|
variableSortType,
|
||||||
|
) as never,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</VariableItemRow>
|
||||||
|
)}
|
||||||
|
{queryType === 'TEXTBOX' && (
|
||||||
|
<VariableItemRow>
|
||||||
|
<LabelContainer>
|
||||||
|
<Typography>Default Value</Typography>
|
||||||
|
</LabelContainer>
|
||||||
|
<Input
|
||||||
|
value={variableTextboxValue}
|
||||||
|
onChange={(e): void => {
|
||||||
|
setVariableTextboxValue(e.target.value);
|
||||||
|
}}
|
||||||
|
placeholder="Default value if any"
|
||||||
|
style={{ width: 400 }}
|
||||||
|
/>
|
||||||
|
</VariableItemRow>
|
||||||
|
)}
|
||||||
|
{(queryType === 'QUERY' || queryType === 'CUSTOM') && (
|
||||||
|
<>
|
||||||
|
<VariableItemRow>
|
||||||
|
<LabelContainer>
|
||||||
|
<Typography>Preview of Values</Typography>
|
||||||
|
</LabelContainer>
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
{errorPreview ? (
|
||||||
|
<Typography style={{ color: orange[5] }}>{errorPreview}</Typography>
|
||||||
|
) : (
|
||||||
|
map(previewValues, (value, idx) => (
|
||||||
|
<Tag key={`${value}${idx}`}>{value.toString()}</Tag>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</VariableItemRow>
|
||||||
|
<VariableItemRow>
|
||||||
|
<LabelContainer>
|
||||||
|
<Typography>Sort</Typography>
|
||||||
|
</LabelContainer>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
defaultActiveFirstOption
|
||||||
|
style={{ width: 400 }}
|
||||||
|
defaultValue={VariableSortTypeArr[0]}
|
||||||
|
value={variableSortType}
|
||||||
|
onChange={(value: TSortVariableValuesType): void =>
|
||||||
|
setVariableSortType(value)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Option value={VariableSortTypeArr[0]}>Disabled</Option>
|
||||||
|
<Option value={VariableSortTypeArr[1]}>Ascending</Option>
|
||||||
|
<Option value={VariableSortTypeArr[2]}>Descending</Option>
|
||||||
|
</Select>
|
||||||
|
</VariableItemRow>
|
||||||
|
<VariableItemRow>
|
||||||
|
<LabelContainer>
|
||||||
|
<Typography>Enable multiple values to be checked</Typography>
|
||||||
|
</LabelContainer>
|
||||||
|
<Switch
|
||||||
|
checked={variableMultiSelect}
|
||||||
|
onChange={(e): void => {
|
||||||
|
setVariableMultiSelect(e);
|
||||||
|
if (!e) {
|
||||||
|
setVariableShowALLOption(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</VariableItemRow>
|
||||||
|
{variableMultiSelect && (
|
||||||
|
<VariableItemRow>
|
||||||
|
<LabelContainer>
|
||||||
|
<Typography>Include an option for ALL values</Typography>
|
||||||
|
</LabelContainer>
|
||||||
|
<Switch
|
||||||
|
checked={variableShowALLOption}
|
||||||
|
onChange={(e): void => setVariableShowALLOption(e)}
|
||||||
|
/>
|
||||||
|
</VariableItemRow>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Divider />
|
||||||
|
<VariableItemRow>
|
||||||
|
<Button type="primary" onClick={handleSave} disabled={errorName}>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
<Button type="dashed" onClick={onCancel}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</VariableItemRow>
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default VariableItem;
|
@ -0,0 +1,11 @@
|
|||||||
|
import { Row } from 'antd';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
export const VariableItemRow = styled(Row)`
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const LabelContainer = styled.div`
|
||||||
|
width: 200px;
|
||||||
|
`;
|
@ -0,0 +1,194 @@
|
|||||||
|
import { blue, red } from '@ant-design/colors';
|
||||||
|
import { PlusOutlined } from '@ant-design/icons';
|
||||||
|
import { Button, Modal, Row, Space, Table, Tag } from 'antd';
|
||||||
|
import React, { useRef, useState } from 'react';
|
||||||
|
import { connect, useSelector } from 'react-redux';
|
||||||
|
import { bindActionCreators, Dispatch } from 'redux';
|
||||||
|
import { ThunkDispatch } from 'redux-thunk';
|
||||||
|
import { UpdateDashboardVariables } from 'store/actions/dashboard/updatedDashboardVariables';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
|
import AppActions from 'types/actions';
|
||||||
|
import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||||
|
import DashboardReducer from 'types/reducer/dashboards';
|
||||||
|
|
||||||
|
import { TVariableViewMode } from './types';
|
||||||
|
import VariableItem from './VariableItem/VariableItem';
|
||||||
|
|
||||||
|
function VariablesSetting({
|
||||||
|
updateDashboardVariables,
|
||||||
|
}: DispatchProps): JSX.Element {
|
||||||
|
const variableToDelete = useRef<string | null>(null);
|
||||||
|
const [deleteVariableModal, setDeleteVariableModal] = useState(false);
|
||||||
|
|
||||||
|
const { dashboards } = useSelector<AppState, DashboardReducer>(
|
||||||
|
(state) => state.dashboards,
|
||||||
|
);
|
||||||
|
|
||||||
|
const [selectedDashboard] = dashboards;
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: { variables = {} },
|
||||||
|
} = selectedDashboard;
|
||||||
|
|
||||||
|
const variablesTableData = Object.keys(variables).map((variableName) => ({
|
||||||
|
key: variableName,
|
||||||
|
name: variableName,
|
||||||
|
...variables[variableName],
|
||||||
|
}));
|
||||||
|
|
||||||
|
const [
|
||||||
|
variableViewMode,
|
||||||
|
setVariableViewMode,
|
||||||
|
] = useState<null | TVariableViewMode>(null);
|
||||||
|
|
||||||
|
const [
|
||||||
|
variableEditData,
|
||||||
|
setVariableEditData,
|
||||||
|
] = useState<null | IDashboardVariable>(null);
|
||||||
|
|
||||||
|
const onDoneVariableViewMode = (): void => {
|
||||||
|
setVariableViewMode(null);
|
||||||
|
setVariableEditData(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onVariableViewModeEnter = (
|
||||||
|
viewType: TVariableViewMode,
|
||||||
|
varData: IDashboardVariable,
|
||||||
|
): void => {
|
||||||
|
setVariableEditData(varData);
|
||||||
|
setVariableViewMode(viewType);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onVariableSaveHandler = (
|
||||||
|
name: string,
|
||||||
|
variableData: IDashboardVariable,
|
||||||
|
oldName: string,
|
||||||
|
): void => {
|
||||||
|
if (!variableData.name) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newVariables = { ...variables };
|
||||||
|
newVariables[name] = variableData;
|
||||||
|
|
||||||
|
if (oldName) {
|
||||||
|
delete newVariables[oldName];
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDashboardVariables(newVariables);
|
||||||
|
onDoneVariableViewMode();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onVariableDeleteHandler = (variableName: string): void => {
|
||||||
|
variableToDelete.current = variableName;
|
||||||
|
setDeleteVariableModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteConfirm = (): void => {
|
||||||
|
const newVariables = { ...variables };
|
||||||
|
if (variableToDelete?.current) delete newVariables[variableToDelete?.current];
|
||||||
|
updateDashboardVariables(newVariables);
|
||||||
|
variableToDelete.current = null;
|
||||||
|
setDeleteVariableModal(false);
|
||||||
|
};
|
||||||
|
const handleDeleteCancel = (): void => {
|
||||||
|
variableToDelete.current = null;
|
||||||
|
setDeleteVariableModal(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateVariableName = (name: string): boolean => {
|
||||||
|
return !variables[name];
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: 'Variable',
|
||||||
|
dataIndex: 'name',
|
||||||
|
key: 'name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Definition',
|
||||||
|
dataIndex: 'description',
|
||||||
|
key: 'description',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Actions',
|
||||||
|
key: 'action',
|
||||||
|
render: (_: IDashboardVariable): JSX.Element => (
|
||||||
|
<Space>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
style={{ padding: 0, cursor: 'pointer', color: blue[5] }}
|
||||||
|
onClick={(): void => onVariableViewModeEnter('EDIT', _)}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
style={{ padding: 0, color: red[6], cursor: 'pointer' }}
|
||||||
|
onClick={(): void => {
|
||||||
|
if (_.name) onVariableDeleteHandler(_.name);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{variableViewMode ? (
|
||||||
|
<VariableItem
|
||||||
|
variableData={{ ...variableEditData } as IDashboardVariable}
|
||||||
|
onSave={onVariableSaveHandler}
|
||||||
|
onCancel={onDoneVariableViewMode}
|
||||||
|
validateName={validateVariableName}
|
||||||
|
variableViewMode={variableViewMode}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Row style={{ flexDirection: 'row-reverse', padding: '0.5rem 0' }}>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={(): void =>
|
||||||
|
onVariableViewModeEnter('ADD', {} as IDashboardVariable)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<PlusOutlined /> New Variables
|
||||||
|
</Button>
|
||||||
|
</Row>
|
||||||
|
<Table columns={columns} dataSource={variablesTableData} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Modal
|
||||||
|
title="Delete variable"
|
||||||
|
centered
|
||||||
|
visible={deleteVariableModal}
|
||||||
|
onOk={handleDeleteConfirm}
|
||||||
|
onCancel={handleDeleteCancel}
|
||||||
|
>
|
||||||
|
Are you sure you want to delete variable{' '}
|
||||||
|
<Tag>{variableToDelete.current}</Tag>?
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DispatchProps {
|
||||||
|
updateDashboardVariables: (
|
||||||
|
props: Record<string, IDashboardVariable>,
|
||||||
|
) => (dispatch: Dispatch<AppActions>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = (
|
||||||
|
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
|
||||||
|
): DispatchProps => ({
|
||||||
|
updateDashboardVariables: bindActionCreators(
|
||||||
|
UpdateDashboardVariables,
|
||||||
|
dispatch,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(null, mapDispatchToProps)(VariablesSetting);
|
@ -0,0 +1 @@
|
|||||||
|
export type TVariableViewMode = 'EDIT' | 'ADD';
|
@ -0,0 +1,22 @@
|
|||||||
|
import { Tabs } from 'antd';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import GeneralDashboardSettings from './General';
|
||||||
|
import VariablesSetting from './Variables';
|
||||||
|
|
||||||
|
const { TabPane } = Tabs;
|
||||||
|
|
||||||
|
function DashboardSettingsContent(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<Tabs>
|
||||||
|
<TabPane tab="General" key="general">
|
||||||
|
<GeneralDashboardSettings />
|
||||||
|
</TabPane>
|
||||||
|
<TabPane tab="Variables" key="variables">
|
||||||
|
<VariablesSetting />
|
||||||
|
</TabPane>
|
||||||
|
</Tabs>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DashboardSettingsContent;
|
@ -0,0 +1,137 @@
|
|||||||
|
import { orange } from '@ant-design/colors';
|
||||||
|
import { WarningOutlined } from '@ant-design/icons';
|
||||||
|
import { Input, Popover, Select, Typography } from 'antd';
|
||||||
|
import query from 'api/dashboard/variables/query';
|
||||||
|
import { commaValuesParser } from 'lib/dashbaordVariables/customCommaValuesParser';
|
||||||
|
import sortValues from 'lib/dashbaordVariables/sortVariableValues';
|
||||||
|
import { map } from 'lodash-es';
|
||||||
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||||
|
|
||||||
|
import { VariableContainer, VariableName } from './styles';
|
||||||
|
|
||||||
|
const { Option } = Select;
|
||||||
|
|
||||||
|
const ALL_SELECT_VALUE = '__ALL__';
|
||||||
|
|
||||||
|
interface VariableItemProps {
|
||||||
|
variableData: IDashboardVariable;
|
||||||
|
onValueUpdate: (name: string | undefined, arg1: string | string[]) => void;
|
||||||
|
onAllSelectedUpdate: (name: string | undefined, arg1: boolean) => void;
|
||||||
|
}
|
||||||
|
function VariableItem({
|
||||||
|
variableData,
|
||||||
|
onValueUpdate,
|
||||||
|
onAllSelectedUpdate,
|
||||||
|
}: VariableItemProps): JSX.Element {
|
||||||
|
const [optionsData, setOptionsData] = useState([]);
|
||||||
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const [errorMessage, setErrorMessage] = useState<null | string>(null);
|
||||||
|
const getOptions = useCallback(async (): Promise<void> => {
|
||||||
|
if (variableData.type === 'QUERY') {
|
||||||
|
try {
|
||||||
|
setErrorMessage(null);
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
const response = await query({
|
||||||
|
query: variableData.queryValue || '',
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsLoading(false);
|
||||||
|
if (response.error) {
|
||||||
|
setErrorMessage(response.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (response.payload?.variableValues)
|
||||||
|
setOptionsData(
|
||||||
|
sortValues(response.payload?.variableValues, variableData.sort) as never,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
} else if (variableData.type === 'CUSTOM') {
|
||||||
|
setOptionsData(
|
||||||
|
sortValues(
|
||||||
|
commaValuesParser(variableData.customValue || ''),
|
||||||
|
variableData.sort,
|
||||||
|
) as never,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
variableData.customValue,
|
||||||
|
variableData.queryValue,
|
||||||
|
variableData.sort,
|
||||||
|
variableData.type,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getOptions();
|
||||||
|
}, [getOptions]);
|
||||||
|
|
||||||
|
const handleChange = (value: string | string[]): void => {
|
||||||
|
if (
|
||||||
|
value === ALL_SELECT_VALUE ||
|
||||||
|
(Array.isArray(value) && value.includes(ALL_SELECT_VALUE))
|
||||||
|
) {
|
||||||
|
onValueUpdate(variableData.name, optionsData);
|
||||||
|
onAllSelectedUpdate(variableData.name, true);
|
||||||
|
} else {
|
||||||
|
onValueUpdate(variableData.name, value);
|
||||||
|
onAllSelectedUpdate(variableData.name, false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<VariableContainer>
|
||||||
|
<VariableName>${variableData.name}</VariableName>
|
||||||
|
{variableData.type === 'TEXTBOX' ? (
|
||||||
|
<Input
|
||||||
|
placeholder="Enter value"
|
||||||
|
bordered={false}
|
||||||
|
value={variableData.selectedValue?.toString()}
|
||||||
|
onChange={(e): void => {
|
||||||
|
handleChange(e.target.value || '');
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
width: 50 + ((variableData.selectedValue?.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 && (
|
||||||
|
<span style={{ margin: '0 0.5rem' }}>
|
||||||
|
<Popover placement="top" content={<Typography>{errorMessage}</Typography>}>
|
||||||
|
<WarningOutlined style={{ color: orange[5] }} />
|
||||||
|
</Popover>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</VariableContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default VariableItem;
|
@ -0,0 +1,72 @@
|
|||||||
|
import { Row } from 'antd';
|
||||||
|
import { map, sortBy } from 'lodash-es';
|
||||||
|
import React from 'react';
|
||||||
|
import { connect, useSelector } from 'react-redux';
|
||||||
|
import { bindActionCreators, Dispatch } from 'redux';
|
||||||
|
import { ThunkDispatch } from 'redux-thunk';
|
||||||
|
import { UpdateDashboardVariables } from 'store/actions/dashboard/updatedDashboardVariables';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
|
import AppActions from 'types/actions';
|
||||||
|
import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||||
|
import DashboardReducer from 'types/reducer/dashboards';
|
||||||
|
|
||||||
|
import VariableItem from './VariableItem';
|
||||||
|
|
||||||
|
function DashboardVariableSelection({
|
||||||
|
updateDashboardVariables,
|
||||||
|
}: DispatchProps): JSX.Element {
|
||||||
|
const { dashboards } = useSelector<AppState, DashboardReducer>(
|
||||||
|
(state) => state.dashboards,
|
||||||
|
);
|
||||||
|
const [selectedDashboard] = dashboards;
|
||||||
|
const {
|
||||||
|
data: { variables = {} },
|
||||||
|
} = selectedDashboard;
|
||||||
|
|
||||||
|
const onValueUpdate = (
|
||||||
|
name: string,
|
||||||
|
value: IDashboardVariable['selectedValue'],
|
||||||
|
): void => {
|
||||||
|
const updatedVariablesData = { ...variables };
|
||||||
|
updatedVariablesData[name].selectedValue = value;
|
||||||
|
updateDashboardVariables(updatedVariablesData);
|
||||||
|
};
|
||||||
|
const onAllSelectedUpdate = (
|
||||||
|
name: string,
|
||||||
|
value: IDashboardVariable['allSelected'],
|
||||||
|
): void => {
|
||||||
|
const updatedVariablesData = { ...variables };
|
||||||
|
updatedVariablesData[name].allSelected = value;
|
||||||
|
updateDashboardVariables(updatedVariablesData);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Row style={{ gap: '1rem' }}>
|
||||||
|
{map(sortBy(Object.keys(variables)), (variableName) => (
|
||||||
|
<VariableItem
|
||||||
|
key={`${variableName}${variables[variableName].modificationUUID}`}
|
||||||
|
variableData={{ name: variableName, ...variables[variableName] }}
|
||||||
|
onValueUpdate={onValueUpdate as never}
|
||||||
|
onAllSelectedUpdate={onAllSelectedUpdate as never}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DispatchProps {
|
||||||
|
updateDashboardVariables: (
|
||||||
|
props: Parameters<typeof UpdateDashboardVariables>[0],
|
||||||
|
) => (dispatch: Dispatch<AppActions>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = (
|
||||||
|
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
|
||||||
|
): DispatchProps => ({
|
||||||
|
updateDashboardVariables: bindActionCreators(
|
||||||
|
UpdateDashboardVariables,
|
||||||
|
dispatch,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(null, mapDispatchToProps)(DashboardVariableSelection);
|
@ -0,0 +1,19 @@
|
|||||||
|
import { grey } from '@ant-design/colors';
|
||||||
|
import { Typography } from 'antd';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
export const VariableContainer = styled.div`
|
||||||
|
border: 1px solid ${grey[1]}66;
|
||||||
|
border-radius: 2px;
|
||||||
|
padding: 0;
|
||||||
|
padding-left: 0.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 0.3rem;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const VariableName = styled(Typography)`
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-style: italic;
|
||||||
|
color: ${grey[0]};
|
||||||
|
`;
|
@ -0,0 +1,37 @@
|
|||||||
|
import { SettingOutlined } from '@ant-design/icons';
|
||||||
|
import { Button } from 'antd';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
import DashboardSettingsContent from '../DashboardSettings';
|
||||||
|
import { DrawerContainer } from './styles';
|
||||||
|
|
||||||
|
function SettingsDrawer(): JSX.Element {
|
||||||
|
const [visible, setVisible] = useState(false); // TODO Make it False
|
||||||
|
|
||||||
|
const showDrawer = (): void => {
|
||||||
|
setVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClose = (): void => {
|
||||||
|
setVisible(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button type="dashed" onClick={showDrawer}>
|
||||||
|
<SettingOutlined /> Configure
|
||||||
|
</Button>
|
||||||
|
<DrawerContainer
|
||||||
|
placement="right"
|
||||||
|
width="70%"
|
||||||
|
onClose={onClose}
|
||||||
|
visible={visible}
|
||||||
|
maskClosable={false}
|
||||||
|
>
|
||||||
|
<DashboardSettingsContent />
|
||||||
|
</DrawerContainer>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SettingsDrawer;
|
@ -1,135 +1,69 @@
|
|||||||
import {
|
import { ShareAltOutlined } from '@ant-design/icons';
|
||||||
EditOutlined,
|
import { Button, Card, Col, Row, Space, Tag, Typography } from 'antd';
|
||||||
SaveOutlined,
|
|
||||||
ShareAltOutlined,
|
|
||||||
} from '@ant-design/icons';
|
|
||||||
import { Card, Col, Row, Space, Tag, Typography } from 'antd';
|
|
||||||
import AddTags from 'container/NewDashboard/DescriptionOfDashboard/AddTags';
|
|
||||||
import NameOfTheDashboard from 'container/NewDashboard/DescriptionOfDashboard/NameOfTheDashboard';
|
|
||||||
import useComponentPermission from 'hooks/useComponentPermission';
|
import useComponentPermission from 'hooks/useComponentPermission';
|
||||||
import React, { useCallback, useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { connect, useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { bindActionCreators, Dispatch } from 'redux';
|
|
||||||
import { ThunkDispatch } from 'redux-thunk';
|
|
||||||
import {
|
|
||||||
ToggleEditMode,
|
|
||||||
UpdateDashboardTitleDescriptionTags,
|
|
||||||
UpdateDashboardTitleDescriptionTagsProps,
|
|
||||||
} from 'store/actions';
|
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
import AppActions from 'types/actions';
|
|
||||||
import AppReducer from 'types/reducer/app';
|
import AppReducer from 'types/reducer/app';
|
||||||
import DashboardReducer from 'types/reducer/dashboards';
|
import DashboardReducer from 'types/reducer/dashboards';
|
||||||
|
|
||||||
import Description from './Description';
|
import DashboardVariableSelection from '../DashboardVariablesSelection';
|
||||||
|
import SettingsDrawer from './SettingsDrawer';
|
||||||
import ShareModal from './ShareModal';
|
import ShareModal from './ShareModal';
|
||||||
import { Button, Container } from './styles';
|
|
||||||
|
|
||||||
function DescriptionOfDashboard({
|
function DescriptionOfDashboard(): JSX.Element {
|
||||||
updateDashboardTitleDescriptionTags,
|
const { dashboards } = useSelector<AppState, DashboardReducer>(
|
||||||
toggleEditMode,
|
|
||||||
}: DescriptionOfDashboardProps): JSX.Element {
|
|
||||||
const { dashboards, isEditMode } = useSelector<AppState, DashboardReducer>(
|
|
||||||
(state) => state.dashboards,
|
(state) => state.dashboards,
|
||||||
);
|
);
|
||||||
|
|
||||||
const [selectedDashboard] = dashboards;
|
const [selectedDashboard] = dashboards;
|
||||||
const selectedData = selectedDashboard.data;
|
const selectedData = selectedDashboard.data;
|
||||||
const { title } = selectedData;
|
const { title, tags, description } = selectedData;
|
||||||
const { tags } = selectedData;
|
|
||||||
const { description } = selectedData;
|
|
||||||
|
|
||||||
const [updatedTitle, setUpdatedTitle] = useState<string>(title);
|
|
||||||
const [updatedTags, setUpdatedTags] = useState<string[]>(tags || []);
|
|
||||||
const [updatedDescription, setUpdatedDescription] = useState(
|
|
||||||
description || '',
|
|
||||||
);
|
|
||||||
const [isJSONModalVisible, isIsJSONModalVisible] = useState<boolean>(false);
|
const [isJSONModalVisible, isIsJSONModalVisible] = useState<boolean>(false);
|
||||||
|
|
||||||
const { t } = useTranslation('common');
|
const { t } = useTranslation('common');
|
||||||
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
|
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||||
const [editDashboard] = useComponentPermission(['edit_dashboard'], role);
|
const [editDashboard] = useComponentPermission(['edit_dashboard'], role);
|
||||||
|
|
||||||
const onClickEditHandler = useCallback(() => {
|
|
||||||
if (isEditMode) {
|
|
||||||
const dashboard = selectedDashboard;
|
|
||||||
// @TODO need to update this function to take title,description,tags only
|
|
||||||
updateDashboardTitleDescriptionTags({
|
|
||||||
dashboard: {
|
|
||||||
...dashboard,
|
|
||||||
data: {
|
|
||||||
...dashboard.data,
|
|
||||||
description: updatedDescription,
|
|
||||||
tags: updatedTags,
|
|
||||||
title: updatedTitle,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
toggleEditMode();
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
isEditMode,
|
|
||||||
updatedTitle,
|
|
||||||
updatedTags,
|
|
||||||
updatedDescription,
|
|
||||||
selectedDashboard,
|
|
||||||
toggleEditMode,
|
|
||||||
updateDashboardTitleDescriptionTags,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const onToggleHandler = (): void => {
|
const onToggleHandler = (): void => {
|
||||||
isIsJSONModalVisible((state) => !state);
|
isIsJSONModalVisible((state) => !state);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<Row align="top" justify="space-between">
|
<Row>
|
||||||
{!isEditMode ? (
|
<Col style={{ flex: 1 }}>
|
||||||
<Col>
|
<Typography.Title level={4} style={{ padding: 0, margin: 0 }}>
|
||||||
<Typography>{title}</Typography>
|
{title}
|
||||||
<Container>
|
</Typography.Title>
|
||||||
{tags?.map((e) => (
|
<Typography>{description}</Typography>
|
||||||
<Tag key={e}>{e}</Tag>
|
<div style={{ margin: '0.5rem 0' }}>
|
||||||
))}
|
{tags?.map((e) => (
|
||||||
</Container>
|
<Tag key={e}>{e}</Tag>
|
||||||
<Container>
|
))}
|
||||||
<Typography>{description}</Typography>
|
</div>
|
||||||
</Container>
|
<DashboardVariableSelection />
|
||||||
</Col>
|
</Col>
|
||||||
) : (
|
|
||||||
<Col lg={8}>
|
|
||||||
<NameOfTheDashboard name={updatedTitle} setName={setUpdatedTitle} />
|
|
||||||
<AddTags tags={updatedTags} setTags={setUpdatedTags} />
|
|
||||||
<Description
|
|
||||||
description={updatedDescription}
|
|
||||||
setDescription={setUpdatedDescription}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<ShareModal
|
|
||||||
{...{
|
|
||||||
isJSONModalVisible,
|
|
||||||
onToggleHandler,
|
|
||||||
selectedData,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Col>
|
<Col>
|
||||||
|
<ShareModal
|
||||||
|
{...{
|
||||||
|
isJSONModalVisible,
|
||||||
|
onToggleHandler,
|
||||||
|
selectedData,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<Space direction="vertical">
|
<Space direction="vertical">
|
||||||
<Button onClick={onToggleHandler} icon={<ShareAltOutlined />}>
|
{editDashboard && <SettingsDrawer />}
|
||||||
|
<Button
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
type="dashed"
|
||||||
|
onClick={onToggleHandler}
|
||||||
|
icon={<ShareAltOutlined />}
|
||||||
|
>
|
||||||
{t('share')}
|
{t('share')}
|
||||||
</Button>
|
</Button>
|
||||||
{editDashboard && (
|
|
||||||
<Button
|
|
||||||
icon={!isEditMode ? <EditOutlined /> : <SaveOutlined />}
|
|
||||||
onClick={onClickEditHandler}
|
|
||||||
>
|
|
||||||
{isEditMode ? t('save') : t('edit')}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Space>
|
</Space>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
@ -137,23 +71,4 @@ function DescriptionOfDashboard({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DispatchProps {
|
export default DescriptionOfDashboard;
|
||||||
updateDashboardTitleDescriptionTags: (
|
|
||||||
props: UpdateDashboardTitleDescriptionTagsProps,
|
|
||||||
) => (dispatch: Dispatch<AppActions>) => void;
|
|
||||||
toggleEditMode: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = (
|
|
||||||
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
|
|
||||||
): DispatchProps => ({
|
|
||||||
updateDashboardTitleDescriptionTags: bindActionCreators(
|
|
||||||
UpdateDashboardTitleDescriptionTags,
|
|
||||||
dispatch,
|
|
||||||
),
|
|
||||||
toggleEditMode: bindActionCreators(ToggleEditMode, dispatch),
|
|
||||||
});
|
|
||||||
|
|
||||||
type DescriptionOfDashboardProps = DispatchProps;
|
|
||||||
|
|
||||||
export default connect(null, mapDispatchToProps)(DescriptionOfDashboard);
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Button as ButtonComponent } from 'antd';
|
import { Button as ButtonComponent, Drawer } from 'antd';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
export const Container = styled.div`
|
export const Container = styled.div`
|
||||||
@ -11,3 +11,10 @@ export const Button = styled(ButtonComponent)`
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const DrawerContainer = styled(Drawer)`
|
||||||
|
.ant-drawer-header {
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { Button, Modal, Typography } from 'antd';
|
import { Button, Modal, Typography } from 'antd';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
|
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
|
||||||
|
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import { DashboardWidgetPageParams } from 'pages/DashboardWidget';
|
import { DashboardWidgetPageParams } from 'pages/DashboardWidget';
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
@ -143,6 +144,7 @@ function NewWidget({
|
|||||||
widgetId: selectedWidget?.id || '',
|
widgetId: selectedWidget?.id || '',
|
||||||
graphType: selectedGraph,
|
graphType: selectedGraph,
|
||||||
globalSelectedInterval,
|
globalSelectedInterval,
|
||||||
|
variables: getDashboardVariables(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
|
@ -1,10 +1,14 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
function Slack(): JSX.Element {
|
interface ISlackProps {
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
}
|
||||||
|
function Slack({ width, height }: ISlackProps): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
width="28"
|
width={`${width}`}
|
||||||
height="28"
|
height={`${height}`}
|
||||||
viewBox="0 0 28 28"
|
viewBox="0 0 28 28"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@ -44,5 +48,9 @@ function Slack(): JSX.Element {
|
|||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Slack.defaultProps = {
|
||||||
|
width: 28,
|
||||||
|
height: 28,
|
||||||
|
};
|
||||||
|
|
||||||
export default Slack;
|
export default Slack;
|
||||||
|
@ -62,7 +62,7 @@ const menus: SidebarMenu[] = [
|
|||||||
{
|
{
|
||||||
Icon: ApiOutlined,
|
Icon: ApiOutlined,
|
||||||
to: ROUTES.INSTRUMENTATION,
|
to: ROUTES.INSTRUMENTATION,
|
||||||
name: 'Add instrumentation',
|
name: 'Get Started',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ const breadcrumbNameMap = {
|
|||||||
[ROUTES.TRACE]: 'Traces',
|
[ROUTES.TRACE]: 'Traces',
|
||||||
[ROUTES.SERVICE_MAP]: 'Service Map',
|
[ROUTES.SERVICE_MAP]: 'Service Map',
|
||||||
[ROUTES.USAGE_EXPLORER]: 'Usage Explorer',
|
[ROUTES.USAGE_EXPLORER]: 'Usage Explorer',
|
||||||
[ROUTES.INSTRUMENTATION]: 'Add instrumentation',
|
[ROUTES.INSTRUMENTATION]: 'Get Started',
|
||||||
[ROUTES.SETTINGS]: 'Settings',
|
[ROUTES.SETTINGS]: 'Settings',
|
||||||
[ROUTES.DASHBOARD]: 'Dashboard',
|
[ROUTES.DASHBOARD]: 'Dashboard',
|
||||||
[ROUTES.ALL_ERROR]: 'Exceptions',
|
[ROUTES.ALL_ERROR]: 'Exceptions',
|
||||||
|
@ -0,0 +1,127 @@
|
|||||||
|
import { Input, notification } from 'antd';
|
||||||
|
import getFilters from 'api/trace/getFilters';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { Dispatch } from 'redux';
|
||||||
|
import { getFilter, updateURL } from 'store/actions/trace/util';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
|
import AppActions from 'types/actions';
|
||||||
|
import { UPDATE_ALL_FILTERS } from 'types/actions/trace';
|
||||||
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
|
import { TraceReducer } from 'types/reducer/trace';
|
||||||
|
|
||||||
|
const { Search } = Input;
|
||||||
|
|
||||||
|
function TraceID(): JSX.Element {
|
||||||
|
const {
|
||||||
|
selectedFilter,
|
||||||
|
filterToFetchData,
|
||||||
|
spansAggregate,
|
||||||
|
selectedTags,
|
||||||
|
userSelectedFilter,
|
||||||
|
isFilterExclude,
|
||||||
|
} = useSelector<AppState, TraceReducer>((state) => state.traces);
|
||||||
|
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||||
|
const globalTime = useSelector<AppState, GlobalReducer>(
|
||||||
|
(state) => state.globalTime,
|
||||||
|
);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [userEnteredValue, setUserEnteredValue] = useState<string>('');
|
||||||
|
useEffect(() => {
|
||||||
|
setUserEnteredValue(selectedFilter.get('traceID')?.[0] || '');
|
||||||
|
}, [selectedFilter]);
|
||||||
|
const onSearch = async (value: string): Promise<void> => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
const preSelectedFilter = new Map(selectedFilter);
|
||||||
|
const preUserSelected = new Map(userSelectedFilter);
|
||||||
|
|
||||||
|
if (value !== '') {
|
||||||
|
preUserSelected.set('traceID', [value]);
|
||||||
|
preSelectedFilter.set('traceID', [value]);
|
||||||
|
} else {
|
||||||
|
preUserSelected.delete('traceID');
|
||||||
|
preSelectedFilter.delete('traceID');
|
||||||
|
}
|
||||||
|
const response = await getFilters({
|
||||||
|
other: Object.fromEntries(preSelectedFilter),
|
||||||
|
end: String(globalTime.maxTime),
|
||||||
|
start: String(globalTime.minTime),
|
||||||
|
getFilters: filterToFetchData,
|
||||||
|
isFilterExclude,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.statusCode === 200) {
|
||||||
|
const preFilter = getFilter(response.payload);
|
||||||
|
preFilter.set('traceID', { traceID: value });
|
||||||
|
preFilter.forEach((value, key) => {
|
||||||
|
const values = Object.keys(value);
|
||||||
|
if (key !== 'duration' && values.length) {
|
||||||
|
preUserSelected.set(key, values);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: UPDATE_ALL_FILTERS,
|
||||||
|
payload: {
|
||||||
|
current: spansAggregate.currentPage,
|
||||||
|
filter: preFilter,
|
||||||
|
filterToFetchData,
|
||||||
|
selectedFilter: preSelectedFilter,
|
||||||
|
selectedTags,
|
||||||
|
userSelected: preUserSelected,
|
||||||
|
isFilterExclude,
|
||||||
|
order: spansAggregate.order,
|
||||||
|
pageSize: spansAggregate.pageSize,
|
||||||
|
orderParam: spansAggregate.orderParam,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
updateURL(
|
||||||
|
preSelectedFilter,
|
||||||
|
filterToFetchData,
|
||||||
|
spansAggregate.currentPage,
|
||||||
|
selectedTags,
|
||||||
|
isFilterExclude,
|
||||||
|
userSelectedFilter,
|
||||||
|
spansAggregate.order,
|
||||||
|
spansAggregate.pageSize,
|
||||||
|
spansAggregate.orderParam,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
notification.error({
|
||||||
|
message: (error as AxiosError).toString() || 'Something went wrong',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const onChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||||
|
setUserEnteredValue(e.target.value);
|
||||||
|
};
|
||||||
|
const onBlur = (): void => {
|
||||||
|
if (userEnteredValue !== selectedFilter.get('traceID')?.[0]) {
|
||||||
|
onSearch(userEnteredValue);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Search
|
||||||
|
placeholder="Filter by Trace ID"
|
||||||
|
onSearch={onSearch}
|
||||||
|
style={{
|
||||||
|
marginBottom: '5rem',
|
||||||
|
padding: '0 3%',
|
||||||
|
}}
|
||||||
|
loading={isLoading}
|
||||||
|
value={userEnteredValue}
|
||||||
|
onChange={onChange}
|
||||||
|
onBlur={onBlur}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TraceID;
|
@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable no-nested-ternary */
|
||||||
import { Card } from 'antd';
|
import { Card } from 'antd';
|
||||||
import Spinner from 'components/Spinner';
|
import Spinner from 'components/Spinner';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
@ -7,6 +8,7 @@ import { TraceFilterEnum, TraceReducer } from 'types/reducer/trace';
|
|||||||
|
|
||||||
import CommonCheckBox from './CommonCheckBox';
|
import CommonCheckBox from './CommonCheckBox';
|
||||||
import Duration from './Duration';
|
import Duration from './Duration';
|
||||||
|
import TraceID from './SearchTraceID';
|
||||||
|
|
||||||
function PanelBody(props: PanelBodyProps): JSX.Element {
|
function PanelBody(props: PanelBodyProps): JSX.Element {
|
||||||
const { type } = props;
|
const { type } = props;
|
||||||
@ -22,12 +24,17 @@ function PanelBody(props: PanelBodyProps): JSX.Element {
|
|||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
const renderBody = (type: TraceFilterEnum): JSX.Element => {
|
||||||
return (
|
switch (type) {
|
||||||
<Card bordered={false}>
|
case 'traceID':
|
||||||
{type === 'duration' ? <Duration /> : <CommonCheckBox name={type} />}
|
return <TraceID />;
|
||||||
</Card>
|
case 'duration':
|
||||||
);
|
return <Duration />;
|
||||||
|
default:
|
||||||
|
return <CommonCheckBox name={type} />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return <Card bordered={false}>{renderBody(type)}</Card>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PanelBodyProps {
|
interface PanelBodyProps {
|
||||||
|
@ -16,6 +16,7 @@ export const AllTraceFilterEnum: TraceFilterEnum[] = [
|
|||||||
'httpMethod',
|
'httpMethod',
|
||||||
'httpRoute',
|
'httpRoute',
|
||||||
'httpUrl',
|
'httpUrl',
|
||||||
|
'traceID',
|
||||||
];
|
];
|
||||||
|
|
||||||
function Filters(): JSX.Element {
|
function Filters(): JSX.Element {
|
||||||
|
@ -0,0 +1,20 @@
|
|||||||
|
export const commaValuesParser = (query: string): (string | number)[] => {
|
||||||
|
if (!query) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const match = query.match(/(?:\\,|[^,])+/g) ?? [];
|
||||||
|
|
||||||
|
const options: string[] = match.map((text) => {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
text = text.replace(/\\,/g, ',');
|
||||||
|
const textMatch = /^(.+)\s:\s(.+)$/g.exec(text) ?? [];
|
||||||
|
if (textMatch.length === 3) {
|
||||||
|
const [, , value] = textMatch;
|
||||||
|
return value.trim();
|
||||||
|
}
|
||||||
|
return text.trim();
|
||||||
|
});
|
||||||
|
return options.map((option): string | number =>
|
||||||
|
Number.isNaN(Number(option)) ? option : Number(option),
|
||||||
|
);
|
||||||
|
};
|
38
frontend/src/lib/dashbaordVariables/getDashboardVariables.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import GetMinMax from 'lib/getMinMax';
|
||||||
|
import GetStartAndEndTime from 'lib/getStartAndEndTime';
|
||||||
|
import store from 'store';
|
||||||
|
|
||||||
|
export const getDashboardVariables = (): Record<string, unknown> => {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
globalTime,
|
||||||
|
dashboards: { dashboards },
|
||||||
|
} = store.getState();
|
||||||
|
const [selectedDashboard] = dashboards;
|
||||||
|
const {
|
||||||
|
data: { variables },
|
||||||
|
} = selectedDashboard;
|
||||||
|
|
||||||
|
const minMax = GetMinMax(globalTime.selectedTime, [
|
||||||
|
globalTime.minTime / 1000000,
|
||||||
|
globalTime.maxTime / 1000000,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const { start, end } = GetStartAndEndTime({
|
||||||
|
type: 'GLOBAL_TIME',
|
||||||
|
minTime: minMax.minTime,
|
||||||
|
maxTime: minMax.maxTime,
|
||||||
|
});
|
||||||
|
const variablesTuple: Record<string, unknown> = {
|
||||||
|
SIGNOZ_START_TIME: parseInt(start, 10) * 1e3,
|
||||||
|
SIGNOZ_END_TIME: parseInt(end, 10) * 1e3,
|
||||||
|
};
|
||||||
|
Object.keys(variables).forEach((key) => {
|
||||||
|
variablesTuple[key] = variables[key].selectedValue;
|
||||||
|
});
|
||||||
|
return variablesTuple;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
};
|
15
frontend/src/lib/dashbaordVariables/sortVariableValues.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { sortBy } from 'lodash-es';
|
||||||
|
import { TSortVariableValuesType } from 'types/api/dashboard/getAll';
|
||||||
|
|
||||||
|
type TValuesDataType = (string | number | boolean)[];
|
||||||
|
const sortValues = (
|
||||||
|
values: TValuesDataType,
|
||||||
|
sortType: TSortVariableValuesType,
|
||||||
|
): TValuesDataType => {
|
||||||
|
if (sortType === 'ASC') return sortBy(values);
|
||||||
|
if (sortType === 'DESC') return sortBy(values).reverse();
|
||||||
|
|
||||||
|
return values;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default sortValues;
|
@ -6,6 +6,16 @@ import convertIntoEpoc from './covertIntoEpoc';
|
|||||||
import { colors } from './getRandomColor';
|
import { colors } from './getRandomColor';
|
||||||
|
|
||||||
const getChartData = ({ queryData }: GetChartDataProps): ChartData => {
|
const getChartData = ({ queryData }: GetChartDataProps): ChartData => {
|
||||||
|
const uniqueTimeLabels = new Set<number>();
|
||||||
|
queryData.forEach((data) => {
|
||||||
|
data.queryData.forEach((query) => {
|
||||||
|
query.values.forEach((value) => {
|
||||||
|
uniqueTimeLabels.add(value[0]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const labels = Array.from(uniqueTimeLabels).sort((a, b) => a - b);
|
||||||
|
|
||||||
const response = queryData.map(
|
const response = queryData.map(
|
||||||
({ queryData, query: queryG, legend: legendG }) => {
|
({ queryData, query: queryG, legend: legendG }) => {
|
||||||
return queryData.map((e) => {
|
return queryData.map((e) => {
|
||||||
@ -22,11 +32,24 @@ const getChartData = ({ queryData }: GetChartDataProps): ChartData => {
|
|||||||
second: Number(parseFloat(second)),
|
second: Number(parseFloat(second)),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
// Fill the missing data with null
|
||||||
|
const filledDataValues = Array.from(labels).map((e) => {
|
||||||
|
const td1 = new Date(parseInt(convertIntoEpoc(e * 1000), 10));
|
||||||
|
const data = dataValue.find((e1) => {
|
||||||
|
return e1.first.getTime() === td1.getTime();
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
data || {
|
||||||
|
first: new Date(parseInt(convertIntoEpoc(e * 1000), 10)),
|
||||||
|
second: null,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
label: labelNames !== 'undefined' ? labelNames : '',
|
label: labelNames !== 'undefined' ? labelNames : '',
|
||||||
first: dataValue.map((e) => e.first),
|
first: filledDataValues.map((e) => e.first),
|
||||||
second: dataValue.map((e) => e.second),
|
second: filledDataValues.map((e) => e.second),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -1,45 +0,0 @@
|
|||||||
import { Typography } from 'antd';
|
|
||||||
import React from 'react';
|
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import { AppState } from 'store/reducers';
|
|
||||||
import AppReducer from 'types/reducer/app';
|
|
||||||
|
|
||||||
import { Container, Heading } from './styles';
|
|
||||||
|
|
||||||
function InstrumentationPage(): JSX.Element {
|
|
||||||
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Heading>Instrument your application</Heading>
|
|
||||||
<Container isDarkMode={isDarkMode}>
|
|
||||||
<Typography>Congrats, you have successfully installed SigNoz!</Typography>{' '}
|
|
||||||
<Typography>
|
|
||||||
To start seeing YOUR application data here, follow the instructions in the
|
|
||||||
docs -
|
|
||||||
</Typography>
|
|
||||||
<a
|
|
||||||
href="https://signoz.io/docs/instrumentation/overview"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
https://signoz.io/docs/instrumentation/overview
|
|
||||||
</a>
|
|
||||||
If you face any issues, join our
|
|
||||||
<a
|
|
||||||
href="https://signoz-community.slack.com/join/shared_invite/zt-lrjknbbp-J_mI13rlw8pGF4EWBnorJA"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
slack community
|
|
||||||
</a>
|
|
||||||
to ask any questions or mail us at
|
|
||||||
<a href="mailto:support@signoz.io" target="_blank" rel="noreferrer">
|
|
||||||
support@signoz.io
|
|
||||||
</a>
|
|
||||||
</Container>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default InstrumentationPage;
|
|
30
frontend/src/pages/GettingStarted/DocCard.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { Typography } from 'antd';
|
||||||
|
import React from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
|
import AppReducer from 'types/reducer/app';
|
||||||
|
|
||||||
|
import { DocCardContainer } from './styles';
|
||||||
|
import { TGetStartedContentDoc } from './types';
|
||||||
|
import UTMParams from './utmParams';
|
||||||
|
|
||||||
|
interface IDocCardProps {
|
||||||
|
text: TGetStartedContentDoc['title'];
|
||||||
|
icon: TGetStartedContentDoc['icon'];
|
||||||
|
url: TGetStartedContentDoc['url'];
|
||||||
|
}
|
||||||
|
function DocCard({ icon, text, url }: IDocCardProps): JSX.Element {
|
||||||
|
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link to={{ pathname: `${url}${UTMParams}` }} target="_blank">
|
||||||
|
<DocCardContainer isDarkMode={isDarkMode}>
|
||||||
|
<span style={{ color: isDarkMode ? '#ddd' : '#333' }}>{icon}</span>
|
||||||
|
<Typography.Text style={{ marginLeft: '0.5rem' }}>{text}</Typography.Text>
|
||||||
|
</DocCardContainer>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DocCard;
|
43
frontend/src/pages/GettingStarted/Section.tsx
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { Col, Row, Typography } from 'antd';
|
||||||
|
import { map } from 'lodash-es';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import DocCard from './DocCard';
|
||||||
|
import { TGetStartedContentSection } from './types';
|
||||||
|
|
||||||
|
interface IDocSectionProps {
|
||||||
|
sectionData: TGetStartedContentSection;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DocSection({ sectionData }: IDocSectionProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div style={{ marginTop: '2rem' }}>
|
||||||
|
<Typography.Text strong>{sectionData.heading}</Typography.Text>
|
||||||
|
<Row
|
||||||
|
gutter={{ xs: 0, sm: 8, md: 16, lg: 24 }}
|
||||||
|
style={{ padding: '0 3%', marginTop: '0.5rem' }}
|
||||||
|
>
|
||||||
|
{sectionData.description && (
|
||||||
|
<Col span={24}>
|
||||||
|
<Typography.Text>{sectionData.description}</Typography.Text>
|
||||||
|
</Col>
|
||||||
|
)}
|
||||||
|
{map(sectionData.items, (item, idx) => (
|
||||||
|
<Col
|
||||||
|
key={`${item.title}+${idx}`}
|
||||||
|
sm={24}
|
||||||
|
md={12}
|
||||||
|
lg={12}
|
||||||
|
xl={8}
|
||||||
|
xxl={6}
|
||||||
|
style={{ margin: '1rem 0' }}
|
||||||
|
>
|
||||||
|
<DocCard icon={item.icon} text={item.title} url={item.url} />
|
||||||
|
</Col>
|
||||||
|
))}
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DocSection;
|
21
frontend/src/pages/GettingStarted/index.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { Typography } from 'antd';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { GetStartedContent } from './renderConfig';
|
||||||
|
import DocSection from './Section';
|
||||||
|
|
||||||
|
function InstrumentationPage(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Typography>
|
||||||
|
Congrats, you have successfully installed SigNoz! Now lets get some data in
|
||||||
|
and start deriving insights from them
|
||||||
|
</Typography>
|
||||||
|
{GetStartedContent().map((section) => {
|
||||||
|
return <DocSection key={section.heading} sectionData={section} />;
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default InstrumentationPage;
|
175
frontend/src/pages/GettingStarted/renderConfig.tsx
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
import {
|
||||||
|
AlertFilled,
|
||||||
|
AlignLeftOutlined,
|
||||||
|
ApiFilled,
|
||||||
|
BarChartOutlined,
|
||||||
|
DashboardFilled,
|
||||||
|
SoundFilled,
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import { Typography } from 'antd';
|
||||||
|
import Slack from 'container/SideNav/Slack';
|
||||||
|
import React from 'react';
|
||||||
|
import store from 'store';
|
||||||
|
|
||||||
|
import { TGetStartedContentSection } from './types';
|
||||||
|
|
||||||
|
export const GetStartedContent = (): TGetStartedContentSection[] => {
|
||||||
|
const {
|
||||||
|
app: { currentVersion },
|
||||||
|
} = store.getState();
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
heading: 'Send data from your applications to SigNoz',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
title: 'Instrument your Java Application',
|
||||||
|
icon: (
|
||||||
|
<img src={`/Logos/java.png?currentVersion=${currentVersion}`} alt="" />
|
||||||
|
),
|
||||||
|
url: 'https://signoz.io/docs/instrumentation/java/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Instrument your Python Application',
|
||||||
|
icon: (
|
||||||
|
<img src={`/Logos/python.png?currentVersion=${currentVersion}`} alt="" />
|
||||||
|
),
|
||||||
|
url: 'https://signoz.io/docs/instrumentation/python/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Instrument your JS Application',
|
||||||
|
icon: (
|
||||||
|
<img
|
||||||
|
src={`/Logos/javascript.png?currentVersion=${currentVersion}`}
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
url: 'https://signoz.io/docs/instrumentation/javascript/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Instrument your Go Application',
|
||||||
|
icon: (
|
||||||
|
<img src={`/Logos/go.png?currentVersion=${currentVersion}`} alt="" />
|
||||||
|
),
|
||||||
|
url: 'https://signoz.io/docs/instrumentation/golang/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Instrument your .NET Application',
|
||||||
|
icon: (
|
||||||
|
<img
|
||||||
|
src={`/Logos/ms-net-framework.png?currentVersion=${currentVersion}`}
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
url: 'https://signoz.io/docs/instrumentation/dotnet/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Instrument your PHP Application',
|
||||||
|
icon: (
|
||||||
|
<img src={`/Logos/php.png?currentVersion=${currentVersion}`} alt="" />
|
||||||
|
),
|
||||||
|
url: 'https://signoz.io/docs/instrumentation/php/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Instrument your Rails Application',
|
||||||
|
icon: (
|
||||||
|
<img src={`/Logos/rails.png?currentVersion=${currentVersion}`} alt="" />
|
||||||
|
),
|
||||||
|
url: 'https://signoz.io/docs/instrumentation/ruby-on-rails/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Instrument your Rust Application',
|
||||||
|
icon: (
|
||||||
|
<img src={`/Logos/rust.png?currentVersion=${currentVersion}`} alt="" />
|
||||||
|
),
|
||||||
|
url: 'https://signoz.io/docs/instrumentation/rust/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Instrument your Elixir Application',
|
||||||
|
icon: (
|
||||||
|
<img src={`/Logos/elixir.png?currentVersion=${currentVersion}`} alt="" />
|
||||||
|
),
|
||||||
|
url: 'https://signoz.io/docs/instrumentation/elixir/',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
heading: 'Send Metrics from your Infrastructure & create Dashboards',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
title: 'Send metrics to SigNoz',
|
||||||
|
icon: <BarChartOutlined style={{ fontSize: '3.5rem' }} />,
|
||||||
|
url: 'https://signoz.io/docs/userguide/send-metrics/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Create and Manage Dashboards',
|
||||||
|
icon: <DashboardFilled style={{ fontSize: '3.5rem' }} />,
|
||||||
|
url: 'https://signoz.io/docs/userguide/manage-dashboards-and-panels/',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
heading: 'Send your logs to SigNoz',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
title: 'Send your logs to SigNoz',
|
||||||
|
icon: <AlignLeftOutlined style={{ fontSize: '3.5rem' }} />,
|
||||||
|
url: 'https://signoz.io/docs/userguide/logs/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Existing log collectors to SigNoz',
|
||||||
|
icon: <ApiFilled style={{ fontSize: '3.5rem' }} />,
|
||||||
|
url: 'https://signoz.io/docs/userguide/fluentbit_to_signoz/',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
heading: 'Create alerts on Metrics',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
title: 'Create alert rules on metrics',
|
||||||
|
icon: <AlertFilled style={{ fontSize: '3.5rem' }} />,
|
||||||
|
url: 'https://signoz.io/docs/userguide/alerts-management/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Configure alert notification channels',
|
||||||
|
icon: <SoundFilled style={{ fontSize: '3.5rem' }} />,
|
||||||
|
url:
|
||||||
|
'https://signoz.io/docs/userguide/alerts-management/#setting-up-a-notification-channel',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
heading: 'Need help?',
|
||||||
|
description: (
|
||||||
|
<>
|
||||||
|
{'Join our slack community and ask any question you may have on '}
|
||||||
|
<Typography.Link
|
||||||
|
href="https://signoz-community.slack.com/archives/C01HWUTP4HH"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
#support
|
||||||
|
</Typography.Link>
|
||||||
|
{' or '}
|
||||||
|
<Typography.Link
|
||||||
|
href="https://signoz-community.slack.com/archives/C01HWQ1R0BC"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
#general
|
||||||
|
</Typography.Link>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
title: 'Join SigNoz slack community ',
|
||||||
|
icon: (
|
||||||
|
<div style={{ padding: '0.7rem' }}>
|
||||||
|
<Slack width={30} height={30} />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
url: 'https://signoz.io/slack',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
@ -1,4 +1,4 @@
|
|||||||
import { Card, Typography } from 'antd';
|
import { Card, Row, Typography } from 'antd';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -18,3 +18,13 @@ export const Heading = styled(Typography)`
|
|||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const DocCardContainer = styled(Row)<{
|
||||||
|
isDarkMode: boolean;
|
||||||
|
}>`
|
||||||
|
display: flex;
|
||||||
|
border: 1px solid ${({ isDarkMode }): string => (isDarkMode ? '#444' : '#ccc')};
|
||||||
|
border-radius: 0.2rem;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.5rem 0.25rem;
|
||||||
|
`;
|
10
frontend/src/pages/GettingStarted/types.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
export type TGetStartedContentDoc = {
|
||||||
|
title: string;
|
||||||
|
icon: JSX.Element;
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
export type TGetStartedContentSection = {
|
||||||
|
heading: string;
|
||||||
|
description?: string | JSX.Element;
|
||||||
|
items: TGetStartedContentDoc[];
|
||||||
|
};
|
3
frontend/src/pages/GettingStarted/utmParams.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
const UTMParams =
|
||||||
|
'?utm_source=instrumentation_page&utm_medium=frontend&utm_term=language';
|
||||||
|
export default UTMParams;
|
@ -30,6 +30,7 @@ export const DeleteWidget = ({
|
|||||||
tags: selectedDashboard.data.tags,
|
tags: selectedDashboard.data.tags,
|
||||||
widgets: updatedWidgets,
|
widgets: updatedWidgets,
|
||||||
layout: updatedLayout,
|
layout: updatedLayout,
|
||||||
|
variables: selectedDashboard.data.variables,
|
||||||
},
|
},
|
||||||
uuid: selectedDashboard.uuid,
|
uuid: selectedDashboard.uuid,
|
||||||
};
|
};
|
||||||
|
@ -19,7 +19,7 @@ import { Dispatch } from 'redux';
|
|||||||
import store from 'store';
|
import store from 'store';
|
||||||
import AppActions from 'types/actions';
|
import AppActions from 'types/actions';
|
||||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
import { Query } from 'types/api/dashboard/getAll';
|
import { IDashboardVariable, Query } from 'types/api/dashboard/getAll';
|
||||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||||
import { EDataSource, EPanelType, EQueryType } from 'types/common/dashboard';
|
import { EDataSource, EPanelType, EQueryType } from 'types/common/dashboard';
|
||||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
@ -29,11 +29,13 @@ export async function GetMetricQueryRange({
|
|||||||
globalSelectedInterval,
|
globalSelectedInterval,
|
||||||
graphType,
|
graphType,
|
||||||
selectedTime,
|
selectedTime,
|
||||||
|
variables = {},
|
||||||
}: {
|
}: {
|
||||||
query: Query;
|
query: Query;
|
||||||
graphType: GRAPH_TYPES;
|
graphType: GRAPH_TYPES;
|
||||||
selectedTime: timePreferenceType;
|
selectedTime: timePreferenceType;
|
||||||
globalSelectedInterval: Time;
|
globalSelectedInterval: Time;
|
||||||
|
variables?: Record<string, unknown>;
|
||||||
}): Promise<SuccessResponse<MetricRangePayloadProps> | ErrorResponse> {
|
}): Promise<SuccessResponse<MetricRangePayloadProps> | ErrorResponse> {
|
||||||
const { queryType } = query;
|
const { queryType } = query;
|
||||||
const queryKey: Record<EQueryTypeToQueryKeyMapping, string> =
|
const queryKey: Record<EQueryTypeToQueryKeyMapping, string> =
|
||||||
@ -138,6 +140,7 @@ export async function GetMetricQueryRange({
|
|||||||
start: parseInt(start, 10) * 1e3,
|
start: parseInt(start, 10) * 1e3,
|
||||||
end: parseInt(end, 10) * 1e3,
|
end: parseInt(end, 10) * 1e3,
|
||||||
step: getStep({ start, end, inputFormat: 'ms' }),
|
step: getStep({ start, end, inputFormat: 'ms' }),
|
||||||
|
variables,
|
||||||
...QueryPayload,
|
...QueryPayload,
|
||||||
});
|
});
|
||||||
if (response.statusCode >= 400) {
|
if (response.statusCode >= 400) {
|
||||||
@ -173,6 +176,14 @@ export const GetQueryResults = (
|
|||||||
): ((dispatch: Dispatch<AppActions>) => void) => {
|
): ((dispatch: Dispatch<AppActions>) => void) => {
|
||||||
return async (dispatch: Dispatch<AppActions>): Promise<void> => {
|
return async (dispatch: Dispatch<AppActions>): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
|
dispatch({
|
||||||
|
type: 'QUERY_ERROR',
|
||||||
|
payload: {
|
||||||
|
errorMessage: '',
|
||||||
|
widgetId: props.widgetId,
|
||||||
|
errorBoolean: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
const response = await GetMetricQueryRange(props);
|
const response = await GetMetricQueryRange(props);
|
||||||
|
|
||||||
const isError = response.error;
|
const isError = response.error;
|
||||||
@ -199,14 +210,6 @@ export const GetQueryResults = (
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
dispatch({
|
|
||||||
type: 'QUERY_ERROR',
|
|
||||||
payload: {
|
|
||||||
errorMessage: '',
|
|
||||||
widgetId: props.widgetId,
|
|
||||||
errorBoolean: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'QUERY_ERROR',
|
type: 'QUERY_ERROR',
|
||||||
@ -226,4 +229,5 @@ export interface GetQueryResultsProps {
|
|||||||
query: Query;
|
query: Query;
|
||||||
graphType: ITEMS;
|
graphType: ITEMS;
|
||||||
globalSelectedInterval: GlobalReducer['selectedTime'];
|
globalSelectedInterval: GlobalReducer['selectedTime'];
|
||||||
|
variables: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,38 @@
|
|||||||
|
import { notification } from 'antd';
|
||||||
|
import update from 'api/dashboard/update';
|
||||||
|
import { Dispatch } from 'redux';
|
||||||
|
import store from 'store/index';
|
||||||
|
import AppActions from 'types/actions';
|
||||||
|
import { UPDATE_DASHBOARD_VARIABLES } from 'types/actions/dashboard';
|
||||||
|
import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||||
|
|
||||||
|
export const UpdateDashboardVariables = (
|
||||||
|
variables: Record<string, IDashboardVariable>,
|
||||||
|
): ((dispatch: Dispatch<AppActions>) => void) => {
|
||||||
|
return async (dispatch: Dispatch<AppActions>): Promise<void> => {
|
||||||
|
try {
|
||||||
|
dispatch({
|
||||||
|
type: UPDATE_DASHBOARD_VARIABLES,
|
||||||
|
payload: variables,
|
||||||
|
});
|
||||||
|
|
||||||
|
const reduxStoreState = store.getState();
|
||||||
|
const [dashboard] = reduxStoreState.dashboards.dashboards;
|
||||||
|
|
||||||
|
const response = await update({
|
||||||
|
data: {
|
||||||
|
...dashboard.data,
|
||||||
|
},
|
||||||
|
uuid: dashboard.uuid,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.statusCode !== 200) {
|
||||||
|
notification.error({
|
||||||
|
message: response.error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
@ -18,6 +18,7 @@ import {
|
|||||||
SAVE_SETTING_TO_PANEL_SUCCESS,
|
SAVE_SETTING_TO_PANEL_SUCCESS,
|
||||||
TOGGLE_EDIT_MODE,
|
TOGGLE_EDIT_MODE,
|
||||||
UPDATE_DASHBOARD,
|
UPDATE_DASHBOARD,
|
||||||
|
UPDATE_DASHBOARD_VARIABLES,
|
||||||
UPDATE_QUERY,
|
UPDATE_QUERY,
|
||||||
UPDATE_TITLE_DESCRIPTION_TAGS_SUCCESS,
|
UPDATE_TITLE_DESCRIPTION_TAGS_SUCCESS,
|
||||||
} from 'types/actions/dashboard';
|
} from 'types/actions/dashboard';
|
||||||
@ -170,7 +171,6 @@ const dashboard = (
|
|||||||
|
|
||||||
case QUERY_ERROR: {
|
case QUERY_ERROR: {
|
||||||
const { widgetId, errorMessage, errorBoolean = true } = action.payload;
|
const { widgetId, errorMessage, errorBoolean = true } = action.payload;
|
||||||
|
|
||||||
const [selectedDashboard] = state.dashboards;
|
const [selectedDashboard] = state.dashboards;
|
||||||
const { data } = selectedDashboard;
|
const { data } = selectedDashboard;
|
||||||
|
|
||||||
@ -397,7 +397,25 @@ const dashboard = (
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
case UPDATE_DASHBOARD_VARIABLES: {
|
||||||
|
const variablesData = action.payload;
|
||||||
|
const { dashboards } = state;
|
||||||
|
const [selectedDashboard] = dashboards;
|
||||||
|
const { data } = selectedDashboard;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
dashboards: [
|
||||||
|
{
|
||||||
|
...selectedDashboard,
|
||||||
|
data: {
|
||||||
|
...data,
|
||||||
|
variables: variablesData,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
@ -68,6 +68,7 @@ const initialValue: TraceReducer = {
|
|||||||
['responseStatusCode', INITIAL_FILTER_VALUE],
|
['responseStatusCode', INITIAL_FILTER_VALUE],
|
||||||
['serviceName', INITIAL_FILTER_VALUE],
|
['serviceName', INITIAL_FILTER_VALUE],
|
||||||
['status', INITIAL_FILTER_VALUE],
|
['status', INITIAL_FILTER_VALUE],
|
||||||
|
['traceID', INITIAL_FILTER_VALUE],
|
||||||
]),
|
]),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,6 +1,11 @@
|
|||||||
import { Layout } from 'react-grid-layout';
|
import { Layout } from 'react-grid-layout';
|
||||||
import { ApplySettingsToPanelProps } from 'store/actions/dashboard/applySettingsToPanel';
|
import { ApplySettingsToPanelProps } from 'store/actions/dashboard/applySettingsToPanel';
|
||||||
import { Dashboard, Query, Widgets } from 'types/api/dashboard/getAll';
|
import {
|
||||||
|
Dashboard,
|
||||||
|
IDashboardVariable,
|
||||||
|
Query,
|
||||||
|
Widgets,
|
||||||
|
} from 'types/api/dashboard/getAll';
|
||||||
import { QueryData } from 'types/api/widgets/getQuery';
|
import { QueryData } from 'types/api/widgets/getQuery';
|
||||||
|
|
||||||
export const GET_DASHBOARD = 'GET_DASHBOARD';
|
export const GET_DASHBOARD = 'GET_DASHBOARD';
|
||||||
@ -42,6 +47,8 @@ export const IS_ADD_WIDGET = 'IS_ADD_WIDGET';
|
|||||||
|
|
||||||
export const DELETE_QUERY = 'DELETE_QUERY';
|
export const DELETE_QUERY = 'DELETE_QUERY';
|
||||||
export const FLUSH_DASHBOARD = 'FLUSH_DASHBOARD';
|
export const FLUSH_DASHBOARD = 'FLUSH_DASHBOARD';
|
||||||
|
export const UPDATE_DASHBOARD_VARIABLES = 'UPDATE_DASHBOARD_VARIABLES';
|
||||||
|
|
||||||
interface GetDashboard {
|
interface GetDashboard {
|
||||||
type: typeof GET_DASHBOARD;
|
type: typeof GET_DASHBOARD;
|
||||||
payload: Dashboard;
|
payload: Dashboard;
|
||||||
@ -174,6 +181,10 @@ interface DeleteQuery {
|
|||||||
interface FlushDashboard {
|
interface FlushDashboard {
|
||||||
type: typeof FLUSH_DASHBOARD;
|
type: typeof FLUSH_DASHBOARD;
|
||||||
}
|
}
|
||||||
|
interface UpdateDashboardVariables {
|
||||||
|
type: typeof UPDATE_DASHBOARD_VARIABLES;
|
||||||
|
payload: Record<string, IDashboardVariable>;
|
||||||
|
}
|
||||||
|
|
||||||
export type DashboardActions =
|
export type DashboardActions =
|
||||||
| GetDashboard
|
| GetDashboard
|
||||||
@ -194,4 +205,5 @@ export type DashboardActions =
|
|||||||
| IsAddWidget
|
| IsAddWidget
|
||||||
| UpdateQuery
|
| UpdateQuery
|
||||||
| DeleteQuery
|
| DeleteQuery
|
||||||
| FlushDashboard;
|
| FlushDashboard
|
||||||
|
| UpdateDashboardVariables;
|
||||||
|
@ -11,6 +11,31 @@ import { QueryData } from '../widgets/getQuery';
|
|||||||
|
|
||||||
export type PayloadProps = Dashboard[];
|
export type PayloadProps = Dashboard[];
|
||||||
|
|
||||||
|
export const VariableQueryTypeArr = ['QUERY', 'TEXTBOX', 'CUSTOM'] as const;
|
||||||
|
export type TVariableQueryType = typeof VariableQueryTypeArr[number];
|
||||||
|
|
||||||
|
export const VariableSortTypeArr = ['DISABLED', 'ASC', 'DESC'] as const;
|
||||||
|
export type TSortVariableValuesType = typeof VariableSortTypeArr[number];
|
||||||
|
|
||||||
|
export interface IDashboardVariable {
|
||||||
|
name?: string; // key will be the source of truth
|
||||||
|
description: string;
|
||||||
|
type: TVariableQueryType;
|
||||||
|
// Query
|
||||||
|
queryValue?: string;
|
||||||
|
// Custom
|
||||||
|
customValue?: string;
|
||||||
|
// Textbox
|
||||||
|
textboxValue?: string;
|
||||||
|
|
||||||
|
sort: TSortVariableValuesType;
|
||||||
|
multiSelect: boolean;
|
||||||
|
showALLOption: boolean;
|
||||||
|
selectedValue?: null | string | string[];
|
||||||
|
// Internal use
|
||||||
|
modificationUUID?: string;
|
||||||
|
allSelected?: boolean;
|
||||||
|
}
|
||||||
export interface Dashboard {
|
export interface Dashboard {
|
||||||
id: number;
|
id: number;
|
||||||
uuid: string;
|
uuid: string;
|
||||||
@ -26,6 +51,7 @@ export interface DashboardData {
|
|||||||
widgets?: Widgets[];
|
widgets?: Widgets[];
|
||||||
title: string;
|
title: string;
|
||||||
layout?: Layout[];
|
layout?: Layout[];
|
||||||
|
variables: Record<string, IDashboardVariable>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IBaseWidget {
|
export interface IBaseWidget {
|
||||||
|
7
frontend/src/types/api/dashboard/variables/query.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export type Props = {
|
||||||
|
query: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PayloadProps = {
|
||||||
|
variableValues: string[] | number[];
|
||||||
|
};
|
@ -5,5 +5,6 @@ export interface MetricRangePayloadProps {
|
|||||||
data: {
|
data: {
|
||||||
result: QueryData[];
|
result: QueryData[];
|
||||||
resultType: string;
|
resultType: string;
|
||||||
|
variables: Record<string, unknown>;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -71,7 +71,8 @@ export type TraceFilterEnum =
|
|||||||
| 'serviceName'
|
| 'serviceName'
|
||||||
| 'status'
|
| 'status'
|
||||||
| 'responseStatusCode'
|
| 'responseStatusCode'
|
||||||
| 'rpcMethod';
|
| 'rpcMethod'
|
||||||
|
| 'traceID';
|
||||||
|
|
||||||
export const AllPanelHeading: {
|
export const AllPanelHeading: {
|
||||||
key: TraceFilterEnum;
|
key: TraceFilterEnum;
|
||||||
@ -125,4 +126,8 @@ export const AllPanelHeading: {
|
|||||||
key: 'status',
|
key: 'status',
|
||||||
displayValue: 'Status',
|
displayValue: 'Status',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'traceID',
|
||||||
|
displayValue: 'Trace ID',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
@ -94,6 +94,7 @@ type ClickHouseReader struct {
|
|||||||
logsResourceKeys string
|
logsResourceKeys string
|
||||||
queryEngine *promql.Engine
|
queryEngine *promql.Engine
|
||||||
remoteStorage *remote.Storage
|
remoteStorage *remote.Storage
|
||||||
|
fanoutStorage *storage.Storage
|
||||||
|
|
||||||
promConfigFile string
|
promConfigFile string
|
||||||
promConfig *config.Config
|
promConfig *config.Config
|
||||||
@ -143,7 +144,7 @@ func NewReader(localDB *sqlx.DB, configFile string) *ClickHouseReader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ClickHouseReader) Start() {
|
func (r *ClickHouseReader) Start(readerReady chan bool) {
|
||||||
logLevel := promlog.AllowedLevel{}
|
logLevel := promlog.AllowedLevel{}
|
||||||
logLevel.Set("debug")
|
logLevel.Set("debug")
|
||||||
// allowedFormat := promlog.AllowedFormat{}
|
// allowedFormat := promlog.AllowedFormat{}
|
||||||
@ -311,6 +312,8 @@ func (r *ClickHouseReader) Start() {
|
|||||||
}
|
}
|
||||||
r.queryEngine = queryEngine
|
r.queryEngine = queryEngine
|
||||||
r.remoteStorage = remoteStorage
|
r.remoteStorage = remoteStorage
|
||||||
|
r.fanoutStorage = &fanoutStorage
|
||||||
|
readerReady <- true
|
||||||
|
|
||||||
if err := g.Run(); err != nil {
|
if err := g.Run(); err != nil {
|
||||||
level.Error(logger).Log("err", err)
|
level.Error(logger).Log("err", err)
|
||||||
@ -319,6 +322,14 @@ func (r *ClickHouseReader) Start() {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *ClickHouseReader) GetQueryEngine() *promql.Engine {
|
||||||
|
return r.queryEngine
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ClickHouseReader) GetFanoutStorage() *storage.Storage {
|
||||||
|
return r.fanoutStorage
|
||||||
|
}
|
||||||
|
|
||||||
func reloadConfig(filename string, logger log.Logger, rls ...func(*config.Config) error) (promConfig *config.Config, err error) {
|
func reloadConfig(filename string, logger log.Logger, rls ...func(*config.Config) error) (promConfig *config.Config, err error) {
|
||||||
level.Info(logger).Log("msg", "Loading configuration file", "filename", filename)
|
level.Info(logger).Log("msg", "Loading configuration file", "filename", filename)
|
||||||
|
|
||||||
@ -925,6 +936,9 @@ func (r *ClickHouseReader) GetSpanFilters(ctx context.Context, queryParams *mode
|
|||||||
}
|
}
|
||||||
|
|
||||||
args := []interface{}{clickhouse.Named("timestampL", strconv.FormatInt(queryParams.Start.UnixNano(), 10)), clickhouse.Named("timestampU", strconv.FormatInt(queryParams.End.UnixNano(), 10))}
|
args := []interface{}{clickhouse.Named("timestampL", strconv.FormatInt(queryParams.Start.UnixNano(), 10)), clickhouse.Named("timestampU", strconv.FormatInt(queryParams.End.UnixNano(), 10))}
|
||||||
|
if len(queryParams.TraceID) > 0 {
|
||||||
|
args = buildFilterArrayQuery(ctx, excludeMap, queryParams.TraceID, constants.TraceID, &query, args)
|
||||||
|
}
|
||||||
if len(queryParams.ServiceName) > 0 {
|
if len(queryParams.ServiceName) > 0 {
|
||||||
args = buildFilterArrayQuery(ctx, excludeMap, queryParams.ServiceName, constants.ServiceName, &query, args)
|
args = buildFilterArrayQuery(ctx, excludeMap, queryParams.ServiceName, constants.ServiceName, &query, args)
|
||||||
}
|
}
|
||||||
@ -984,6 +998,8 @@ func (r *ClickHouseReader) GetSpanFilters(ctx context.Context, queryParams *mode
|
|||||||
|
|
||||||
for _, e := range queryParams.GetFilters {
|
for _, e := range queryParams.GetFilters {
|
||||||
switch e {
|
switch e {
|
||||||
|
case constants.TraceID:
|
||||||
|
continue
|
||||||
case constants.ServiceName:
|
case constants.ServiceName:
|
||||||
finalQuery := fmt.Sprintf("SELECT serviceName, count() as count FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU", r.traceDB, r.indexTable)
|
finalQuery := fmt.Sprintf("SELECT serviceName, count() as count FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU", r.traceDB, r.indexTable)
|
||||||
finalQuery += query
|
finalQuery += query
|
||||||
@ -1260,6 +1276,9 @@ func (r *ClickHouseReader) GetFilteredSpans(ctx context.Context, queryParams *mo
|
|||||||
|
|
||||||
var query string
|
var query string
|
||||||
args := []interface{}{clickhouse.Named("timestampL", strconv.FormatInt(queryParams.Start.UnixNano(), 10)), clickhouse.Named("timestampU", strconv.FormatInt(queryParams.End.UnixNano(), 10))}
|
args := []interface{}{clickhouse.Named("timestampL", strconv.FormatInt(queryParams.Start.UnixNano(), 10)), clickhouse.Named("timestampU", strconv.FormatInt(queryParams.End.UnixNano(), 10))}
|
||||||
|
if len(queryParams.TraceID) > 0 {
|
||||||
|
args = buildFilterArrayQuery(ctx, excludeMap, queryParams.TraceID, constants.TraceID, &query, args)
|
||||||
|
}
|
||||||
if len(queryParams.ServiceName) > 0 {
|
if len(queryParams.ServiceName) > 0 {
|
||||||
args = buildFilterArrayQuery(ctx, excludeMap, queryParams.ServiceName, constants.ServiceName, &query, args)
|
args = buildFilterArrayQuery(ctx, excludeMap, queryParams.ServiceName, constants.ServiceName, &query, args)
|
||||||
}
|
}
|
||||||
@ -1450,6 +1469,9 @@ func (r *ClickHouseReader) GetTagFilters(ctx context.Context, queryParams *model
|
|||||||
|
|
||||||
var query string
|
var query string
|
||||||
args := []interface{}{clickhouse.Named("timestampL", strconv.FormatInt(queryParams.Start.UnixNano(), 10)), clickhouse.Named("timestampU", strconv.FormatInt(queryParams.End.UnixNano(), 10))}
|
args := []interface{}{clickhouse.Named("timestampL", strconv.FormatInt(queryParams.Start.UnixNano(), 10)), clickhouse.Named("timestampU", strconv.FormatInt(queryParams.End.UnixNano(), 10))}
|
||||||
|
if len(queryParams.TraceID) > 0 {
|
||||||
|
args = buildFilterArrayQuery(ctx, excludeMap, queryParams.TraceID, constants.TraceID, &query, args)
|
||||||
|
}
|
||||||
if len(queryParams.ServiceName) > 0 {
|
if len(queryParams.ServiceName) > 0 {
|
||||||
args = buildFilterArrayQuery(ctx, excludeMap, queryParams.ServiceName, constants.ServiceName, &query, args)
|
args = buildFilterArrayQuery(ctx, excludeMap, queryParams.ServiceName, constants.ServiceName, &query, args)
|
||||||
}
|
}
|
||||||
@ -1546,6 +1568,9 @@ func (r *ClickHouseReader) GetTagValues(ctx context.Context, queryParams *model.
|
|||||||
|
|
||||||
var query string
|
var query string
|
||||||
args := []interface{}{clickhouse.Named("timestampL", strconv.FormatInt(queryParams.Start.UnixNano(), 10)), clickhouse.Named("timestampU", strconv.FormatInt(queryParams.End.UnixNano(), 10))}
|
args := []interface{}{clickhouse.Named("timestampL", strconv.FormatInt(queryParams.Start.UnixNano(), 10)), clickhouse.Named("timestampU", strconv.FormatInt(queryParams.End.UnixNano(), 10))}
|
||||||
|
if len(queryParams.TraceID) > 0 {
|
||||||
|
args = buildFilterArrayQuery(ctx, excludeMap, queryParams.TraceID, constants.TraceID, &query, args)
|
||||||
|
}
|
||||||
if len(queryParams.ServiceName) > 0 {
|
if len(queryParams.ServiceName) > 0 {
|
||||||
args = buildFilterArrayQuery(ctx, excludeMap, queryParams.ServiceName, constants.ServiceName, &query, args)
|
args = buildFilterArrayQuery(ctx, excludeMap, queryParams.ServiceName, constants.ServiceName, &query, args)
|
||||||
}
|
}
|
||||||
@ -1853,6 +1878,9 @@ func (r *ClickHouseReader) GetFilteredSpansAggregates(ctx context.Context, query
|
|||||||
query = fmt.Sprintf("SELECT toStartOfInterval(timestamp, INTERVAL %d minute) as time, %s FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU", queryParams.StepSeconds/60, aggregation_query, r.traceDB, r.indexTable)
|
query = fmt.Sprintf("SELECT toStartOfInterval(timestamp, INTERVAL %d minute) as time, %s FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU", queryParams.StepSeconds/60, aggregation_query, r.traceDB, r.indexTable)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(queryParams.TraceID) > 0 {
|
||||||
|
args = buildFilterArrayQuery(ctx, excludeMap, queryParams.TraceID, constants.TraceID, &query, args)
|
||||||
|
}
|
||||||
if len(queryParams.ServiceName) > 0 {
|
if len(queryParams.ServiceName) > 0 {
|
||||||
args = buildFilterArrayQuery(ctx, excludeMap, queryParams.ServiceName, constants.ServiceName, &query, args)
|
args = buildFilterArrayQuery(ctx, excludeMap, queryParams.ServiceName, constants.ServiceName, &query, args)
|
||||||
}
|
}
|
||||||
@ -2813,7 +2841,7 @@ func (r *ClickHouseReader) GetMetricResult(ctx context.Context, query string) ([
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
zap.S().Debug("Error in processing query: ", err)
|
zap.S().Debug("Error in processing query: ", err)
|
||||||
return nil, fmt.Errorf("error in processing query")
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -3239,3 +3267,39 @@ func (r *ClickHouseReader) AggregateLogs(ctx context.Context, params *model.Logs
|
|||||||
|
|
||||||
return &aggregateResponse, nil
|
return &aggregateResponse, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *ClickHouseReader) QueryDashboardVars(ctx context.Context, query string) (*model.DashboardVar, error) {
|
||||||
|
var result model.DashboardVar
|
||||||
|
rows, err := r.db.Query(ctx, query)
|
||||||
|
|
||||||
|
zap.S().Info(query)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
zap.S().Debug("Error in processing sql query: ", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
columnTypes = rows.ColumnTypes()
|
||||||
|
vars = make([]interface{}, len(columnTypes))
|
||||||
|
)
|
||||||
|
for i := range columnTypes {
|
||||||
|
vars[i] = reflect.New(columnTypes[i].ScanType()).Interface()
|
||||||
|
}
|
||||||
|
|
||||||
|
defer rows.Close()
|
||||||
|
for rows.Next() {
|
||||||
|
if err := rows.Scan(vars...); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, v := range vars {
|
||||||
|
switch v := v.(type) {
|
||||||
|
case *string, *int8, *int16, *int32, *int64, *uint8, *uint16, *uint32, *uint64, *float32, *float64, *time.Time, *bool:
|
||||||
|
result.VariableValues = append(result.VariableValues, reflect.ValueOf(v).Elem().Interface())
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported value type encountered")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
@ -9,7 +9,9 @@ import (
|
|||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"text/template"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
@ -320,6 +322,7 @@ func (aH *APIHandler) RegisterRoutes(router *mux.Router) {
|
|||||||
router.HandleFunc("/api/v1/dashboards/{uuid}", ViewAccess(aH.getDashboard)).Methods(http.MethodGet)
|
router.HandleFunc("/api/v1/dashboards/{uuid}", ViewAccess(aH.getDashboard)).Methods(http.MethodGet)
|
||||||
router.HandleFunc("/api/v1/dashboards/{uuid}", EditAccess(aH.updateDashboard)).Methods(http.MethodPut)
|
router.HandleFunc("/api/v1/dashboards/{uuid}", EditAccess(aH.updateDashboard)).Methods(http.MethodPut)
|
||||||
router.HandleFunc("/api/v1/dashboards/{uuid}", EditAccess(aH.deleteDashboard)).Methods(http.MethodDelete)
|
router.HandleFunc("/api/v1/dashboards/{uuid}", EditAccess(aH.deleteDashboard)).Methods(http.MethodDelete)
|
||||||
|
router.HandleFunc("/api/v1/variables/query", ViewAccess(aH.queryDashboardVars)).Methods(http.MethodGet)
|
||||||
|
|
||||||
router.HandleFunc("/api/v1/feedback", OpenAccess(aH.submitFeedback)).Methods(http.MethodPost)
|
router.HandleFunc("/api/v1/feedback", OpenAccess(aH.submitFeedback)).Methods(http.MethodPost)
|
||||||
// router.HandleFunc("/api/v1/get_percentiles", aH.getApplicationPercentiles).Methods(http.MethodGet)
|
// router.HandleFunc("/api/v1/get_percentiles", aH.getApplicationPercentiles).Methods(http.MethodGet)
|
||||||
@ -483,9 +486,11 @@ func (aH *APIHandler) queryRangeMetricsV2(w http.ResponseWriter, r *http.Request
|
|||||||
type channelResult struct {
|
type channelResult struct {
|
||||||
Series []*model.Series
|
Series []*model.Series
|
||||||
Err error
|
Err error
|
||||||
|
Name string
|
||||||
|
Query string
|
||||||
}
|
}
|
||||||
|
|
||||||
execClickHouseQueries := func(queries map[string]string) ([]*model.Series, error) {
|
execClickHouseQueries := func(queries map[string]string) ([]*model.Series, error, map[string]string) {
|
||||||
var seriesList []*model.Series
|
var seriesList []*model.Series
|
||||||
ch := make(chan channelResult, len(queries))
|
ch := make(chan channelResult, len(queries))
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
@ -500,7 +505,7 @@ func (aH *APIHandler) queryRangeMetricsV2(w http.ResponseWriter, r *http.Request
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ch <- channelResult{Err: fmt.Errorf("error in query-%s: %v", name, err)}
|
ch <- channelResult{Err: fmt.Errorf("error in query-%s: %v", name, err), Name: name, Query: query}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ch <- channelResult{Series: seriesList}
|
ch <- channelResult{Series: seriesList}
|
||||||
@ -511,21 +516,23 @@ func (aH *APIHandler) queryRangeMetricsV2(w http.ResponseWriter, r *http.Request
|
|||||||
close(ch)
|
close(ch)
|
||||||
|
|
||||||
var errs []error
|
var errs []error
|
||||||
|
errQuriesByName := make(map[string]string)
|
||||||
// read values from the channel
|
// read values from the channel
|
||||||
for r := range ch {
|
for r := range ch {
|
||||||
if r.Err != nil {
|
if r.Err != nil {
|
||||||
errs = append(errs, r.Err)
|
errs = append(errs, r.Err)
|
||||||
|
errQuriesByName[r.Name] = r.Query
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
seriesList = append(seriesList, r.Series...)
|
seriesList = append(seriesList, r.Series...)
|
||||||
}
|
}
|
||||||
if len(errs) != 0 {
|
if len(errs) != 0 {
|
||||||
return nil, fmt.Errorf("encountered multiple errors: %s", metrics.FormatErrs(errs, "\n"))
|
return nil, fmt.Errorf("encountered multiple errors: %s", metrics.FormatErrs(errs, "\n")), errQuriesByName
|
||||||
}
|
}
|
||||||
return seriesList, nil
|
return seriesList, nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
execPromQueries := func(metricsQueryRangeParams *model.QueryRangeParamsV2) ([]*model.Series, error) {
|
execPromQueries := func(metricsQueryRangeParams *model.QueryRangeParamsV2) ([]*model.Series, error, map[string]string) {
|
||||||
var seriesList []*model.Series
|
var seriesList []*model.Series
|
||||||
ch := make(chan channelResult, len(metricsQueryRangeParams.CompositeMetricQuery.PromQueries))
|
ch := make(chan channelResult, len(metricsQueryRangeParams.CompositeMetricQuery.PromQueries))
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
@ -538,6 +545,19 @@ func (aH *APIHandler) queryRangeMetricsV2(w http.ResponseWriter, r *http.Request
|
|||||||
go func(name string, query *model.PromQuery) {
|
go func(name string, query *model.PromQuery) {
|
||||||
var seriesList []*model.Series
|
var seriesList []*model.Series
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
|
tmpl := template.New("promql-query")
|
||||||
|
tmpl, tmplErr := tmpl.Parse(query.Query)
|
||||||
|
if tmplErr != nil {
|
||||||
|
ch <- channelResult{Err: fmt.Errorf("error in parsing query-%s: %v", name, tmplErr), Name: name, Query: query.Query}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var queryBuf bytes.Buffer
|
||||||
|
tmplErr = tmpl.Execute(&queryBuf, metricsQueryRangeParams.Variables)
|
||||||
|
if tmplErr != nil {
|
||||||
|
ch <- channelResult{Err: fmt.Errorf("error in parsing query-%s: %v", name, tmplErr), Name: name, Query: query.Query}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
query.Query = queryBuf.String()
|
||||||
queryModel := model.QueryRangeParams{
|
queryModel := model.QueryRangeParams{
|
||||||
Start: time.UnixMilli(metricsQueryRangeParams.Start),
|
Start: time.UnixMilli(metricsQueryRangeParams.Start),
|
||||||
End: time.UnixMilli(metricsQueryRangeParams.End),
|
End: time.UnixMilli(metricsQueryRangeParams.End),
|
||||||
@ -546,7 +566,7 @@ func (aH *APIHandler) queryRangeMetricsV2(w http.ResponseWriter, r *http.Request
|
|||||||
}
|
}
|
||||||
promResult, _, err := (*aH.reader).GetQueryRangeResult(r.Context(), &queryModel)
|
promResult, _, err := (*aH.reader).GetQueryRangeResult(r.Context(), &queryModel)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ch <- channelResult{Err: fmt.Errorf("error in query-%s: %v", name, err)}
|
ch <- channelResult{Err: fmt.Errorf("error in query-%s: %v", name, err), Name: name, Query: query.Query}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
matrix, _ := promResult.Matrix()
|
matrix, _ := promResult.Matrix()
|
||||||
@ -567,22 +587,25 @@ func (aH *APIHandler) queryRangeMetricsV2(w http.ResponseWriter, r *http.Request
|
|||||||
close(ch)
|
close(ch)
|
||||||
|
|
||||||
var errs []error
|
var errs []error
|
||||||
|
errQuriesByName := make(map[string]string)
|
||||||
// read values from the channel
|
// read values from the channel
|
||||||
for r := range ch {
|
for r := range ch {
|
||||||
if r.Err != nil {
|
if r.Err != nil {
|
||||||
errs = append(errs, r.Err)
|
errs = append(errs, r.Err)
|
||||||
|
errQuriesByName[r.Name] = r.Query
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
seriesList = append(seriesList, r.Series...)
|
seriesList = append(seriesList, r.Series...)
|
||||||
}
|
}
|
||||||
if len(errs) != 0 {
|
if len(errs) != 0 {
|
||||||
return nil, fmt.Errorf("encountered multiple errors: %s", metrics.FormatErrs(errs, "\n"))
|
return nil, fmt.Errorf("encountered multiple errors: %s", metrics.FormatErrs(errs, "\n")), errQuriesByName
|
||||||
}
|
}
|
||||||
return seriesList, nil
|
return seriesList, nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var seriesList []*model.Series
|
var seriesList []*model.Series
|
||||||
var err error
|
var err error
|
||||||
|
var errQuriesByName map[string]string
|
||||||
switch metricsQueryRangeParams.CompositeMetricQuery.QueryType {
|
switch metricsQueryRangeParams.CompositeMetricQuery.QueryType {
|
||||||
case model.QUERY_BUILDER:
|
case model.QUERY_BUILDER:
|
||||||
runQueries := metrics.PrepareBuilderMetricQueries(metricsQueryRangeParams, constants.SIGNOZ_TIMESERIES_TABLENAME)
|
runQueries := metrics.PrepareBuilderMetricQueries(metricsQueryRangeParams, constants.SIGNOZ_TIMESERIES_TABLENAME)
|
||||||
@ -590,7 +613,7 @@ func (aH *APIHandler) queryRangeMetricsV2(w http.ResponseWriter, r *http.Request
|
|||||||
respondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: runQueries.Err}, nil)
|
respondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: runQueries.Err}, nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
seriesList, err = execClickHouseQueries(runQueries.Queries)
|
seriesList, err, errQuriesByName = execClickHouseQueries(runQueries.Queries)
|
||||||
|
|
||||||
case model.CLICKHOUSE:
|
case model.CLICKHOUSE:
|
||||||
queries := make(map[string]string)
|
queries := make(map[string]string)
|
||||||
@ -598,20 +621,32 @@ func (aH *APIHandler) queryRangeMetricsV2(w http.ResponseWriter, r *http.Request
|
|||||||
if chQuery.Disabled {
|
if chQuery.Disabled {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
queries[name] = chQuery.Query
|
tmpl := template.New("clickhouse-query")
|
||||||
|
tmpl, err := tmpl.Parse(chQuery.Query)
|
||||||
|
if err != nil {
|
||||||
|
respondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var query bytes.Buffer
|
||||||
|
err = tmpl.Execute(&query, metricsQueryRangeParams.Variables)
|
||||||
|
if err != nil {
|
||||||
|
respondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
queries[name] = query.String()
|
||||||
}
|
}
|
||||||
seriesList, err = execClickHouseQueries(queries)
|
seriesList, err, errQuriesByName = execClickHouseQueries(queries)
|
||||||
case model.PROM:
|
case model.PROM:
|
||||||
seriesList, err = execPromQueries(metricsQueryRangeParams)
|
seriesList, err, errQuriesByName = execPromQueries(metricsQueryRangeParams)
|
||||||
default:
|
default:
|
||||||
err = fmt.Errorf("invalid query type")
|
err = fmt.Errorf("invalid query type")
|
||||||
respondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil)
|
respondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, errQuriesByName)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
apiErrObj := &model.ApiError{Typ: model.ErrorBadData, Err: err}
|
apiErrObj := &model.ApiError{Typ: model.ErrorBadData, Err: err}
|
||||||
respondError(w, apiErrObj, nil)
|
respondError(w, apiErrObj, errQuriesByName)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if metricsQueryRangeParams.CompositeMetricQuery.PanelType == model.QUERY_VALUE &&
|
if metricsQueryRangeParams.CompositeMetricQuery.PanelType == model.QUERY_VALUE &&
|
||||||
@ -707,6 +742,25 @@ func (aH *APIHandler) deleteDashboard(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (aH *APIHandler) queryDashboardVars(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
|
query := r.URL.Query().Get("query")
|
||||||
|
if query == "" {
|
||||||
|
respondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("query is required")}, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if strings.Contains(strings.ToLower(query), "alter table") {
|
||||||
|
respondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("query shouldn't alter data")}, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dashboardVars, err := (*aH.reader).QueryDashboardVars(r.Context(), query)
|
||||||
|
if err != nil {
|
||||||
|
respondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
aH.respond(w, dashboardVars)
|
||||||
|
}
|
||||||
|
|
||||||
func (aH *APIHandler) updateDashboard(w http.ResponseWriter, r *http.Request) {
|
func (aH *APIHandler) updateDashboard(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
uuid := mux.Vars(r)["uuid"]
|
uuid := mux.Vars(r)["uuid"]
|
||||||
@ -1034,11 +1088,11 @@ func (aH *APIHandler) queryRangeMetrics(w http.ResponseWriter, r *http.Request)
|
|||||||
if res.Err != nil {
|
if res.Err != nil {
|
||||||
switch res.Err.(type) {
|
switch res.Err.(type) {
|
||||||
case promql.ErrQueryCanceled:
|
case promql.ErrQueryCanceled:
|
||||||
respondError(w, &model.ApiError{model.ErrorCanceled, res.Err}, nil)
|
respondError(w, &model.ApiError{Typ: model.ErrorCanceled, Err: res.Err}, nil)
|
||||||
case promql.ErrQueryTimeout:
|
case promql.ErrQueryTimeout:
|
||||||
respondError(w, &model.ApiError{model.ErrorTimeout, res.Err}, nil)
|
respondError(w, &model.ApiError{Typ: model.ErrorTimeout, Err: res.Err}, nil)
|
||||||
}
|
}
|
||||||
respondError(w, &model.ApiError{model.ErrorExec, res.Err}, nil)
|
respondError(w, &model.ApiError{Typ: model.ErrorExec, Err: res.Err}, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
response_data := &model.QueryData{
|
response_data := &model.QueryData{
|
||||||
@ -1088,11 +1142,11 @@ func (aH *APIHandler) queryMetrics(w http.ResponseWriter, r *http.Request) {
|
|||||||
if res.Err != nil {
|
if res.Err != nil {
|
||||||
switch res.Err.(type) {
|
switch res.Err.(type) {
|
||||||
case promql.ErrQueryCanceled:
|
case promql.ErrQueryCanceled:
|
||||||
respondError(w, &model.ApiError{model.ErrorCanceled, res.Err}, nil)
|
respondError(w, &model.ApiError{Typ: model.ErrorCanceled, Err: res.Err}, nil)
|
||||||
case promql.ErrQueryTimeout:
|
case promql.ErrQueryTimeout:
|
||||||
respondError(w, &model.ApiError{model.ErrorTimeout, res.Err}, nil)
|
respondError(w, &model.ApiError{Typ: model.ErrorTimeout, Err: res.Err}, nil)
|
||||||
}
|
}
|
||||||
respondError(w, &model.ApiError{model.ErrorExec, res.Err}, nil)
|
respondError(w, &model.ApiError{Typ: model.ErrorExec, Err: res.Err}, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
response_data := &model.QueryData{
|
response_data := &model.QueryData{
|
||||||
|
@ -8,6 +8,7 @@ import (
|
|||||||
"github.com/SigNoz/govaluate"
|
"github.com/SigNoz/govaluate"
|
||||||
"go.signoz.io/query-service/constants"
|
"go.signoz.io/query-service/constants"
|
||||||
"go.signoz.io/query-service/model"
|
"go.signoz.io/query-service/model"
|
||||||
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
type RunQueries struct {
|
type RunQueries struct {
|
||||||
@ -50,8 +51,8 @@ func GoValuateFuncs() map[string]govaluate.ExpressionFunction {
|
|||||||
return GoValuateFuncs
|
return GoValuateFuncs
|
||||||
}
|
}
|
||||||
|
|
||||||
// formattedValue formats the value to be used in clickhouse query
|
// FormattedValue formats the value to be used in clickhouse query
|
||||||
func formattedValue(v interface{}) string {
|
func FormattedValue(v interface{}) string {
|
||||||
switch x := v.(type) {
|
switch x := v.(type) {
|
||||||
case int:
|
case int:
|
||||||
return fmt.Sprintf("%d", x)
|
return fmt.Sprintf("%d", x)
|
||||||
@ -62,6 +63,9 @@ func formattedValue(v interface{}) string {
|
|||||||
case bool:
|
case bool:
|
||||||
return fmt.Sprintf("%v", x)
|
return fmt.Sprintf("%v", x)
|
||||||
case []interface{}:
|
case []interface{}:
|
||||||
|
if len(x) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
switch x[0].(type) {
|
switch x[0].(type) {
|
||||||
case string:
|
case string:
|
||||||
str := "["
|
str := "["
|
||||||
@ -75,10 +79,12 @@ func formattedValue(v interface{}) string {
|
|||||||
return str
|
return str
|
||||||
case int, float32, float64, bool:
|
case int, float32, float64, bool:
|
||||||
return strings.Join(strings.Fields(fmt.Sprint(x)), ",")
|
return strings.Join(strings.Fields(fmt.Sprint(x)), ",")
|
||||||
|
default:
|
||||||
|
zap.L().Error("invalid type for formatted value", zap.Any("type", reflect.TypeOf(x[0])))
|
||||||
|
return ""
|
||||||
}
|
}
|
||||||
return ""
|
|
||||||
default:
|
default:
|
||||||
// may be log the warning here?
|
zap.L().Error("invalid type for formatted value", zap.Any("type", reflect.TypeOf(x)))
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -87,7 +93,7 @@ func formattedValue(v interface{}) string {
|
|||||||
// timeseries based on search criteria
|
// timeseries based on search criteria
|
||||||
func BuildMetricsTimeSeriesFilterQuery(fs *model.FilterSet, groupTags []string, metricName string, aggregateOperator model.AggregateOperator) (string, error) {
|
func BuildMetricsTimeSeriesFilterQuery(fs *model.FilterSet, groupTags []string, metricName string, aggregateOperator model.AggregateOperator) (string, error) {
|
||||||
var conditions []string
|
var conditions []string
|
||||||
conditions = append(conditions, fmt.Sprintf("metric_name = %s", formattedValue(metricName)))
|
conditions = append(conditions, fmt.Sprintf("metric_name = %s", FormattedValue(metricName)))
|
||||||
if fs != nil && len(fs.Items) != 0 {
|
if fs != nil && len(fs.Items) != 0 {
|
||||||
for _, item := range fs.Items {
|
for _, item := range fs.Items {
|
||||||
toFormat := item.Value
|
toFormat := item.Value
|
||||||
@ -102,7 +108,7 @@ func BuildMetricsTimeSeriesFilterQuery(fs *model.FilterSet, groupTags []string,
|
|||||||
toFormat = x[0]
|
toFormat = x[0]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fmtVal := formattedValue(toFormat)
|
fmtVal := FormattedValue(toFormat)
|
||||||
switch op {
|
switch op {
|
||||||
case "eq":
|
case "eq":
|
||||||
conditions = append(conditions, fmt.Sprintf("labels_object.%s = %s", item.Key, fmtVal))
|
conditions = append(conditions, fmt.Sprintf("labels_object.%s = %s", item.Key, fmtVal))
|
||||||
@ -152,7 +158,7 @@ func BuildMetricQuery(qp *model.QueryRangeParamsV2, mq *model.MetricQuery, table
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
samplesTableTimeFilter := fmt.Sprintf("metric_name = %s AND timestamp_ms >= %d AND timestamp_ms <= %d", formattedValue(mq.MetricName), qp.Start, qp.End)
|
samplesTableTimeFilter := fmt.Sprintf("metric_name = %s AND timestamp_ms >= %d AND timestamp_ms <= %d", FormattedValue(mq.MetricName), qp.Start, qp.End)
|
||||||
|
|
||||||
// Select the aggregate value for interval
|
// Select the aggregate value for interval
|
||||||
queryTmpl :=
|
queryTmpl :=
|
||||||
@ -419,3 +425,31 @@ func PrepareBuilderMetricQueries(qp *model.QueryRangeParamsV2, tableName string)
|
|||||||
}
|
}
|
||||||
return &RunQueries{Queries: namedQueries}
|
return &RunQueries{Queries: namedQueries}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PromFormattedValue formats the value to be used in promql
|
||||||
|
func PromFormattedValue(v interface{}) string {
|
||||||
|
switch x := v.(type) {
|
||||||
|
case int:
|
||||||
|
return fmt.Sprintf("%d", x)
|
||||||
|
case float32, float64:
|
||||||
|
return fmt.Sprintf("%f", x)
|
||||||
|
case string:
|
||||||
|
return fmt.Sprintf("%s", x)
|
||||||
|
case bool:
|
||||||
|
return fmt.Sprintf("%v", x)
|
||||||
|
case []interface{}:
|
||||||
|
if len(x) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
switch x[0].(type) {
|
||||||
|
case string, int, float32, float64, bool:
|
||||||
|
return strings.Trim(strings.Join(strings.Fields(fmt.Sprint(x)), "|"), "[]")
|
||||||
|
default:
|
||||||
|
zap.L().Error("invalid type for prom formatted value", zap.Any("type", reflect.TypeOf(x[0])))
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
zap.L().Error("invalid type for prom formatted value", zap.Any("type", reflect.TypeOf(x)))
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"go.signoz.io/query-service/app/metrics"
|
"go.signoz.io/query-service/app/metrics"
|
||||||
"go.signoz.io/query-service/model"
|
"go.signoz.io/query-service/model"
|
||||||
@ -36,6 +37,44 @@ func ParseMetricQueryRangeParams(r *http.Request) (*model.QueryRangeParamsV2, *m
|
|||||||
if err := validateQueryRangeParamsV2(postData); err != nil {
|
if err := validateQueryRangeParamsV2(postData); err != nil {
|
||||||
return nil, &model.ApiError{Typ: model.ErrorBadData, Err: err}
|
return nil, &model.ApiError{Typ: model.ErrorBadData, Err: err}
|
||||||
}
|
}
|
||||||
|
// prepare the variables for the corrspnding query type
|
||||||
|
formattedVars := make(map[string]interface{})
|
||||||
|
for name, value := range postData.Variables {
|
||||||
|
if postData.CompositeMetricQuery.QueryType == model.PROM {
|
||||||
|
formattedVars[name] = metrics.PromFormattedValue(value)
|
||||||
|
} else if postData.CompositeMetricQuery.QueryType == model.CLICKHOUSE {
|
||||||
|
formattedVars[name] = metrics.FormattedValue(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// replace the variables in metrics builder filter item with actual value
|
||||||
|
if postData.CompositeMetricQuery.QueryType == model.QUERY_BUILDER {
|
||||||
|
for _, query := range postData.CompositeMetricQuery.BuilderQueries {
|
||||||
|
for idx := range query.TagFilters.Items {
|
||||||
|
item := &query.TagFilters.Items[idx]
|
||||||
|
value := item.Value
|
||||||
|
if value != nil {
|
||||||
|
switch x := value.(type) {
|
||||||
|
case string:
|
||||||
|
variableName := strings.Trim(x, "{{ . }}")
|
||||||
|
if _, ok := postData.Variables[variableName]; ok {
|
||||||
|
item.Value = postData.Variables[variableName]
|
||||||
|
}
|
||||||
|
case []interface{}:
|
||||||
|
if len(x) > 0 {
|
||||||
|
switch x[0].(type) {
|
||||||
|
case string:
|
||||||
|
variableName := strings.Trim(x[0].(string), "{{ . }}")
|
||||||
|
if _, ok := postData.Variables[variableName]; ok {
|
||||||
|
item.Value = postData.Variables[variableName]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
postData.Variables = formattedVars
|
||||||
|
|
||||||
return postData, nil
|
return postData, nil
|
||||||
}
|
}
|
||||||
|
@ -77,18 +77,20 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
localDB.SetMaxOpenConns(10)
|
localDB.SetMaxOpenConns(10)
|
||||||
|
readerReady := make(chan bool)
|
||||||
|
|
||||||
var reader interfaces.Reader
|
var reader interfaces.Reader
|
||||||
storage := os.Getenv("STORAGE")
|
storage := os.Getenv("STORAGE")
|
||||||
if storage == "clickhouse" {
|
if storage == "clickhouse" {
|
||||||
zap.S().Info("Using ClickHouse as datastore ...")
|
zap.S().Info("Using ClickHouse as datastore ...")
|
||||||
clickhouseReader := clickhouseReader.NewReader(localDB, serverOptions.PromConfigPath)
|
clickhouseReader := clickhouseReader.NewReader(localDB, serverOptions.PromConfigPath)
|
||||||
go clickhouseReader.Start()
|
go clickhouseReader.Start(readerReady)
|
||||||
reader = clickhouseReader
|
reader = clickhouseReader
|
||||||
} else {
|
} else {
|
||||||
return nil, fmt.Errorf("Storage type: %s is not supported in query service", storage)
|
return nil, fmt.Errorf("Storage type: %s is not supported in query service", storage)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<-readerReady
|
||||||
rm, err := makeRulesManager(serverOptions.PromConfigPath, constants.GetAlertManagerApiPrefix(), serverOptions.RuleRepoURL, localDB, reader, serverOptions.DisableRules)
|
rm, err := makeRulesManager(serverOptions.PromConfigPath, constants.GetAlertManagerApiPrefix(), serverOptions.RuleRepoURL, localDB, reader, serverOptions.DisableRules)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -232,9 +234,10 @@ func (s *Server) analyticsMiddleware(next http.Handler) http.Handler {
|
|||||||
next.ServeHTTP(lrw, r)
|
next.ServeHTTP(lrw, r)
|
||||||
|
|
||||||
data := map[string]interface{}{"path": path, "statusCode": lrw.statusCode}
|
data := map[string]interface{}{"path": path, "statusCode": lrw.statusCode}
|
||||||
|
if telemetry.GetInstance().IsSampled() {
|
||||||
if _, ok := telemetry.IgnoredPaths()[path]; !ok {
|
if _, ok := telemetry.IgnoredPaths()[path]; !ok {
|
||||||
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_PATH, data)
|
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_PATH, data)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
@ -361,7 +364,7 @@ func makeRulesManager(
|
|||||||
disableRules bool) (*rules.Manager, error) {
|
disableRules bool) (*rules.Manager, error) {
|
||||||
|
|
||||||
// create engine
|
// create engine
|
||||||
pqle, err := pqle.FromConfigPath(promConfigPath)
|
pqle, err := pqle.FromReader(ch)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create pql engine : %v", err)
|
return nil, fmt.Errorf("failed to create pql engine : %v", err)
|
||||||
}
|
}
|
||||||
|
@ -19,8 +19,7 @@ rule_files:
|
|||||||
|
|
||||||
# A scrape configuration containing exactly one endpoint to scrape:
|
# A scrape configuration containing exactly one endpoint to scrape:
|
||||||
# Here it's Prometheus itself.
|
# Here it's Prometheus itself.
|
||||||
scrape_configs:
|
scrape_configs: []
|
||||||
|
|
||||||
|
|
||||||
remote_read:
|
remote_read:
|
||||||
- url: tcp://localhost:9000/?database=signoz_metrics
|
- url: tcp://localhost:9000/?database=signoz_metrics
|
||||||
|
@ -41,6 +41,7 @@ var AmChannelApiPath = GetOrDefaultEnv("ALERTMANAGER_API_CHANNEL_PATH", "v1/rout
|
|||||||
var RELATIONAL_DATASOURCE_PATH = GetOrDefaultEnv("SIGNOZ_LOCAL_DB_PATH", "/var/lib/signoz/signoz.db")
|
var RELATIONAL_DATASOURCE_PATH = GetOrDefaultEnv("SIGNOZ_LOCAL_DB_PATH", "/var/lib/signoz/signoz.db")
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
TraceID = "traceID"
|
||||||
ServiceName = "serviceName"
|
ServiceName = "serviceName"
|
||||||
HttpRoute = "httpRoute"
|
HttpRoute = "httpRoute"
|
||||||
HttpCode = "httpCode"
|
HttpCode = "httpCode"
|
||||||
|
@ -34,6 +34,7 @@ require (
|
|||||||
github.com/minio/md5-simd v1.1.0 // indirect
|
github.com/minio/md5-simd v1.1.0 // indirect
|
||||||
github.com/minio/sha256-simd v0.1.1 // indirect
|
github.com/minio/sha256-simd v0.1.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/posthog/posthog-go v0.0.0-20220817142604-0b0bbf0f9c0f // indirect
|
||||||
gopkg.in/ini.v1 v1.42.0 // indirect
|
gopkg.in/ini.v1 v1.42.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
||||||
)
|
)
|
||||||
|
@ -93,6 +93,7 @@ github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWH
|
|||||||
github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||||
github.com/cockroachdb/cmux v0.0.0-20170110192607-30d10be49292/go.mod h1:qRiX68mZX1lGBkTWyp3CLcenw9I94W2dLeRvMzcn9N4=
|
github.com/cockroachdb/cmux v0.0.0-20170110192607-30d10be49292/go.mod h1:qRiX68mZX1lGBkTWyp3CLcenw9I94W2dLeRvMzcn9N4=
|
||||||
github.com/cockroachdb/cockroach v0.0.0-20170608034007-84bc9597164f/go.mod h1:xeT/CQ0qZHangbYbWShlCGAx31aV4AjGswDUjhKS6HQ=
|
github.com/cockroachdb/cockroach v0.0.0-20170608034007-84bc9597164f/go.mod h1:xeT/CQ0qZHangbYbWShlCGAx31aV4AjGswDUjhKS6HQ=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
@ -378,6 +379,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
|||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/posthog/posthog-go v0.0.0-20220817142604-0b0bbf0f9c0f h1:h0p1aZ9F5d6IXOygysob3g4B07b+HuVUQC0VJKD8wA4=
|
||||||
|
github.com/posthog/posthog-go v0.0.0-20220817142604-0b0bbf0f9c0f/go.mod h1:oa2sAs9tGai3VldabTV0eWejt/O4/OOD7azP8GaikqU=
|
||||||
github.com/prometheus/client_golang v0.9.0-pre1.0.20181001174001-0a8115f42e03 h1:hqNopISksxji/N5zEy1xMN7TrnSyVG/LymiwnkXi6/Q=
|
github.com/prometheus/client_golang v0.9.0-pre1.0.20181001174001-0a8115f42e03 h1:hqNopISksxji/N5zEy1xMN7TrnSyVG/LymiwnkXi6/Q=
|
||||||
github.com/prometheus/client_golang v0.9.0-pre1.0.20181001174001-0a8115f42e03/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
github.com/prometheus/client_golang v0.9.0-pre1.0.20181001174001-0a8115f42e03/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM=
|
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM=
|
||||||
@ -393,6 +396,7 @@ github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6L
|
|||||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik=
|
github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik=
|
||||||
github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
|
github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
|
||||||
|
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/samuel/go-zookeeper v0.0.0-20161028232340-1d7be4effb13/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
|
github.com/samuel/go-zookeeper v0.0.0-20161028232340-1d7be4effb13/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
|
||||||
github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da h1:p3Vo3i64TCLY7gIfzeQaUJ+kppEO5WQG3cL8iE8tGHU=
|
github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da h1:p3Vo3i64TCLY7gIfzeQaUJ+kppEO5WQG3cL8iE8tGHU=
|
||||||
github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
|
github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
|
||||||
@ -406,6 +410,7 @@ github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q
|
|||||||
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
|
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
|
||||||
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||||
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
|
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
|
||||||
|
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||||
github.com/shurcooL/vfsgen v0.0.0-20180711163814-62bca832be04/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw=
|
github.com/shurcooL/vfsgen v0.0.0-20180711163814-62bca832be04/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw=
|
||||||
github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo=
|
github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo=
|
||||||
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
|
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
|
||||||
@ -433,6 +438,7 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
|||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
|
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||||
github.com/urfave/negroni v1.0.0 h1:kIimOitoypq34K7TG7DUaJ9kq/N4Ofuwi1sjz0KipXc=
|
github.com/urfave/negroni v1.0.0 h1:kIimOitoypq34K7TG7DUaJ9kq/N4Ofuwi1sjz0KipXc=
|
||||||
github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4=
|
github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4=
|
||||||
github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c h1:3lbZUMbMiGUW/LMkfsEABsc5zNT9+b1CvsJx47JzJ8g=
|
github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c h1:3lbZUMbMiGUW/LMkfsEABsc5zNT9+b1CvsJx47JzJ8g=
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
|
|
||||||
"github.com/ClickHouse/clickhouse-go/v2"
|
"github.com/ClickHouse/clickhouse-go/v2"
|
||||||
"github.com/prometheus/prometheus/promql"
|
"github.com/prometheus/prometheus/promql"
|
||||||
|
"github.com/prometheus/prometheus/storage"
|
||||||
"github.com/prometheus/prometheus/util/stats"
|
"github.com/prometheus/prometheus/util/stats"
|
||||||
am "go.signoz.io/query-service/integrations/alertManager"
|
am "go.signoz.io/query-service/integrations/alertManager"
|
||||||
"go.signoz.io/query-service/model"
|
"go.signoz.io/query-service/model"
|
||||||
@ -70,4 +71,8 @@ type Reader interface {
|
|||||||
|
|
||||||
// Connection needed for rules, not ideal but required
|
// Connection needed for rules, not ideal but required
|
||||||
GetConn() clickhouse.Conn
|
GetConn() clickhouse.Conn
|
||||||
|
GetQueryEngine() *promql.Engine
|
||||||
|
GetFanoutStorage() *storage.Storage
|
||||||
|
|
||||||
|
QueryDashboardVars(ctx context.Context, query string) (*model.DashboardVar, error)
|
||||||
}
|
}
|
||||||
|
@ -118,11 +118,12 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type QueryRangeParamsV2 struct {
|
type QueryRangeParamsV2 struct {
|
||||||
DataSource DataSource `json:"dataSource"`
|
DataSource DataSource `json:"dataSource"`
|
||||||
Start int64 `json:"start"`
|
Start int64 `json:"start"`
|
||||||
End int64 `json:"end"`
|
End int64 `json:"end"`
|
||||||
Step int64 `json:"step"`
|
Step int64 `json:"step"`
|
||||||
CompositeMetricQuery *CompositeMetricQuery `json:"compositeMetricQuery"`
|
CompositeMetricQuery *CompositeMetricQuery `json:"compositeMetricQuery"`
|
||||||
|
Variables map[string]interface{} `json:"variables,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Metric auto complete types
|
// Metric auto complete types
|
||||||
@ -181,6 +182,7 @@ type TagQuery struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type GetFilteredSpansParams struct {
|
type GetFilteredSpansParams struct {
|
||||||
|
TraceID []string `json:"traceID"`
|
||||||
ServiceName []string `json:"serviceName"`
|
ServiceName []string `json:"serviceName"`
|
||||||
Operation []string `json:"operation"`
|
Operation []string `json:"operation"`
|
||||||
Kind string `json:"kind"`
|
Kind string `json:"kind"`
|
||||||
@ -208,6 +210,7 @@ type GetFilteredSpansParams struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type GetFilteredSpanAggregatesParams struct {
|
type GetFilteredSpanAggregatesParams struct {
|
||||||
|
TraceID []string `json:"traceID"`
|
||||||
ServiceName []string `json:"serviceName"`
|
ServiceName []string `json:"serviceName"`
|
||||||
Operation []string `json:"operation"`
|
Operation []string `json:"operation"`
|
||||||
Kind string `json:"kind"`
|
Kind string `json:"kind"`
|
||||||
@ -236,6 +239,7 @@ type GetFilteredSpanAggregatesParams struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type SpanFilterParams struct {
|
type SpanFilterParams struct {
|
||||||
|
TraceID []string `json:"traceID"`
|
||||||
Status []string `json:"status"`
|
Status []string `json:"status"`
|
||||||
ServiceName []string `json:"serviceName"`
|
ServiceName []string `json:"serviceName"`
|
||||||
HttpRoute []string `json:"httpRoute"`
|
HttpRoute []string `json:"httpRoute"`
|
||||||
@ -258,6 +262,7 @@ type SpanFilterParams struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type TagFilterParams struct {
|
type TagFilterParams struct {
|
||||||
|
TraceID []string `json:"traceID"`
|
||||||
Status []string `json:"status"`
|
Status []string `json:"status"`
|
||||||
ServiceName []string `json:"serviceName"`
|
ServiceName []string `json:"serviceName"`
|
||||||
HttpRoute []string `json:"httpRoute"`
|
HttpRoute []string `json:"httpRoute"`
|
||||||
|
@ -492,3 +492,7 @@ func (s *ServiceItem) MarshalJSON() ([]byte, error) {
|
|||||||
Alias: (*Alias)(s),
|
Alias: (*Alias)(s),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DashboardVar struct {
|
||||||
|
VariableValues []interface{} `json:"variableValues"`
|
||||||
|
}
|
||||||
|
@ -3,6 +3,8 @@ package promql
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/go-kit/log"
|
"github.com/go-kit/log"
|
||||||
pmodel "github.com/prometheus/common/model"
|
pmodel "github.com/prometheus/common/model"
|
||||||
plog "github.com/prometheus/common/promlog"
|
plog "github.com/prometheus/common/promlog"
|
||||||
@ -11,7 +13,7 @@ import (
|
|||||||
pql "github.com/prometheus/prometheus/promql"
|
pql "github.com/prometheus/prometheus/promql"
|
||||||
pstorage "github.com/prometheus/prometheus/storage"
|
pstorage "github.com/prometheus/prometheus/storage"
|
||||||
premote "github.com/prometheus/prometheus/storage/remote"
|
premote "github.com/prometheus/prometheus/storage/remote"
|
||||||
"time"
|
"go.signoz.io/query-service/interfaces"
|
||||||
)
|
)
|
||||||
|
|
||||||
type PqlEngine struct {
|
type PqlEngine struct {
|
||||||
@ -29,6 +31,13 @@ func FromConfigPath(promConfigPath string) (*PqlEngine, error) {
|
|||||||
return NewPqlEngine(c)
|
return NewPqlEngine(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func FromReader(ch interfaces.Reader) (*PqlEngine, error) {
|
||||||
|
return &PqlEngine{
|
||||||
|
engine: ch.GetQueryEngine(),
|
||||||
|
fanoutStorage: *ch.GetFanoutStorage(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func NewPqlEngine(config *pconfig.Config) (*PqlEngine, error) {
|
func NewPqlEngine(config *pconfig.Config) (*PqlEngine, error) {
|
||||||
|
|
||||||
logLevel := plog.AllowedLevel{}
|
logLevel := plog.AllowedLevel{}
|
||||||
|
@ -3,11 +3,14 @@ package telemetry
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
ph "github.com/posthog/posthog-go"
|
||||||
"go.signoz.io/query-service/constants"
|
"go.signoz.io/query-service/constants"
|
||||||
"go.signoz.io/query-service/interfaces"
|
"go.signoz.io/query-service/interfaces"
|
||||||
"go.signoz.io/query-service/model"
|
"go.signoz.io/query-service/model"
|
||||||
@ -16,15 +19,19 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
TELEMETRY_EVENT_PATH = "API Call"
|
TELEMETRY_EVENT_PATH = "API Call"
|
||||||
TELEMETRY_EVENT_USER = "User"
|
TELEMETRY_EVENT_USER = "User"
|
||||||
TELEMETRY_EVENT_INPRODUCT_FEEDBACK = "InProduct Feeback Submitted"
|
TELEMETRY_EVENT_INPRODUCT_FEEDBACK = "InProduct Feeback Submitted"
|
||||||
TELEMETRY_EVENT_NUMBER_OF_SERVICES = "Number of Services"
|
TELEMETRY_EVENT_NUMBER_OF_SERVICES = "Number of Services"
|
||||||
TELEMETRY_EVENT_HEART_BEAT = "Heart Beat"
|
TELEMETRY_EVENT_NUMBER_OF_SERVICES_PH = "Number of Services V2"
|
||||||
TELEMETRY_EVENT_ORG_SETTINGS = "Org Settings"
|
TELEMETRY_EVENT_HEART_BEAT = "Heart Beat"
|
||||||
|
TELEMETRY_EVENT_ORG_SETTINGS = "Org Settings"
|
||||||
|
DEFAULT_SAMPLING = 0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
const api_key = "4Gmoa4ixJAUHx2BpJxsjwA1bEfnwEeRz"
|
const api_key = "4Gmoa4ixJAUHx2BpJxsjwA1bEfnwEeRz"
|
||||||
|
const ph_api_key = "H-htDCae7CR3RV57gUzmol6IAKtm5IMCvbcm_fwnL-w"
|
||||||
|
|
||||||
const IP_NOT_FOUND_PLACEHOLDER = "NA"
|
const IP_NOT_FOUND_PLACEHOLDER = "NA"
|
||||||
|
|
||||||
const HEART_BEAT_DURATION = 6 * time.Hour
|
const HEART_BEAT_DURATION = 6 * time.Hour
|
||||||
@ -34,20 +41,41 @@ const HEART_BEAT_DURATION = 6 * time.Hour
|
|||||||
var telemetry *Telemetry
|
var telemetry *Telemetry
|
||||||
var once sync.Once
|
var once sync.Once
|
||||||
|
|
||||||
|
func (a *Telemetry) IsSampled() bool {
|
||||||
|
|
||||||
|
random_number := a.minRandInt + rand.Intn(a.maxRandInt-a.minRandInt) + 1
|
||||||
|
|
||||||
|
if (random_number % a.maxRandInt) == 0 {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
type Telemetry struct {
|
type Telemetry struct {
|
||||||
operator analytics.Client
|
operator analytics.Client
|
||||||
ipAddress string
|
phOperator ph.Client
|
||||||
isEnabled bool
|
ipAddress string
|
||||||
isAnonymous bool
|
isEnabled bool
|
||||||
distinctId string
|
isAnonymous bool
|
||||||
reader interfaces.Reader
|
distinctId string
|
||||||
|
reader interfaces.Reader
|
||||||
|
companyDomain string
|
||||||
|
minRandInt int
|
||||||
|
maxRandInt int
|
||||||
}
|
}
|
||||||
|
|
||||||
func createTelemetry() {
|
func createTelemetry() {
|
||||||
telemetry = &Telemetry{
|
telemetry = &Telemetry{
|
||||||
operator: analytics.New(api_key),
|
operator: analytics.New(api_key),
|
||||||
ipAddress: getOutboundIP(),
|
phOperator: ph.New(ph_api_key),
|
||||||
|
ipAddress: getOutboundIP(),
|
||||||
}
|
}
|
||||||
|
telemetry.minRandInt = 0
|
||||||
|
telemetry.maxRandInt = int(1 / DEFAULT_SAMPLING)
|
||||||
|
|
||||||
|
rand.Seed(time.Now().UnixNano())
|
||||||
|
|
||||||
data := map[string]interface{}{}
|
data := map[string]interface{}{}
|
||||||
|
|
||||||
@ -106,13 +134,36 @@ func (a *Telemetry) IdentifyUser(user *model.User) {
|
|||||||
if !a.isTelemetryEnabled() || a.isTelemetryAnonymous() {
|
if !a.isTelemetryEnabled() || a.isTelemetryAnonymous() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
a.setCompanyDomain(user.Email)
|
||||||
|
|
||||||
a.operator.Enqueue(analytics.Identify{
|
a.operator.Enqueue(analytics.Identify{
|
||||||
UserId: a.ipAddress,
|
UserId: a.ipAddress,
|
||||||
Traits: analytics.NewTraits().SetName(user.Name).SetEmail(user.Email).Set("ip", a.ipAddress),
|
Traits: analytics.NewTraits().SetName(user.Name).SetEmail(user.Email).Set("ip", a.ipAddress),
|
||||||
})
|
})
|
||||||
|
// Updating a groups properties
|
||||||
|
a.phOperator.Enqueue(ph.GroupIdentify{
|
||||||
|
Type: "companyDomain",
|
||||||
|
Key: a.getCompanyDomain(),
|
||||||
|
Properties: ph.NewProperties().
|
||||||
|
Set("companyDomain", a.getCompanyDomain()),
|
||||||
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *Telemetry) setCompanyDomain(email string) {
|
||||||
|
|
||||||
|
email_split := strings.Split(email, "@")
|
||||||
|
if len(email_split) != 2 {
|
||||||
|
a.companyDomain = email
|
||||||
|
}
|
||||||
|
a.companyDomain = email_split[1]
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Telemetry) getCompanyDomain() string {
|
||||||
|
return a.companyDomain
|
||||||
|
}
|
||||||
|
|
||||||
func (a *Telemetry) checkEvents(event string) bool {
|
func (a *Telemetry) checkEvents(event string) bool {
|
||||||
sendEvent := true
|
sendEvent := true
|
||||||
if event == TELEMETRY_EVENT_USER && a.isTelemetryAnonymous() {
|
if event == TELEMETRY_EVENT_USER && a.isTelemetryAnonymous() {
|
||||||
@ -136,6 +187,7 @@ func (a *Telemetry) SendEvent(event string, data map[string]interface{}) {
|
|||||||
properties := analytics.NewProperties()
|
properties := analytics.NewProperties()
|
||||||
properties.Set("version", version.GetVersion())
|
properties.Set("version", version.GetVersion())
|
||||||
properties.Set("deploymentType", getDeploymentType())
|
properties.Set("deploymentType", getDeploymentType())
|
||||||
|
properties.Set("companyDomain", a.getCompanyDomain())
|
||||||
|
|
||||||
for k, v := range data {
|
for k, v := range data {
|
||||||
properties.Set(k, v)
|
properties.Set(k, v)
|
||||||
@ -151,6 +203,18 @@ func (a *Telemetry) SendEvent(event string, data map[string]interface{}) {
|
|||||||
UserId: userId,
|
UserId: userId,
|
||||||
Properties: properties,
|
Properties: properties,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if event == TELEMETRY_EVENT_NUMBER_OF_SERVICES {
|
||||||
|
|
||||||
|
a.phOperator.Enqueue(ph.Capture{
|
||||||
|
DistinctId: userId,
|
||||||
|
Event: TELEMETRY_EVENT_NUMBER_OF_SERVICES_PH,
|
||||||
|
Properties: ph.Properties(properties),
|
||||||
|
Groups: ph.NewGroups().
|
||||||
|
Set("companyDomain", a.getCompanyDomain()),
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Telemetry) GetDistinctId() string {
|
func (a *Telemetry) GetDistinctId() string {
|
||||||
|
@ -61,7 +61,7 @@ services:
|
|||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|
||||||
otel-collector:
|
otel-collector:
|
||||||
image: signoz-otel-collector:0.55.0
|
image: signoz-otel-collector:0.55.1
|
||||||
command: ["--config=/etc/otel-collector-config.yaml"]
|
command: ["--config=/etc/otel-collector-config.yaml"]
|
||||||
user: root # required for reading docker container logs
|
user: root # required for reading docker container logs
|
||||||
volumes:
|
volumes:
|
||||||
@ -77,7 +77,7 @@ services:
|
|||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|
||||||
otel-collector-metrics:
|
otel-collector-metrics:
|
||||||
image: signoz-otel-collector:0.55.0
|
image: signoz-otel-collector:0.55.1
|
||||||
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
|
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
|
||||||
volumes:
|
volumes:
|
||||||
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
|
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
|
||||||
|
@ -19,8 +19,7 @@ rule_files:
|
|||||||
|
|
||||||
# A scrape configuration containing exactly one endpoint to scrape:
|
# A scrape configuration containing exactly one endpoint to scrape:
|
||||||
# Here it's Prometheus itself.
|
# Here it's Prometheus itself.
|
||||||
scrape_configs:
|
scrape_configs: []
|
||||||
|
|
||||||
|
|
||||||
remote_read:
|
remote_read:
|
||||||
- url: tcp://clickhouse:9000/?database=signoz_metrics
|
- url: tcp://clickhouse:9000/?database=signoz_metrics
|
||||||
|