Merge pull request #2034 from SigNoz/release/v0.14.0

Release/v0.14.0
This commit is contained in:
Ankit Nayan 2023-01-12 18:55:14 +05:30 committed by GitHub
commit 2f323056d0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
102 changed files with 4900 additions and 4302 deletions

View File

@ -32,6 +32,10 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Run tests
shell: bash
run: |
make test
- name: Build query-service image
shell: bash
run: |

View File

@ -135,3 +135,6 @@ clear-standalone-data:
clear-swarm-data:
@docker run --rm -v "$(PWD)/$(SWARM_DIRECTORY)/data:/pwd" busybox \
sh -c "cd /pwd && rm -rf alertmanager/* clickhouse*/* signoz/* zookeeper-*/*"
test:
go test ./pkg/query-service/app/metrics/...

View File

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

View File

@ -86,6 +86,10 @@ processors:
default: default
- name: deployment.environment
default: default
# This is added to ensure the uniqueness of the timeseries
# Otherwise, identical timeseries produced by multiple replicas of
# collectors result in incorrect APM metrics
- name: 'signoz.collector.id'
# memory_limiter:
# # 80% of maximum memory up to 2G
# limit_mib: 1500

View File

@ -905,7 +905,8 @@
<dictionaries_config>*_dictionary.xml</dictionaries_config>
<!-- Configuration of user defined executable functions -->
<user_defined_executable_functions_config>*_function.xml</user_defined_executable_functions_config>
<user_defined_executable_functions_config>*function.xml</user_defined_executable_functions_config>
<user_scripts_path>/var/lib/clickhouse/user_scripts/</user_scripts_path>
<!-- Uncomment if you want data to be compressed 30-100% better.
Don't do that if you just started using ClickHouse.

View File

@ -0,0 +1,21 @@
<functions>
<function>
<type>executable</type>
<name>histogramQuantile</name>
<return_type>Float64</return_type>
<argument>
<type>Array(Float64)</type>
<name>buckets</name>
</argument>
<argument>
<type>Array(Float64)</type>
<name>counts</name>
</argument>
<argument>
<type>Float64</type>
<name>quantile</name>
</argument>
<format>CSV</format>
<command>./histogramQuantile</command>
</function>
</functions>

View File

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

View File

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

View File

@ -83,6 +83,10 @@ processors:
default: default
- name: deployment.environment
default: default
# This is added to ensure the uniqueness of the timeseries
# Otherwise, identical timeseries produced by multiple replicas of
# collectors result in incorrect APM metrics
- name: 'signoz.collector.id'
# memory_limiter:
# # 80% of maximum memory up to 2G
# limit_mib: 1500

View File

@ -0,0 +1,237 @@
package main
import (
"bufio"
"fmt"
"math"
"os"
"sort"
"strconv"
"strings"
)
// NOTE: executable must be built with target OS and architecture set to linux/amd64
// env GOOS=linux GOARCH=amd64 go build -o histogramQuantile histogramQuantile.go
// The following code is adapted from the following source:
// https://github.com/prometheus/prometheus/blob/main/promql/quantile.go
type bucket struct {
upperBound float64
count float64
}
// buckets implements sort.Interface.
type buckets []bucket
func (b buckets) Len() int { return len(b) }
func (b buckets) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
func (b buckets) Less(i, j int) bool { return b[i].upperBound < b[j].upperBound }
// bucketQuantile calculates the quantile 'q' based on the given buckets. The
// buckets will be sorted by upperBound by this function (i.e. no sorting
// needed before calling this function). The quantile value is interpolated
// assuming a linear distribution within a bucket. However, if the quantile
// falls into the highest bucket, the upper bound of the 2nd highest bucket is
// returned. A natural lower bound of 0 is assumed if the upper bound of the
// lowest bucket is greater 0. In that case, interpolation in the lowest bucket
// happens linearly between 0 and the upper bound of the lowest bucket.
// However, if the lowest bucket has an upper bound less or equal 0, this upper
// bound is returned if the quantile falls into the lowest bucket.
//
// There are a number of special cases (once we have a way to report errors
// happening during evaluations of AST functions, we should report those
// explicitly):
//
// If 'buckets' has 0 observations, NaN is returned.
//
// If 'buckets' has fewer than 2 elements, NaN is returned.
//
// If the highest bucket is not +Inf, NaN is returned.
//
// If q==NaN, NaN is returned.
//
// If q<0, -Inf is returned.
//
// If q>1, +Inf is returned.
func bucketQuantile(q float64, buckets buckets) float64 {
if math.IsNaN(q) {
return math.NaN()
}
if q < 0 {
return math.Inf(-1)
}
if q > 1 {
return math.Inf(+1)
}
sort.Sort(buckets)
if !math.IsInf(buckets[len(buckets)-1].upperBound, +1) {
return math.NaN()
}
buckets = coalesceBuckets(buckets)
ensureMonotonic(buckets)
if len(buckets) < 2 {
return math.NaN()
}
observations := buckets[len(buckets)-1].count
if observations == 0 {
return math.NaN()
}
rank := q * observations
b := sort.Search(len(buckets)-1, func(i int) bool { return buckets[i].count >= rank })
if b == len(buckets)-1 {
return buckets[len(buckets)-2].upperBound
}
if b == 0 && buckets[0].upperBound <= 0 {
return buckets[0].upperBound
}
var (
bucketStart float64
bucketEnd = buckets[b].upperBound
count = buckets[b].count
)
if b > 0 {
bucketStart = buckets[b-1].upperBound
count -= buckets[b-1].count
rank -= buckets[b-1].count
}
return bucketStart + (bucketEnd-bucketStart)*(rank/count)
}
// coalesceBuckets merges buckets with the same upper bound.
//
// The input buckets must be sorted.
func coalesceBuckets(buckets buckets) buckets {
last := buckets[0]
i := 0
for _, b := range buckets[1:] {
if b.upperBound == last.upperBound {
last.count += b.count
} else {
buckets[i] = last
last = b
i++
}
}
buckets[i] = last
return buckets[:i+1]
}
// The assumption that bucket counts increase monotonically with increasing
// upperBound may be violated during:
//
// * Recording rule evaluation of histogram_quantile, especially when rate()
// has been applied to the underlying bucket timeseries.
// * Evaluation of histogram_quantile computed over federated bucket
// timeseries, especially when rate() has been applied.
//
// This is because scraped data is not made available to rule evaluation or
// federation atomically, so some buckets are computed with data from the
// most recent scrapes, but the other buckets are missing data from the most
// recent scrape.
//
// Monotonicity is usually guaranteed because if a bucket with upper bound
// u1 has count c1, then any bucket with a higher upper bound u > u1 must
// have counted all c1 observations and perhaps more, so that c >= c1.
//
// Randomly interspersed partial sampling breaks that guarantee, and rate()
// exacerbates it. Specifically, suppose bucket le=1000 has a count of 10 from
// 4 samples but the bucket with le=2000 has a count of 7 from 3 samples. The
// monotonicity is broken. It is exacerbated by rate() because under normal
// operation, cumulative counting of buckets will cause the bucket counts to
// diverge such that small differences from missing samples are not a problem.
// rate() removes this divergence.)
//
// bucketQuantile depends on that monotonicity to do a binary search for the
// bucket with the φ-quantile count, so breaking the monotonicity
// guarantee causes bucketQuantile() to return undefined (nonsense) results.
//
// As a somewhat hacky solution until ingestion is atomic per scrape, we
// calculate the "envelope" of the histogram buckets, essentially removing
// any decreases in the count between successive buckets.
func ensureMonotonic(buckets buckets) {
max := buckets[0].count
for i := 1; i < len(buckets); i++ {
switch {
case buckets[i].count > max:
max = buckets[i].count
case buckets[i].count < max:
buckets[i].count = max
}
}
}
// End of copied code.
func readLines() []string {
r := bufio.NewReader(os.Stdin)
bytes := []byte{}
lines := []string{}
for {
line, isPrefix, err := r.ReadLine()
if err != nil {
break
}
bytes = append(bytes, line...)
if !isPrefix {
str := strings.TrimSpace(string(bytes))
if len(str) > 0 {
lines = append(lines, str)
bytes = []byte{}
}
}
}
if len(bytes) > 0 {
lines = append(lines, string(bytes))
}
return lines
}
func main() {
lines := readLines()
for _, text := range lines {
// Example input
// "[1, 2, 4, 8, 16]", "[1, 5, 8, 10, 14]", 0.9"
// bounds - counts - quantile
parts := strings.Split(text, "\",")
var bucketNumbers []float64
// Strip the ends with square brackets
text = parts[0][2 : len(parts[0])-1]
// Parse the bucket bounds
for _, num := range strings.Split(text, ",") {
num = strings.TrimSpace(num)
number, err := strconv.ParseFloat(num, 64)
if err == nil {
bucketNumbers = append(bucketNumbers, number)
}
}
var bucketCounts []float64
// Strip the ends with square brackets
text = parts[1][2 : len(parts[1])-1]
// Parse the bucket counts
for _, num := range strings.Split(text, ",") {
num = strings.TrimSpace(num)
number, err := strconv.ParseFloat(num, 64)
if err == nil {
bucketCounts = append(bucketCounts, number)
}
}
// Parse the quantile
q, err := strconv.ParseFloat(parts[2], 64)
var b buckets
if err == nil {
for i := 0; i < len(bucketNumbers); i++ {
b = append(b, bucket{upperBound: bucketNumbers[i], count: bucketCounts[i]})
}
}
fmt.Println(bucketQuantile(q, b))
}
}

View File

@ -275,22 +275,31 @@ func extractDashboardMetaData(path string, r *http.Request) (map[string]interfac
data := map[string]interface{}{}
if path == pathToExtractBodyFrom && (r.Method == "POST") {
bodyBytes, _ := ioutil.ReadAll(r.Body)
r.Body.Close() // must close
r.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))
json.Unmarshal(bodyBytes, &requestBody)
if r.Body != nil {
bodyBytes, err := ioutil.ReadAll(r.Body)
if err != nil {
return nil, false
}
r.Body.Close() // must close
r.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))
json.Unmarshal(bodyBytes, &requestBody)
} else {
return nil, false
}
} else {
return nil, false
}
compositeMetricQuery, compositeMetricQueryExists := requestBody["compositeMetricQuery"]
compositeMetricQueryMap := compositeMetricQuery.(map[string]interface{})
signozMetricFound := false
if compositeMetricQueryExists {
compositeMetricQueryMap := compositeMetricQuery.(map[string]interface{})
signozMetricFound = telemetry.GetInstance().CheckSigNozMetrics(compositeMetricQueryMap)
queryType, queryTypeExists := compositeMetricQueryMap["queryType"]
if queryTypeExists {
data["queryType"] = queryType

View File

@ -27,8 +27,8 @@
"author": "",
"license": "ISC",
"dependencies": {
"@ant-design/colors": "^6.0.0",
"@ant-design/icons": "^4.6.2",
"@ant-design/colors": "6.0.0",
"@ant-design/icons": "4.8.0",
"@grafana/data": "^8.4.3",
"@monaco-editor/react": "^4.3.1",
"@testing-library/jest-dom": "^5.11.4",
@ -36,7 +36,7 @@
"@testing-library/user-event": "^12.1.10",
"@welldone-software/why-did-you-render": "^6.2.1",
"@xstate/react": "^3.0.0",
"antd": "4.19.2",
"antd": "5.0.5",
"axios": "^0.21.0",
"babel-eslint": "^10.1.0",
"babel-jest": "^26.6.0",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,8 @@
import { ConfigProvider } from 'antd';
import NotFound from 'components/NotFound';
import Spinner from 'components/Spinner';
import AppLayout from 'container/AppLayout';
import { useThemeConfig } from 'hooks/useDarkMode';
import history from 'lib/history';
import React, { Suspense } from 'react';
import { Route, Router, Switch } from 'react-router-dom';
@ -9,29 +11,31 @@ import PrivateRoute from './Private';
import routes from './routes';
function App(): JSX.Element {
const themeConfig = useThemeConfig();
return (
<Router history={history}>
<PrivateRoute>
<AppLayout>
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
<Switch>
{routes.map(({ path, component, exact }) => {
return (
<ConfigProvider theme={themeConfig}>
<Router history={history}>
<PrivateRoute>
<AppLayout>
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
<Switch>
{routes.map(({ path, component, exact }) => (
<Route
key={`${path}`}
exact={exact}
path={path}
component={component}
/>
);
})}
))}
<Route path="*" component={NotFound} />
</Switch>
</Suspense>
</AppLayout>
</PrivateRoute>
</Router>
<Route path="*" component={NotFound} />
</Switch>
</Suspense>
</AppLayout>
</PrivateRoute>
</Router>
</ConfigProvider>
);
}

View File

@ -1,9 +0,0 @@
import generatePicker from 'antd/es/date-picker/generatePicker';
import { Dayjs } from 'dayjs';
// included in antd
// eslint-disable-next-line import/no-extraneous-dependencies
import dayjsGenerateConfig from 'rc-picker/lib/generate/dayjs';
const DatePicker = generatePicker<Dayjs>(dayjsGenerateConfig);
export default DatePicker;

View File

@ -1,8 +1,6 @@
import MEditor, { EditorProps } from '@monaco-editor/react';
import { useIsDarkMode } from 'hooks/useDarkMode';
import React from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import AppReducer from 'types/reducer/app';
function Editor({
value,
@ -12,7 +10,7 @@ function Editor({
height,
options,
}: MEditorProps): JSX.Element {
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
const isDarkMode = useIsDarkMode();
return (
<MEditor
theme={isDarkMode ? 'vs-dark' : 'vs-light'}

View File

@ -11,7 +11,7 @@ export const emptyGraph = {
ctx.textBaseline = 'middle';
ctx.font = '1.5rem sans-serif';
ctx.fillStyle = `${grey.primary}`;
ctx.fillText('No data to display', width / 2, height / 2);
ctx.fillText('No data', width / 2, height / 2);
ctx.restore();
},
};

View File

@ -23,10 +23,8 @@ import {
} from 'chart.js';
import * as chartjsAdapter from 'chartjs-adapter-date-fns';
import annotationPlugin from 'chartjs-plugin-annotation';
import { useIsDarkMode } from 'hooks/useDarkMode';
import React, { useCallback, useEffect, useRef } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import AppReducer from 'types/reducer/app';
import { hasData } from './hasData';
import { legend } from './Plugin';
@ -67,8 +65,9 @@ function Graph({
staticLine,
containerHeight,
}: GraphProps): JSX.Element {
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
const chartRef = useRef<HTMLCanvasElement>(null);
const isDarkMode = useIsDarkMode();
const currentTheme = isDarkMode ? 'dark' : 'light';
const xAxisTimeUnit = useXAxisTimeUnit(data); // Computes the relevant time unit for x axis by analyzing the time stamp data

View File

@ -1,29 +1,27 @@
import { Popover } from 'antd';
import React from 'react';
import { notification, Popover } from 'antd';
import React, { useCallback, useEffect } from 'react';
import { useCopyToClipboard } from 'react-use';
interface CopyClipboardHOCProps {
textToCopy: string;
children: React.ReactNode;
}
function CopyClipboardHOC({
textToCopy,
children,
}: CopyClipboardHOCProps): JSX.Element {
const [, setCopy] = useCopyToClipboard();
const [value, setCopy] = useCopyToClipboard();
useEffect(() => {
if (value.value) {
notification.success({
message: 'Copied to clipboard',
});
}
}, [value]);
const onClick = useCallback((): void => {
setCopy(textToCopy);
}, [setCopy, textToCopy]);
return (
<span
style={{
margin: 0,
padding: 0,
cursor: 'pointer',
}}
onClick={(): void => setCopy(textToCopy)}
onKeyDown={(): void => setCopy(textToCopy)}
role="button"
tabIndex={0}
>
<span onClick={onClick} onKeyDown={onClick} role="button" tabIndex={0}>
<Popover
placement="top"
content={<span style={{ fontSize: '0.9rem' }}>Copy to clipboard</span>}
@ -34,4 +32,9 @@ function CopyClipboardHOC({
);
}
interface CopyClipboardHOCProps {
textToCopy: string;
children: React.ReactNode;
}
export default CopyClipboardHOC;

View File

@ -14,7 +14,7 @@ import { ILogsReducer } from 'types/reducer/logs';
import AddToQueryHOC from '../AddToQueryHOC';
import CopyClipboardHOC from '../CopyClipboardHOC';
import { Container } from './styles';
import { Container, Text, TextContainer } from './styles';
import { isValidLogField } from './util';
interface LogFieldProps {
@ -23,21 +23,17 @@ interface LogFieldProps {
}
function LogGeneralField({ fieldKey, fieldValue }: LogFieldProps): JSX.Element {
return (
<div
style={{
display: 'flex',
overflow: 'hidden',
width: '100%',
}}
>
<Typography.Text type="secondary">{fieldKey}</Typography.Text>
<TextContainer>
<Text ellipsis type="secondary">
{fieldKey}
</Text>
<CopyClipboardHOC textToCopy={fieldValue}>
<Typography.Text ellipsis>
{': '}
{fieldValue}
</Typography.Text>
</CopyClipboardHOC>
</div>
</TextContainer>
);
}
function LogSelectedField({

View File

@ -1,4 +1,4 @@
import { Card } from 'antd';
import { Card, Typography } from 'antd';
import styled, { keyframes } from 'styled-components';
const fadeInAnimation = keyframes`
@ -16,3 +16,16 @@ export const Container = styled(Card)`
animation-duration: 0.2s;
animation-timing-function: ease-in;
`;
export const Text = styled(Typography.Text)`
&&& {
min-width: 1.5rem;
white-space: nowrap;
}
`;
export const TextContainer = styled.div`
display: flex;
overflow: hidden;
width: 100%;
`;

View File

@ -11,7 +11,7 @@ function CustomModal({
return (
<Modal
title={title}
visible={isModalVisible}
open={isModalVisible}
footer={footer}
closable={closable}
>

View File

@ -3,4 +3,5 @@ export enum LOCALSTORAGE {
IS_LOGGED_IN = 'IS_LOGGED_IN',
AUTH_TOKEN = 'AUTH_TOKEN',
REFRESH_AUTH_TOKEN = 'REFRESH_AUTH_TOKEN',
THEME = 'THEME',
}

View File

@ -1,5 +1,5 @@
import { Button } from 'antd';
import { NotificationInstance } from 'antd/lib/notification';
import { NotificationInstance } from 'antd/es/notification/interface';
import deleteChannel from 'api/channels/delete';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';

View File

@ -3,9 +3,10 @@ import styled from 'styled-components';
export const Layout = styled(LayoutComponent)`
&&& {
min-height: 92vh;
display: flex;
position: relative;
min-height: calc(100vh - 4rem);
overflow: hidden;
}
`;

View File

@ -1,13 +1,12 @@
import { Menu, Space } from 'antd';
import Spinner from 'components/Spinner';
import { useIsDarkMode } from 'hooks/useDarkMode';
import React, { Suspense, useMemo } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { ConfigProps } from 'types/api/dynamicConfigs/getDynamicConfigs';
import AppReducer from 'types/reducer/app';
import ErrorLink from './ErrorLink';
import LinkContainer from './Link';
import { MenuItem } from './styles';
function HelpToolTip({ config }: HelpToolTipProps): JSX.Element {
const sortedConfig = useMemo(
@ -15,10 +14,10 @@ function HelpToolTip({ config }: HelpToolTipProps): JSX.Element {
[config.components],
);
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
const isDarkMode = useIsDarkMode();
return (
<Menu.ItemGroup>
<Menu>
{sortedConfig.map((item) => {
const iconName = `${isDarkMode ? item.darkIcon : item.lightIcon}`;
@ -28,19 +27,19 @@ function HelpToolTip({ config }: HelpToolTipProps): JSX.Element {
return (
<ErrorLink key={item.text + item.href}>
<Suspense fallback={<Spinner height="5vh" />}>
<Menu.Item>
<MenuItem>
<LinkContainer href={item.href}>
<Space size="small" align="start">
<Component />
{item.text}
</Space>
</LinkContainer>
</Menu.Item>
</MenuItem>
</Suspense>
</ErrorLink>
);
})}
</Menu.ItemGroup>
</Menu>
);
}

View File

@ -0,0 +1,10 @@
import { Menu } from 'antd';
import styled from 'styled-components';
export const MenuItem = styled(Menu.Item)`
&&& {
height: 1.75rem;
display: flex;
align-items: center;
}
`;

View File

@ -5,6 +5,7 @@ import {
QuestionCircleOutlined,
} from '@ant-design/icons';
import { Dropdown, Menu, Space } from 'antd';
import { useIsDarkMode } from 'hooks/useDarkMode';
import React, { useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
@ -15,9 +16,8 @@ import HelpToolTip from './Config';
function DynamicConfigDropdown({
frontendId,
}: DynamicConfigDropdownProps): JSX.Element {
const { configs, isDarkMode } = useSelector<AppState, AppReducer>(
(state) => state.app,
);
const { configs } = useSelector<AppState, AppReducer>((state) => state.app);
const isDarkMode = useIsDarkMode();
const [isHelpDropDownOpen, setIsHelpDropDownOpen] = useState<boolean>(false);
const config = useMemo(

View File

@ -1,5 +1,4 @@
import { Select } from 'antd';
import FormItem from 'antd/lib/form/FormItem';
import { Form, Select } from 'antd';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { AlertDef, Labels } from 'types/api/alerts/def';
@ -31,7 +30,7 @@ function BasicInfo({ alertDef, setAlertDef }: BasicInfoProps): JSX.Element {
<>
<StepHeading> {t('alert_form_step3')} </StepHeading>
<FormContainer>
<FormItem
<Form.Item
label={t('field_severity')}
labelAlign="left"
name={['labels', 'severity']}
@ -54,9 +53,9 @@ function BasicInfo({ alertDef, setAlertDef }: BasicInfoProps): JSX.Element {
<Option value="warning">{t('option_warning')}</Option>
<Option value="info">{t('option_info')}</Option>
</SeveritySelect>
</FormItem>
</Form.Item>
<FormItem label={t('field_alert_name')} labelAlign="left" name="alert">
<Form.Item label={t('field_alert_name')} labelAlign="left" name="alert">
<InputSmall
onChange={(e): void => {
setAlertDef({
@ -65,8 +64,8 @@ function BasicInfo({ alertDef, setAlertDef }: BasicInfoProps): JSX.Element {
});
}}
/>
</FormItem>
<FormItem
</Form.Item>
<Form.Item
label={t('field_alert_desc')}
labelAlign="left"
name={['annotations', 'description']}
@ -82,7 +81,7 @@ function BasicInfo({ alertDef, setAlertDef }: BasicInfoProps): JSX.Element {
});
}}
/>
</FormItem>
</Form.Item>
<FormItemMedium label={t('field_labels')}>
<LabelSelect
onSetLabels={(l: Labels): void => {

View File

@ -1,5 +1,5 @@
import { ExclamationCircleOutlined, SaveOutlined } from '@ant-design/icons';
import { FormInstance, Modal, notification, Typography } from 'antd';
import { Col, FormInstance, Modal, notification, Typography } from 'antd';
import saveAlertApi from 'api/alerts/save';
import testAlertApi from 'api/alerts/testAlert';
import ROUTES from 'constants/routes';
@ -34,7 +34,6 @@ import {
MainFormContainer,
PanelContainer,
StyledLeftContainer,
StyledRightContainer,
} from './styles';
import useDebounce from './useDebounce';
import UserGuide from './UserGuide';
@ -535,9 +534,9 @@ function FormAlertRules({
</ButtonContainer>
</MainFormContainer>
</StyledLeftContainer>
<StyledRightContainer flex="1 1 300px">
<Col flex="1 1 300px">
<UserGuide queryType={queryCategory} />
</StyledRightContainer>
</Col>
</PanelContainer>
</>
);

View File

@ -4,13 +4,11 @@ import {
} from '@ant-design/icons';
import { useMachine } from '@xstate/react';
import { Button, Input, message, Modal } from 'antd';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { map } from 'lodash-es';
import React, { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { Labels } from 'types/api/alerts/def';
import AppReducer from 'types/reducer/app';
import { v4 as uuid } from 'uuid';
import { ResourceAttributesFilterMachine } from './Labels.machine';
@ -29,7 +27,8 @@ function LabelSelect({
initialValues,
}: LabelSelectProps): JSX.Element | null {
const { t } = useTranslation('alerts');
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
const isDarkMode = useIsDarkMode();
const [currentVal, setCurrentVal] = useState('');
const [staging, setStaging] = useState<string[]>([]);
const [queries, setQueries] = useState<ILabelRecord[]>(

View File

@ -9,19 +9,15 @@ import {
Select,
Typography,
} from 'antd';
import FormItem from 'antd/lib/form/FormItem';
import TextArea from 'antd/lib/input/TextArea';
import styled from 'styled-components';
const { TextArea } = Input;
const { Item } = Form;
export const PanelContainer = styled(Row)`
flex-wrap: nowrap;
`;
export const StyledRightContainer = styled(Col)`
&&& {
}
`;
export const StyledLeftContainer = styled(Col)`
&&& {
margin-right: 1rem;
@ -111,7 +107,7 @@ export const TextareaMedium = styled(TextArea)`
width: 70%;
`;
export const FormItemMedium = styled(FormItem)`
export const FormItemMedium = styled(Item)`
width: 70%;
`;

View File

@ -1,5 +1,5 @@
import { convertTimeToRelevantUnit } from 'container/TraceDetail/utils';
import useThemeMode from 'hooks/useThemeMode';
import { useIsDarkMode } from 'hooks/useDarkMode';
import React from 'react';
import { toFixed } from 'utils/toFixed';
@ -14,7 +14,7 @@ interface SpanLengthProps {
function SpanLength(props: SpanLengthProps): JSX.Element {
const { width, leftOffset, bgColor, inMsCount } = props;
const { isDarkMode } = useThemeMode();
const isDarkMode = useIsDarkMode();
const { time, timeUnitName } = convertTimeToRelevantUnit(inMsCount);
return (
<SpanWrapper>

View File

@ -1,10 +1,10 @@
import { CaretDownFilled, CaretRightFilled } from '@ant-design/icons';
import { Col } from 'antd';
import { Col, Typography } from 'antd';
import { StyledCol, StyledRow } from 'components/Styled';
import { IIntervalUnit } from 'container/TraceDetail/utils';
import useThemeMode from 'hooks/useThemeMode';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { SPAN_DETAILS_LEFT_COL_WIDTH } from 'pages/TraceDetail/constants';
import React, { useEffect, useRef, useState } from 'react';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { ITraceTree } from 'types/api/trace/getTraceItem';
import { ITraceMetaData } from '..';
@ -19,6 +19,7 @@ import {
styles,
Wrapper,
} from './styles';
import { getIconStyles } from './utils';
function Trace(props: TraceProps): JSX.Element {
const {
@ -42,7 +43,8 @@ function Trace(props: TraceProps): JSX.Element {
isMissing,
} = props;
const { isDarkMode } = useThemeMode();
const isDarkMode = useIsDarkMode();
const [isOpen, setOpen] = useState<boolean>(activeSpanPath[level] === id);
const localTreeExpandInteraction = useRef<boolean | 0>(0); // Boolean is for the state of the expansion whereas the number i.e. 0 is for skipping the user interaction.
@ -111,6 +113,18 @@ function Trace(props: TraceProps): JSX.Element {
const width = (value * 1e2) / (globalSpread * 1e6);
const panelWidth = SPAN_DETAILS_LEFT_COL_WIDTH - level * (16 + 1) - 48;
const iconStyles = useMemo(() => getIconStyles(isDarkMode), [isDarkMode]);
const icon = useMemo(
() =>
isOpen ? (
<CaretDownFilled style={iconStyles} />
) : (
<CaretRightFilled style={iconStyles} />
),
[isOpen, iconStyles],
);
return (
<Wrapper
onMouseEnter={onMouseEnterHandler}
@ -136,10 +150,8 @@ function Trace(props: TraceProps): JSX.Element {
isDarkMode={isDarkMode}
onClick={onClickTreeExpansion}
>
{totalSpans}
<CaretContainer>
{isOpen ? <CaretDownFilled /> : <CaretRightFilled />}
</CaretContainer>
<Typography>{totalSpans}</Typography>
<CaretContainer>{icon}</CaretContainer>
</CardComponent>
)}
</Col>

View File

@ -0,0 +1,3 @@
export const getIconStyles = (isDarkMode: boolean): Record<string, string> => ({
color: isDarkMode ? 'white' : 'black',
});

View File

@ -572,7 +572,7 @@ function GeneralSettings({
onOkHandler(category.name.toLowerCase() as TTTLType)
}
centered
visible={category.save.modal}
open={category.save.modal}
confirmLoading={category.save.apiLoading}
>
<Typography>

View File

@ -1,4 +1,4 @@
import { Button, Typography } from 'antd';
import { Button } from 'antd';
import { GraphOnClickHandler } from 'components/Graph';
import Spinner from 'components/Spinner';
import TimePreference from 'components/TimePreferenceDropDown';
@ -19,7 +19,7 @@ import { Widgets } from 'types/api/dashboard/getAll';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { GlobalReducer } from 'types/reducer/globalTime';
import { NotFoundContainer, TimeContainer } from './styles';
import { TimeContainer } from './styles';
function FullView({
widget,
@ -57,20 +57,11 @@ function FullView({
}),
);
const isError = response?.error;
const isLoading = response.isLoading === true;
const errorMessage = isError instanceof Error ? isError?.message : '';
if (isLoading) {
return <Spinner height="100%" size="large" tip="Loading..." />;
}
if (isError || !response?.data?.payload?.data?.result) {
return (
<NotFoundContainer>
<Typography>{errorMessage}</Typography>
</NotFoundContainer>
);
}
return (
<>

View File

@ -1,14 +1,5 @@
import styled from 'styled-components';
export const GraphContainer = styled.div`
min-height: 70vh;
align-items: center;
justify-content: center;
display: flex;
flex-direction: column;
width: 100%;
`;
export const NotFoundContainer = styled.div`
display: flex;
justify-content: center;

View File

@ -183,7 +183,7 @@ function GridCardGraph({
<Modal
destroyOnClose
onCancel={(): void => onToggleModal(setDeleteModal)}
visible={deleteModal}
open={deleteModal}
title="Delete"
height="10vh"
onOk={onDeleteHandler}
@ -196,7 +196,7 @@ function GridCardGraph({
title="View"
footer={[]}
centered
visible={modal}
open={modal}
onCancel={(): void => onToggleModal(setModal)}
width="85%"
destroyOnClose

View File

@ -1,5 +1,6 @@
import { PlusOutlined, SaveFilled } from '@ant-design/icons';
import useComponentPermission from 'hooks/useComponentPermission';
import { useIsDarkMode } from 'hooks/useDarkMode';
import React from 'react';
import { Layout } from 'react-grid-layout';
import { useSelector } from 'react-redux';
@ -27,7 +28,7 @@ function GraphLayout({
setLayout,
}: GraphLayoutProps): JSX.Element {
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
const isDarkMode = useIsDarkMode();
const [saveLayoutPermission, addPanelPermission] = useComponentPermission(
['save_layout', 'add_panel'],

View File

@ -3,67 +3,39 @@ import {
CaretUpFilled,
LogoutOutlined,
} from '@ant-design/icons';
import {
Avatar,
Divider,
Dropdown,
Layout,
Menu,
Space,
Typography,
} from 'antd';
import { Divider, Dropdown, Menu, Space, Typography } from 'antd';
import { Logout } from 'api/utils';
import ROUTES from 'constants/routes';
import Config from 'container/ConfigDropdown';
import setTheme, { AppMode } from 'lib/theme/setTheme';
import { useIsDarkMode, useThemeMode } from 'hooks/useDarkMode';
import React, { Dispatch, SetStateAction, useCallback, useState } from 'react';
import { connect, useSelector } from 'react-redux';
import { useSelector } from 'react-redux';
import { NavLink } from 'react-router-dom';
import { bindActionCreators } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import { ToggleDarkMode } from 'store/actions';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import AppReducer from 'types/reducer/app';
import CurrentOrganization from './CurrentOrganization';
import ManageLicense from './ManageLicense';
import SignedInAS from './SignedInAs';
import {
AvatarWrapper,
Container,
Header,
IconContainer,
LogoutContainer,
NavLinkWrapper,
ToggleButton,
} from './styles';
function HeaderContainer({ toggleDarkMode }: Props): JSX.Element {
const { isDarkMode, user, currentVersion } = useSelector<AppState, AppReducer>(
function HeaderContainer(): JSX.Element {
const { user, currentVersion } = useSelector<AppState, AppReducer>(
(state) => state.app,
);
const isDarkMode = useIsDarkMode();
const { toggleTheme } = useThemeMode();
const [isUserDropDownOpen, setIsUserDropDownOpen] = useState<boolean>(false);
const onToggleThemeHandler = useCallback(() => {
const preMode: AppMode = isDarkMode ? 'lightMode' : 'darkMode';
setTheme(preMode);
const id: AppMode = preMode;
const { head } = document;
const link = document.createElement('link');
link.rel = 'stylesheet';
link.type = 'text/css';
link.href = !isDarkMode ? '/css/antd.dark.min.css' : '/css/antd.min.css';
link.media = 'all';
link.id = id;
head.appendChild(link);
link.onload = (): void => {
toggleDarkMode();
const prevNode = document.getElementById('appMode');
prevNode?.remove();
};
}, [toggleDarkMode, isDarkMode]);
const onToggleHandler = useCallback(
(functionToExecute: Dispatch<SetStateAction<boolean>>) => (): void => {
functionToExecute((state) => !state);
@ -100,15 +72,18 @@ function HeaderContainer({ toggleDarkMode }: Props): JSX.Element {
);
return (
<Layout.Header>
<Header>
<Container>
<NavLink to={ROUTES.APPLICATION}>
<Space align="center" direction="horizontal">
<NavLinkWrapper>
<img src={`/signoz.svg?currentVersion=${currentVersion}`} alt="SigNoz" />
<Typography.Title style={{ margin: 0, color: '#DBDBDB' }} level={4}>
<Typography.Title
style={{ margin: 0, color: 'rgb(219, 219, 219)' }}
level={4}
>
SigNoz
</Typography.Title>
</Space>
</NavLinkWrapper>
</NavLink>
<Space style={{ height: '100%' }} align="center">
@ -116,7 +91,7 @@ function HeaderContainer({ toggleDarkMode }: Props): JSX.Element {
<ToggleButton
checked={isDarkMode}
onChange={onToggleThemeHandler}
onChange={toggleTheme}
defaultChecked={isDarkMode}
checkedChildren="🌜"
unCheckedChildren="🌞"
@ -129,7 +104,7 @@ function HeaderContainer({ toggleDarkMode }: Props): JSX.Element {
visible={isUserDropDownOpen}
>
<Space>
<Avatar shape="circle">{user?.name[0]}</Avatar>
<AvatarWrapper shape="circle">{user?.name[0]}</AvatarWrapper>
<IconContainer>
{!isUserDropDownOpen ? <CaretDownFilled /> : <CaretUpFilled />}
</IconContainer>
@ -137,20 +112,8 @@ function HeaderContainer({ toggleDarkMode }: Props): JSX.Element {
</Dropdown>
</Space>
</Container>
</Layout.Header>
</Header>
);
}
interface DispatchProps {
toggleDarkMode: () => void;
}
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
toggleDarkMode: bindActionCreators(ToggleDarkMode, dispatch),
});
type Props = DispatchProps;
export default connect(null, mapDispatchToProps)(HeaderContainer);
export default HeaderContainer;

View File

@ -1,10 +1,14 @@
import { Switch, Typography } from 'antd';
import { Avatar, Layout, Switch, Typography } from 'antd';
import styled from 'styled-components';
export const Header = styled(Layout.Header)`
background: #1f1f1f !important;
`;
export const Container = styled.div`
display: flex;
justify-content: space-between;
height: 100%;
height: 4rem;
`;
export const AvatarContainer = styled.div`
@ -66,3 +70,15 @@ export const ToggleButton = styled(Switch)<DarkModeProps>`
export const IconContainer = styled.div`
color: white;
`;
export const NavLinkWrapper = styled.div`
display: flex;
align-items: center;
justify-content: center;
height: 100%;
gap: 0.5rem;
`;
export const AvatarWrapper = styled(Avatar)`
background-color: rgba(255, 255, 255, 0.25);
`;

View File

@ -1,4 +1,4 @@
import { NotificationInstance } from 'antd/lib/notification/index';
import { NotificationInstance } from 'antd/es/notification/interface';
import deleteAlerts from 'api/alerts/delete';
import { State } from 'hooks/useFetch';
import React, { useState } from 'react';

View File

@ -1,7 +1,7 @@
/* eslint-disable react/display-name */
import { PlusOutlined } from '@ant-design/icons';
import { notification, Typography } from 'antd';
import Table, { ColumnsType } from 'antd/lib/table';
import { notification, Table, Typography } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import TextToolTip from 'components/TextToolTip';
import ROUTES from 'constants/routes';
import useComponentPermission from 'hooks/useComponentPermission';

View File

@ -131,7 +131,7 @@ function ImportJSON({
return (
<Modal
visible={isImportJSONModalVisible}
open={isImportJSONModalVisible}
centered
maskClosable
destroyOnClose

View File

@ -5,10 +5,7 @@ import { RefSelectProps } from 'antd/lib/select';
import history from 'lib/history';
import { filter, map } from 'lodash-es';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { Dashboard } from 'types/api/dashboard/getAll';
import AppReducer from 'types/reducer/app';
import { v4 as uuidv4 } from 'uuid';
import { DashboardSearchAndFilter } from './Dashboard.machine';
@ -30,7 +27,6 @@ function SearchFilter({
searchData: Dashboard[];
filterDashboards: (filteredDashboards: Dashboard[]) => void;
}): JSX.Element {
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
const [category, setCategory] = useState<TCategory>();
const [optionsData, setOptionsData] = useState<IOptionsData>(
OptionsSchemas.attribute,
@ -154,14 +150,8 @@ function SearchFilter({
};
return (
<SearchContainer isDarkMode={isDarkMode}>
<div
style={{
maxWidth: '70%',
display: 'flex',
overflowX: 'auto',
}}
>
<SearchContainer>
<div>
{map(queries, (query) => (
<QueryChip key={query.id} queryData={query} onRemove={removeQueryById} />
))}

View File

@ -2,10 +2,7 @@ import { grey } from '@ant-design/colors';
import { Tag } from 'antd';
import styled from 'styled-components';
export const SearchContainer = styled.div<{
isDarkMode: boolean;
}>`
background: ${({ isDarkMode }): string => (isDarkMode ? '#000' : '#fff')};
export const SearchContainer = styled.div`
width: 100%;
display: flex;
align-items: center;

View File

@ -45,7 +45,7 @@ function TableView({ logData }: TableViewProps): JSX.Element | null {
const columns = [
{
title: 'Action',
width: 75,
width: 100,
render: (fieldData: Record<string, string>): JSX.Element | null => {
const fieldKey = fieldData.field.split('.').slice(-1);
if (!RESTRICTED_FIELDS.includes(fieldKey[0])) {

View File

@ -1,20 +1,22 @@
import { Drawer, Tabs } from 'antd';
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Dispatch } from 'redux';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import { SET_DETAILED_LOG_DATA } from 'types/actions/logs';
import { ILogsReducer } from 'types/reducer/logs';
import JSONView from './JsonView';
import TableView from './TableView';
const { TabPane } = Tabs;
function LogDetailedView(): JSX.Element {
const { detailedLog } = useSelector<AppState, ILogsReducer>(
(state) => state.logs,
);
const dispatch = useDispatch();
const dispatch = useDispatch<Dispatch<AppActions>>();
const onDrawerClose = (): void => {
dispatch({
type: SET_DETAILED_LOG_DATA,
@ -23,30 +25,25 @@ function LogDetailedView(): JSX.Element {
};
return (
<div style={{}}>
<Drawer
width="60%"
title="Log Details"
placement="right"
closable
mask={false}
onClose={onDrawerClose}
visible={detailedLog !== null}
getContainer={false}
style={{ overscrollBehavior: 'contain' }}
>
{detailedLog && (
<Tabs defaultActiveKey="1">
<TabPane tab="Table" key="1">
<TableView logData={detailedLog} />
</TabPane>
<TabPane tab="JSON" key="2">
<JSONView logData={detailedLog} />
</TabPane>
</Tabs>
)}
</Drawer>
</div>
<Drawer
width="60%"
title="Log Details"
placement="right"
closable
onClose={onDrawerClose}
open={detailedLog !== null}
style={{ overscrollBehavior: 'contain' }}
destroyOnClose
>
<Tabs defaultActiveKey="1">
<Tabs.TabPane tab="Table" key="1">
{detailedLog && <TableView logData={detailedLog} />}
</Tabs.TabPane>
<Tabs.TabPane tab="JSON" key="2">
{detailedLog && <JSONView logData={detailedLog} />}
</Tabs.TabPane>
</Tabs>
</Drawer>
);
}

View File

@ -7,6 +7,7 @@ import {
import { Button, Popover, Select, Space } from 'antd';
import { LiveTail } from 'api/logs/livetail';
import dayjs from 'dayjs';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { throttle } from 'lodash-es';
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import { useDispatch, useSelector } from 'react-redux';
@ -19,7 +20,6 @@ import {
TOGGLE_LIVE_TAIL,
} from 'types/actions/logs';
import { TLogsLiveTailState } from 'types/api/logs/liveTail';
import AppReducer from 'types/reducer/app';
import { GlobalReducer } from 'types/reducer/globalTime';
import { ILogsReducer } from 'types/reducer/logs';
@ -35,7 +35,8 @@ function LogLiveTail(): JSX.Element {
liveTailStartRange,
logs,
} = useSelector<AppState, ILogsReducer>((state) => state.logs);
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
const isDarkMode = useIsDarkMode();
const { selectedAutoRefreshInterval } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);

View File

@ -1,9 +1,7 @@
import { LoadingOutlined } from '@ant-design/icons';
import { Button, Popover, Spin } from 'antd';
import { useIsDarkMode } from 'hooks/useDarkMode';
import React, { useState } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import AppReducer from 'types/reducer/app';
import { Field } from './styles';
@ -26,7 +24,7 @@ export function FieldItem({
iconHoverText,
}: FieldItemProps): JSX.Element {
const [isHovered, setIsHovered] = useState(false);
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
const isDarkMode = useIsDarkMode();
return (
<Field
onMouseEnter={(): void => {

View File

@ -2,7 +2,7 @@
import { Typography } from 'antd';
import LogItem from 'components/Logs/LogItem';
import Spinner from 'components/Spinner';
import { map } from 'lodash-es';
import map from 'lodash-es/map';
import React, { memo } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
@ -22,14 +22,7 @@ function LogsTable(): JSX.Element {
return (
<Container flex="auto">
<Heading>
<Typography.Text
style={{
fontSize: '1rem',
fontWeight: 400,
}}
>
Event
</Typography.Text>
<Typography.Text>Event</Typography.Text>
</Heading>
{Array.isArray(logs) && logs.length > 0 ? (
map(logs, (log) => <LogItem key={log.id} logData={log} />)

View File

@ -10,7 +10,6 @@ import { useDispatch, useSelector } from 'react-redux';
import { ResetInitialData } from 'store/actions/metrics/resetInitialData';
import { SetResourceAttributeQueries } from 'store/actions/metrics/setResourceAttributeQueries';
import { AppState } from 'store/reducers';
import AppReducer from 'types/reducer/app';
import MetricReducer from 'types/reducer/metrics';
import { v4 as uuid } from 'uuid';
@ -38,7 +37,6 @@ function ResourceAttributesFilter(): JSX.Element | null {
};
}, [dispatch]);
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
const { resourceAttributeQueries } = useSelector<AppState, MetricReducer>(
(state) => state.metrics,
);
@ -156,7 +154,7 @@ function ResourceAttributesFilter(): JSX.Element | null {
}
return (
<SearchContainer isDarkMode={isDarkMode} disabled={disabled}>
<SearchContainer disabled={disabled}>
<div
style={{
maxWidth: disabled ? '100%' : '70%',

View File

@ -3,10 +3,8 @@ import { Tag } from 'antd';
import styled from 'styled-components';
export const SearchContainer = styled.div<{
isDarkMode: boolean;
disabled: boolean;
}>`
background: ${({ isDarkMode }): string => (isDarkMode ? '#000' : '#fff')};
width: 100%;
display: flex;
align-items: center;

View File

@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { notification } from 'antd';
import { useIsDarkMode } from 'hooks/useDarkMode';
import history from 'lib/history';
import React, { useCallback } from 'react';
import { connect, useSelector } from 'react-redux';
@ -11,7 +12,6 @@ import {
} from 'store/actions/dashboard/toggleAddWidget';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import AppReducer from 'types/reducer/app';
import DashboardReducer from 'types/reducer/dashboards';
import menuItems, { ITEMS } from './menuItems';
@ -50,7 +50,7 @@ function DashboardGraphSlider({ toggleAddWidget }: Props): JSX.Element {
},
[data, toggleAddWidget],
);
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
const isDarkMode = useIsDarkMode();
const fillColor: React.CSSProperties['color'] = isDarkMode ? 'white' : 'black';
return (

View File

@ -165,7 +165,7 @@ function VariablesSetting({
<Modal
title="Delete variable"
centered
visible={deleteVariableModal}
open={deleteVariableModal}
onOk={handleDeleteConfirm}
onCancel={handleDeleteCancel}
>

View File

@ -83,7 +83,7 @@ function ShareModal({
return (
<Modal
visible={isJSONModalVisible}
open={isJSONModalVisible}
onCancel={(): void => {
onToggleHandler();
setIsViewJSON(false);

View File

@ -1,12 +1,10 @@
import { CloseCircleFilled } from '@ant-design/icons';
import { useMachine } from '@xstate/react';
import { Button, Select, Spin } from 'antd';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { map } from 'lodash-es';
import React, { useCallback, useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { IMetricsBuilderQuery } from 'types/api/dashboard/getAll';
import AppReducer from 'types/reducer/app';
import { v4 as uuid } from 'uuid';
import { ResourceAttributesFilterMachine } from './MetricTagKey.machine';
@ -32,7 +30,7 @@ function MetricTagKeyFilter({
onSetQuery,
selectedTagFilters: selectedTagQueries,
}: IMetricTagKeyFilterProps): JSX.Element | null {
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
const isDarkMode = useIsDarkMode();
const [loading, setLoading] = useState(true);
const [selectedValues, setSelectedValues] = useState<string[]>([]);
const [staging, setStaging] = useState<string[]>([]);

View File

@ -213,7 +213,7 @@ function NewWidget({
onClickSaveHandler();
}}
centered
visible={saveModal}
open={saveModal}
width={600}
>
{hasUnstagedChanges ? (

View File

@ -69,7 +69,7 @@ function AddDomain({ refetch }: Props): JSX.Element {
centered
title="Add Domain"
footer={null}
visible={isAddDomains}
open={isAddDomains}
destroyOnClose
onCancel={(): void => setIsDomain(false)}
>

View File

@ -256,7 +256,7 @@ function AuthDomains(): JSX.Element {
title="Configure Authentication Method"
onCancel={onCloseHandler(setIsSettingsOpen)}
destroyOnClose
visible={isSettingsOpen}
open={isSettingsOpen}
footer={null}
>
<Create
@ -285,7 +285,7 @@ function AuthDomains(): JSX.Element {
title="Configure Authentication Method"
onCancel={onCloseHandler(setIsSettingsOpen)}
destroyOnClose
visible={isSettingsOpen}
open={isSettingsOpen}
footer={null}
>
<Create
@ -297,7 +297,7 @@ function AuthDomains(): JSX.Element {
</Modal>
<Modal
visible={isEditModalOpen}
open={isEditModalOpen}
centered
title={EditModalTitleText(currentDomain?.ssoType)}
onCancel={onCloseHandler(setIsEditModalOpen)}

View File

@ -1,4 +1,4 @@
import { Button, Input, notification, Space, Typography } from 'antd';
import { Button, Form, Input, notification } from 'antd';
import editOrg from 'api/user/editOrg';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
@ -14,14 +14,15 @@ function DisplayName({
id: orgId,
isAnonymous,
}: DisplayNameProps): JSX.Element {
const [form] = Form.useForm();
const { t } = useTranslation(['organizationsettings', 'common']);
const { org } = useSelector<AppState, AppReducer>((state) => state.app);
const { name } = (org || [])[index];
const [orgName, setOrgName] = useState<string>(name);
const [isLoading, setIsLoading] = useState<boolean>(false);
const dispatch = useDispatch<Dispatch<AppActions>>();
const onClickHandler = async (): Promise<void> => {
const onSubmit = async ({ name: orgName }: OnSubmitProps): Promise<void> => {
try {
setIsLoading(true);
const { statusCode, error } = await editOrg({
@ -67,26 +68,22 @@ function DisplayName({
}
return (
<Space direction="vertical">
<Typography.Title level={3}>{t('display_name')}</Typography.Title>
<Space direction="vertical" size="middle">
<Input
value={orgName}
onChange={(e): void => setOrgName(e.target.value)}
size="large"
placeholder={t('signoz')}
disabled={isLoading}
/>
<Button
onClick={onClickHandler}
disabled={isLoading || orgName === name}
loading={isLoading}
type="primary"
>
Change Org Name
<Form
initialValues={{ name }}
form={form}
layout="vertical"
onFinish={onSubmit}
autoComplete="off"
>
<Form.Item name="name" label="Display name" rules={[{ required: true }]}>
<Input size="large" placeholder={t('signoz')} disabled={isLoading} />
</Form.Item>
<Form.Item>
<Button loading={isLoading} type="primary" htmlType="submit">
Submit
</Button>
</Space>
</Space>
</Form.Item>
</Form>
);
}
@ -96,4 +93,8 @@ interface DisplayNameProps {
isAnonymous: boolean;
}
interface OnSubmitProps {
name: string;
}
export default DisplayName;

View File

@ -1,5 +1,5 @@
import { Button, Modal, notification, Space, Typography } from 'antd';
import Table, { ColumnsType } from 'antd/lib/table';
import { Button, Modal, notification, Space, Table, Typography } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import deleteUser from 'api/user/deleteUser';
import editUserApi from 'api/user/editUser';
import getOrgUser from 'api/user/getOrgUser';
@ -176,7 +176,7 @@ function UserFunction({
</Space>
<Modal
title="Edit member details"
visible={isModalVisible}
open={isModalVisible}
onOk={(): void => onModalToggleHandler(setIsModalVisible, false)}
onCancel={(): void => onModalToggleHandler(setIsModalVisible, false)}
centered
@ -214,7 +214,7 @@ function UserFunction({
</Modal>
<Modal
title="Edit member details"
visible={isDeleteModalVisible}
open={isDeleteModalVisible}
onOk={onDeleteHandler}
onCancel={(): void => onModalToggleHandler(setIsDeleteModalVisible, false)}
centered

View File

@ -1,6 +1,6 @@
import { PlusOutlined } from '@ant-design/icons';
import { Button, Modal, notification, Space, Typography } from 'antd';
import Table, { ColumnsType } from 'antd/lib/table';
import { Button, Modal, notification, Space, Table, Typography } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import deleteInvite from 'api/user/deleteInvite';
import getPendingInvites from 'api/user/getPendingInvites';
import sendInvite from 'api/user/sendInvite';
@ -222,7 +222,7 @@ function PendingInvitesContainer(): JSX.Element {
<div>
<Modal
title={t('invite_team_members')}
visible={isInviteTeamMemberModalOpen}
open={isInviteTeamMemberModalOpen}
onCancel={(): void => toggleModal(false)}
centered
destroyOnClose

View File

@ -0,0 +1 @@
export const styles = { background: '#1f1f1f' };

View File

@ -4,7 +4,7 @@ import getLocalStorageKey from 'api/browser/localstorage/get';
import { IS_SIDEBAR_COLLAPSED } from 'constants/app';
import ROUTES from 'constants/routes';
import history from 'lib/history';
import React, { useCallback, useLayoutEffect, useState } from 'react';
import React, { useCallback, useLayoutEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom';
@ -12,6 +12,7 @@ import { SideBarCollapse } from 'store/actions/app';
import { AppState } from 'store/reducers';
import AppReducer from 'types/reducer/app';
import { styles } from './config';
import menus from './menuItems';
import Slack from './Slack';
import {
@ -89,7 +90,10 @@ function SideNav(): JSX.Element {
},
];
const currentMenu = menus.find((menu) => pathname.startsWith(menu.to));
const currentMenu = useMemo(
() => menus.find((menu) => pathname.startsWith(menu.to)),
[pathname],
);
return (
<Sider collapsible collapsed={collapsed} onCollapse={onCollapse} width={200}>
@ -98,6 +102,7 @@ function SideNav(): JSX.Element {
defaultSelectedKeys={[ROUTES.APPLICATION]}
selectedKeys={currentMenu ? [currentMenu?.to] : []}
mode="inline"
style={styles}
>
{menus.map(({ to, Icon, name, tags }) => (
<Menu.Item

View File

@ -10,18 +10,19 @@ interface LogoProps {
}
export const Sider = styled(SiderComponent)`
z-index: 999;
.ant-typography {
color: white;
}
.ant-layout-sider-trigger {
background-color: #1f1f1f;
&&& {
background: #1f1f1f;
.ant-layout-sider-trigger {
background: #1f1f1f;
}
}
`;
export const SlackButton = styled(Typography)`
&&& {
margin-left: 1rem;
color: white;
}
`;
@ -31,6 +32,7 @@ export const SlackMenuItemContainer = styled.div<LogoProps>`
background: #262626;
width: ${({ collapsed }): string => (!collapsed ? '200px' : '80px')};
transition: inherit;
background: #1f1f1f;
&&& {
li {

View File

@ -1,7 +1,7 @@
import { StyledDiv } from 'components/Styled';
import { ITraceMetaData } from 'container/GantChart';
import { IIntervalUnit, INTERVAL_UNITS } from 'container/TraceDetail/utils';
import useThemeMode from 'hooks/useThemeMode';
import { useIsDarkMode } from 'hooks/useDarkMode';
import React, { useEffect, useState } from 'react';
import { useMeasure } from 'react-use';
@ -18,7 +18,7 @@ function Timeline({
setIntervalUnit,
}: TimelineProps): JSX.Element {
const [ref, { width }] = useMeasure<HTMLDivElement>();
const { isDarkMode } = useThemeMode();
const isDarkMode = useIsDarkMode();
const [intervals, setIntervals] = useState<Interval[] | null>(null);

View File

@ -1,6 +1,5 @@
/* eslint-disable react/jsx-no-bind */
import { Modal } from 'antd';
import DatePicker from 'components/DatePicker';
import { DatePicker, Modal } from 'antd';
import dayjs, { Dayjs } from 'dayjs';
import React, { useState } from 'react';
@ -13,22 +12,22 @@ function CustomDateTimeModal({
onCreate,
onCancel,
}: CustomDateTimeModalProps): JSX.Element {
const [
customDateTimeRange,
setCustomDateTimeRange,
] = useState<DateTimeRangeType>();
const [customDateTimeRange, setCustomDateTimeRange] = useState();
function handleRangePickerOk(date_time: DateTimeRangeType): void {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function handleRangePickerOk(date_time: any): void {
setCustomDateTimeRange(date_time);
}
function disabledDate(current: Dayjs): boolean {
return current > dayjs();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function disabledDate(current: any): boolean {
const currentDay = dayjs(current);
return currentDay.isAfter(dayjs());
}
return (
<Modal
visible={visible}
open={visible}
title="Chose date and time range"
okText="Apply"
cancelText="Cancel"

View File

@ -1,5 +1,5 @@
import { TableProps, Tag, Typography } from 'antd';
import Table, { ColumnsType } from 'antd/lib/table';
import { Table, TableProps, Tag, Typography } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import ROUTES from 'constants/routes';
import {
getSpanOrder,

View File

@ -1,5 +1,5 @@
import { Collapse } from 'antd';
import useThemeMode from 'hooks/useThemeMode';
import { useIsDarkMode } from 'hooks/useDarkMode';
import keys from 'lodash-es/keys';
import map from 'lodash-es/map';
import React from 'react';
@ -18,7 +18,7 @@ function ErrorTag({
setText,
firstSpanStartTime,
}: ErrorTagProps): JSX.Element {
const { isDarkMode } = useThemeMode();
const isDarkMode = useIsDarkMode();
return (
<>

View File

@ -1,12 +1,12 @@
import { Popover } from 'antd';
import dayjs from 'dayjs';
import useThemeMode from 'hooks/useThemeMode';
import { useIsDarkMode } from 'hooks/useDarkMode';
import React from 'react';
import { CustomSubText, CustomSubTitle } from '../styles';
function EventStartTime({ timeUnixNano }: EventStartTimeProps): JSX.Element {
const { isDarkMode } = useThemeMode();
const isDarkMode = useIsDarkMode();
const humanReadableTimeInDayJs = dayjs(timeUnixNano / 1e6).format(
'YYYY-MM-DD hh:mm:ss.SSS A',

View File

@ -1,7 +1,7 @@
import { InfoCircleOutlined } from '@ant-design/icons';
import { Popover, Space } from 'antd';
import { convertTimeToRelevantUnit } from 'container/TraceDetail/utils';
import useThemeMode from 'hooks/useThemeMode';
import { useIsDarkMode } from 'hooks/useDarkMode';
import React from 'react';
import { CustomSubText, CustomSubTitle } from '../styles';
@ -10,7 +10,7 @@ function StartTime({
firstSpanStartTime,
timeUnixNano,
}: StartTimeProps): JSX.Element {
const { isDarkMode } = useThemeMode();
const isDarkMode = useIsDarkMode();
const { time, timeUnitName } = convertTimeToRelevantUnit(
timeUnixNano / 1e6 - (firstSpanStartTime || 0),

View File

@ -1,9 +1,7 @@
import { Tooltip } from 'antd';
import { useIsDarkMode } from 'hooks/useDarkMode';
import React, { useMemo } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { ITraceTag } from 'types/api/trace/getTraceItem';
import AppReducer from 'types/reducer/app';
import EllipsedButton from '../EllipsedButton';
import { CustomSubText, CustomSubTitle, SubTextContainer } from '../styles';
@ -11,7 +9,7 @@ import { CommonTagsProps } from '.';
import { Container } from './styles';
function Tag({ tags, onToggleHandler, setText }: TagProps): JSX.Element {
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
const isDarkMode = useIsDarkMode();
const { value, isEllipsed } = useMemo(() => {
const value = tags.key === 'error' ? 'true' : tags.value;

View File

@ -1,7 +1,7 @@
import { Modal, Tabs, Tooltip } from 'antd';
import Editor from 'components/Editor';
import { StyledSpace } from 'components/Styled';
import useThemeMode from 'hooks/useThemeMode';
import { useIsDarkMode } from 'hooks/useDarkMode';
import React, { useMemo, useState } from 'react';
import { ITraceTree } from 'types/api/trace/getTraceItem';
@ -20,7 +20,7 @@ const { TabPane } = Tabs;
function SelectedSpanDetails(props: SelectedSpanDetailsProps): JSX.Element {
const { tree, firstSpanStartTime } = props;
const { isDarkMode } = useThemeMode();
const isDarkMode = useIsDarkMode();
const OverLayComponentName = useMemo(() => tree?.name, [tree?.name]);
const OverLayComponentServiceName = useMemo(() => tree?.serviceName, [
@ -67,7 +67,7 @@ function SelectedSpanDetails(props: SelectedSpanDetailsProps): JSX.Element {
<Modal
onCancel={(): void => onToggleHandler(false)}
title={text.text}
visible={isOpen}
open={isOpen}
destroyOnClose
footer={[]}
width="70vw"

View File

@ -1,7 +1,7 @@
/* eslint-disable react/no-unstable-nested-components */
import Color from 'color';
import { ITraceMetaData } from 'container/GantChart';
import useThemeMode from 'hooks/useThemeMode';
import { useIsDarkMode } from 'hooks/useDarkMode';
import React, { useLayoutEffect, useMemo, useState } from 'react';
import { ITraceTree } from 'types/api/trace/getTraceItem';
@ -37,7 +37,7 @@ function SpanItem({
const { serviceColour } = spanData;
const [isSelected, setIsSelected] = useState<boolean>(false);
// const [isLocalHover, setIsLocalHover] = useState<boolean>(false);
const { isDarkMode } = useThemeMode();
const isDarkMode = useIsDarkMode();
useLayoutEffect(() => {
if (

View File

@ -0,0 +1,4 @@
export const THEME_MODE = {
LIGHT: 'light',
DARK: 'dark',
};

View File

@ -0,0 +1,74 @@
import { theme as antdTheme } from 'antd';
import { ThemeConfig } from 'antd/es/config-provider/context';
import get from 'api/browser/localstorage/get';
import set from 'api/browser/localstorage/set';
import { LOCALSTORAGE } from 'constants/localStorage';
import React, { createContext, useCallback, useMemo, useState } from 'react';
import { THEME_MODE } from './constant';
export const ThemeContext = createContext({
theme: THEME_MODE.DARK,
toggleTheme: () => {},
});
export function ThemeProvider({ children }: ThemeProviderProps): JSX.Element {
const [theme, setTheme] = useState(get(LOCALSTORAGE.THEME) || THEME_MODE.DARK);
const toggleTheme = useCallback(() => {
if (theme === THEME_MODE.LIGHT) {
setTheme(THEME_MODE.DARK);
set(LOCALSTORAGE.THEME, THEME_MODE.DARK);
} else {
setTheme(THEME_MODE.LIGHT);
set(LOCALSTORAGE.THEME, THEME_MODE.LIGHT);
}
}, [theme]);
const value = useMemo(
() => ({
theme,
toggleTheme,
}),
[theme, toggleTheme],
);
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
}
interface ThemeProviderProps {
children: React.ReactNode;
}
interface ThemeMode {
theme: string;
toggleTheme: () => void;
}
export const useThemeMode = (): ThemeMode => {
const { theme, toggleTheme } = React.useContext(ThemeContext);
return { theme, toggleTheme };
};
export const useIsDarkMode = (): boolean => {
const { theme } = React.useContext(ThemeContext);
return theme === THEME_MODE.DARK;
};
export const useThemeConfig = (): ThemeConfig => {
const isDarkMode = useIsDarkMode();
return {
algorithm: isDarkMode ? antdTheme.darkAlgorithm : antdTheme.defaultAlgorithm,
token: {
borderRadius: 2,
borderRadiusLG: 2,
borderRadiusSM: 2,
borderRadiusXS: 2,
},
};
};
export default useThemeMode;

View File

@ -1,15 +0,0 @@
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import AppReducer from 'types/reducer/app';
export interface IUseThemeModeReturn {
isDarkMode: boolean;
}
const useThemeMode = (): IUseThemeModeReturn => {
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
return { isDarkMode };
};
export default useThemeMode;

View File

@ -50,27 +50,9 @@
<meta data-react-helmet="true" name="docusaurus_locale" content="en" />
<meta data-react-helmet="true" name="docusaurus_tag" content="default" />
<link data-react-helmet="true" rel="shortcut icon" href="/favicon.ico" />
<link
id="appMode"
rel="stylesheet"
type="text/css"
rel="preload"
/>
</head>
<body>
<body style="margin: 0; padding: 0; box-sizing: border-box;">
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script>
const themeNode = document.getElementById('appMode');
let userTheme = localStorage.getItem('theme') || "";
if (userTheme === 'lightMode') {
themeNode.setAttribute('href', '/css/antd.min.css');
} else {
themeNode.setAttribute('href', '/css/antd.dark.min.css');
}
</script>
</body>
</html>

View File

@ -2,6 +2,7 @@ import './wdyr';
import './ReactI18';
import AppRoutes from 'AppRoutes';
import { ThemeProvider } from 'hooks/useDarkMode';
import React from 'react';
import ReactDOM from 'react-dom';
import { QueryClient, QueryClientProvider } from 'react-query';
@ -23,15 +24,17 @@ const queryClient = new QueryClient({
});
ReactDOM.render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<React.StrictMode>
<AppRoutes />
</React.StrictMode>
</Provider>
{process.env.NODE_ENV === 'development' && (
<ReactQueryDevtools initialIsOpen />
)}
</QueryClientProvider>,
<ThemeProvider>
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<React.StrictMode>
<AppRoutes />
</React.StrictMode>
</Provider>
{process.env.NODE_ENV === 'development' && (
<ReactQueryDevtools initialIsOpen />
)}
</QueryClientProvider>
</ThemeProvider>,
document.querySelector('#root'),
);

View File

@ -1,14 +0,0 @@
import getLocalStorageKey from 'api/browser/localstorage/get';
import { AppMode } from './setTheme';
const getTheme = (): AppMode => {
const userTheme = getLocalStorageKey('theme');
if (userTheme === null || userTheme === 'darkMode') {
return 'darkMode';
}
return 'lightMode';
};
export default getTheme;

View File

@ -1,9 +0,0 @@
import setLocalStorageKey from 'api/browser/localstorage/set';
const setTheme = (value: AppMode): void => {
setLocalStorageKey('theme', value);
};
export type AppMode = 'darkMode' | 'lightMode';
export default setTheme;

View File

@ -1,84 +0,0 @@
/* eslint-disable */
//@ts-nocheck
import { InfoCircleOutlined } from '@ant-design/icons';
import { Select } from 'antd';
import { cloneDeep } from 'lodash-es';
import React, { useState } from 'react';
import { ServicesItem } from 'store/actions';
import styled from 'styled-components';
const { Option } = Select;
const Container = styled.div`
margin-top: 12px;
display: flex;
.info {
display: flex;
font-family: Roboto;
margin-left: auto;
margin-right: 12px;
color: #4f4f4f;
font-size: 14px;
.anticon-info-circle {
margin-top: 22px;
margin-right: 18px;
}
}
`;
interface SelectServiceProps {
services: ServicesItem[];
zoomToService: (arg0: string) => void;
zoomToDefault: () => void;
}
const defaultOption = {
serviceName: 'Default',
};
function SelectService(props: SelectServiceProps): JSX.Element {
const [selectedVal, setSelectedVal] = useState<string>(
defaultOption.serviceName,
);
const { zoomToService, zoomToDefault, services } = props;
const service = cloneDeep(services);
service.unshift(defaultOption);
const handleSelect = (value: string): void => {
if (value === defaultOption.serviceName) {
zoomToDefault();
} else {
zoomToService(value);
}
setSelectedVal(value);
};
return (
<Container>
<Select
style={{ width: 270, marginBottom: '56px' }}
placeholder="Select a service"
onChange={handleSelect}
value={selectedVal}
>
{service.map(({ serviceName }) => (
<Option key={serviceName} value={serviceName}>
{serviceName}
</Option>
))}
</Select>
<div className="info">
<InfoCircleOutlined />
<div>
<div>
&gt; Size of circles is proportial to the number of requests served by
each node
</div>
<div>&gt; Click on node name to reposition the node</div>
</div>
</div>
</Container>
);
}
export default SelectService;

View File

@ -3,17 +3,16 @@
import { Card } from 'antd';
import Spinner from 'components/Spinner';
import { useIsDarkMode } from 'hooks/useDarkMode';
import React, { useEffect, useRef } from 'react';
import { ForceGraph2D } from 'react-force-graph';
import { connect, useSelector } from 'react-redux';
import { connect } from 'react-redux';
import { RouteComponentProps, withRouter } from 'react-router-dom';
import { getDetailedServiceMapItems, ServiceMapStore } from 'store/actions';
import { AppState } from 'store/reducers';
import styled from 'styled-components';
import { GlobalTime } from 'types/actions/globalTime';
import AppReducer from 'types/reducer/app';
import SelectService from './SelectService';
import { getGraphData, getTooltip, getZoomPx, transformLabel } from './utils';
const Container = styled.div`
@ -61,7 +60,7 @@ export interface graphDataType {
function ServiceMap(props: ServiceMapProps): JSX.Element {
const fgRef = useRef();
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
const isDarkMode = useIsDarkMode();
const { getDetailedServiceMapItems, globalTime, serviceMap } = props;
@ -89,25 +88,10 @@ function ServiceMap(props: ServiceMapProps): JSX.Element {
);
}
const zoomToService = (value: string): void => {
fgRef &&
fgRef.current &&
fgRef.current.zoomToFit(700, getZoomPx(), (e) => e.id === value);
};
const zoomToDefault = () => {
fgRef && fgRef.current && fgRef.current.zoomToFit(100, 120);
};
const { nodes, links } = getGraphData(serviceMap, isDarkMode);
const graphData = { nodes, links };
return (
<Container>
{/* <SelectService
services={serviceMap.items}
zoomToService={zoomToService}
zoomToDefault={zoomToDefault}
/> */}
<ForceGraph2D
ref={fgRef}
cooldownTicks={100}
@ -153,12 +137,10 @@ const mapStateToProps = (
): {
serviceMap: serviceMapStore;
globalTime: GlobalTime;
} => {
return {
serviceMap: state.serviceMap,
globalTime: state.globalTime,
};
};
} => ({
serviceMap: state.serviceMap,
globalTime: state.globalTime,
});
export default withRouter(
connect(mapStateToProps, {

View File

@ -1,9 +1,7 @@
import { Typography } from 'antd';
import { useIsDarkMode } from 'hooks/useDarkMode';
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';
@ -15,7 +13,7 @@ interface IDocCardProps {
url: TGetStartedContentDoc['url'];
}
function DocCard({ icon, text, url }: IDocCardProps): JSX.Element {
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
const isDarkMode = useIsDarkMode();
return (
<Link to={{ pathname: `${url}${UTMParams}` }} target="_blank">

View File

@ -1,2 +1 @@
export * from './sideBarCollapse';
export * from './toggleDarkMode';

View File

@ -1,12 +0,0 @@
import { Dispatch } from 'redux';
import AppActions from 'types/actions';
export const ToggleDarkMode = (): ((
dispatch: Dispatch<AppActions>,
) => void) => {
return (dispatch: Dispatch<AppActions>): void => {
dispatch({
type: 'SWITCH_DARK_MODE',
});
};
};

View File

@ -1,13 +1,11 @@
import getLocalStorageKey from 'api/browser/localstorage/get';
import { IS_SIDEBAR_COLLAPSED } from 'constants/app';
import { LOCALSTORAGE } from 'constants/localStorage';
import getTheme from 'lib/theme/getTheme';
import { getInitialUserTokenRefreshToken } from 'store/utils';
import {
AppAction,
LOGGED_IN,
SIDEBAR_COLLAPSE,
SWITCH_DARK_MODE,
UPDATE_CONFIGS,
UPDATE_CURRENT_ERROR,
UPDATE_CURRENT_VERSION,
@ -45,7 +43,6 @@ const getInitialUser = (): User | null => {
};
const InitialValue: InitialValueTypes = {
isDarkMode: getTheme() === 'darkMode',
isLoggedIn: getLocalStorageKey(LOCALSTORAGE.IS_LOGGED_IN) === 'true',
isSideBarCollapsed: getLocalStorageKey(IS_SIDEBAR_COLLAPSED) === 'true',
currentVersion: '',
@ -67,13 +64,6 @@ const appReducer = (
action: AppAction,
): InitialValueTypes => {
switch (action.type) {
case SWITCH_DARK_MODE: {
return {
...state,
isDarkMode: !state.isDarkMode,
};
}
case LOGGED_IN: {
return {
...state,

View File

@ -7,7 +7,6 @@ import { UserFlags } from 'types/api/user/setFlags';
import AppReducer, { User } from 'types/reducer/app';
import { ROLES } from 'types/roles';
export const SWITCH_DARK_MODE = 'SWITCH_DARK_MODE';
export const LOGGED_IN = 'LOGGED_IN';
export const SIDEBAR_COLLAPSE = 'SIDEBAR_COLLAPSE';
@ -27,10 +26,6 @@ export const UPDATE_FEATURE_FLAGS = 'UPDATE_FEATURE_FLAGS';
export const UPDATE_CONFIGS = 'UPDATE_CONFIGS';
export const UPDATE_USER_FLAG = 'UPDATE_USER_FLAG';
export interface SwitchDarkMode {
type: typeof SWITCH_DARK_MODE;
}
export interface LoggedInUser {
type: typeof LOGGED_IN;
payload: {
@ -134,7 +129,6 @@ export interface UpdateConfigs {
}
export type AppAction =
| SwitchDarkMode
| LoggedInUser
| SideBarCollapse
| UpdateAppVersion

View File

@ -102,7 +102,7 @@ export interface SetLogsAggregateSeries {
}
export interface SetDetailedLogData {
type: typeof SET_DETAILED_LOG_DATA;
payload: ILog;
payload: ILog | null;
}
export interface ToggleLiveTail {

View File

@ -34,6 +34,11 @@ export enum EAggregateOperator {
RATE_AVG = 23,
RATE_MAX = 24,
RATE_MIN = 25,
HIST_QUANTILE_50 = 26,
HIST_QUANTILE_75 = 27,
HIST_QUANTILE_90 = 28,
HIST_QUANTILE_95 = 29,
HIST_QUANTILE_99 = 30,
}
export enum EPanelType {

View File

@ -15,7 +15,6 @@ export interface User {
}
export default interface AppReducer {
isDarkMode: boolean;
isLoggedIn: boolean;
isSideBarCollapsed: boolean;
currentVersion: string;

File diff suppressed because it is too large Load Diff

View File

@ -17,15 +17,20 @@ type RunQueries struct {
}
var AggregateOperatorToPercentile = map[model.AggregateOperator]float64{
model.P05: 0.5,
model.P10: 0.10,
model.P20: 0.20,
model.P25: 0.25,
model.P50: 0.50,
model.P75: 0.75,
model.P90: 0.90,
model.P95: 0.95,
model.P99: 0.99,
model.P05: 0.5,
model.P10: 0.10,
model.P20: 0.20,
model.P25: 0.25,
model.P50: 0.50,
model.P75: 0.75,
model.P90: 0.90,
model.P95: 0.95,
model.P99: 0.99,
model.HIST_QUANTILE_50: 0.50,
model.HIST_QUANTILE_75: 0.75,
model.HIST_QUANTILE_90: 0.90,
model.HIST_QUANTILE_95: 0.95,
model.HIST_QUANTILE_99: 0.99,
}
var AggregateOperatorToSQLFunc = map[model.AggregateOperator]string{
@ -173,6 +178,16 @@ func BuildMetricQuery(qp *model.QueryRangeParamsV2, mq *model.MetricQuery, table
" GROUP BY %s" +
" ORDER BY %s ts"
tagsWithoutLe := []string{}
for _, tag := range mq.GroupingTags {
if tag != "le" {
tagsWithoutLe = append(tagsWithoutLe, tag)
}
}
groupByWithoutLe := groupBy(tagsWithoutLe...)
groupTagsWithoutLe := groupSelect(tagsWithoutLe...)
groupBy := groupBy(mq.GroupingTags...)
groupTags := groupSelect(mq.GroupingTags...)
@ -210,6 +225,20 @@ func BuildMetricQuery(qp *model.QueryRangeParamsV2, mq *model.MetricQuery, table
op := fmt.Sprintf("quantile(%v)(value)", AggregateOperatorToPercentile[mq.AggregateOperator])
query := fmt.Sprintf(queryTmpl, groupTags, qp.Step, op, filterSubQuery, groupBy, groupTags)
return query, nil
case model.HIST_QUANTILE_50, model.HIST_QUANTILE_75, model.HIST_QUANTILE_90, model.HIST_QUANTILE_95, model.HIST_QUANTILE_99:
rateGroupBy := "fingerprint, " + groupBy
rateGroupTags := "fingerprint, " + groupTags
op := "max(value)"
subQuery := fmt.Sprintf(
queryTmpl, rateGroupTags, qp.Step, op, filterSubQuery, rateGroupBy, rateGroupTags,
) // labels will be same so any should be fine
query := `SELECT %s ts, runningDifference(value)/runningDifference(ts) as value FROM(%s) OFFSET 1`
query = fmt.Sprintf(query, groupTags, subQuery)
query = fmt.Sprintf(`SELECT %s ts, sum(value) as value FROM (%s) GROUP BY %s ORDER BY %s ts`, groupTags, query, groupBy, groupTags)
value := AggregateOperatorToPercentile[mq.AggregateOperator]
query = fmt.Sprintf(`SELECT %s ts, histogramQuantile(arrayMap(x -> toFloat64(x), groupArray(le)), groupArray(value), %.3f) as value FROM (%s) GROUP BY %s ORDER BY %s ts`, groupTagsWithoutLe, value, query, groupByWithoutLe, groupTagsWithoutLe)
return query, nil
case model.AVG, model.SUM, model.MIN, model.MAX:
op := fmt.Sprintf("%s(value)", AggregateOperatorToSQLFunc[mq.AggregateOperator])
query := fmt.Sprintf(queryTmpl, groupTags, qp.Step, op, filterSubQuery, groupBy, groupTags)
@ -311,6 +340,7 @@ func varToQuery(qp *model.QueryRangeParamsV2, tableName string) (map[string]stri
evalFuncs := GoValuateFuncs()
varToQuery := make(map[string]string)
for _, builderQuery := range qp.CompositeMetricQuery.BuilderQueries {
// err should be nil here since the expression is already validated
expression, _ := govaluate.NewEvaluableExpressionWithFunctions(builderQuery.Expression, evalFuncs)
// Use the parsed expression and build the query for each variable
@ -318,7 +348,11 @@ func varToQuery(qp *model.QueryRangeParamsV2, tableName string) (map[string]stri
var errs []error
for _, _var := range expression.Vars() {
if _, ok := varToQuery[_var]; !ok {
mq := qp.CompositeMetricQuery.BuilderQueries[_var]
mq, varExists := qp.CompositeMetricQuery.BuilderQueries[_var]
if !varExists {
errs = append(errs, fmt.Errorf("variable %s not found in builder queries", _var))
continue
}
query, err := BuildMetricQuery(qp, mq, tableName)
if err != nil {
errs = append(errs, err)
@ -340,10 +374,22 @@ func varToQuery(qp *model.QueryRangeParamsV2, tableName string) (map[string]stri
return varToQuery, nil
}
func unique(slice []string) []string {
keys := make(map[string]struct{})
list := []string{}
for _, entry := range slice {
if _, value := keys[entry]; !value {
keys[entry] = struct{}{}
list = append(list, entry)
}
}
return list
}
// expressionToQuery constructs the query for the expression
func expressionToQuery(qp *model.QueryRangeParamsV2, varToQuery map[string]string, expression *govaluate.EvaluableExpression) (string, error) {
var formulaQuery string
vars := expression.Vars()
vars := unique(expression.Vars())
for idx, var_ := range vars[1:] {
x, y := vars[idx], var_
if !reflect.DeepEqual(qp.CompositeMetricQuery.BuilderQueries[x].GroupingTags, qp.CompositeMetricQuery.BuilderQueries[y].GroupingTags) {
@ -360,21 +406,34 @@ func expressionToQuery(qp *model.QueryRangeParamsV2, varToQuery map[string]strin
}
modified = append(modified, token)
}
// err should be nil here since the expression is already validated
formula, _ := govaluate.NewEvaluableExpressionFromTokens(modified)
var formulaSubQuery string
var joinUsing string
var prevVar string
for idx, var_ := range vars {
query := varToQuery[var_]
groupTags := qp.CompositeMetricQuery.BuilderQueries[var_].GroupingTags
groupTags = append(groupTags, "ts")
joinUsing = strings.Join(groupTags, ",")
formulaSubQuery += fmt.Sprintf("(%s) as %s ", query, var_)
if idx < len(vars)-1 {
formulaSubQuery += "GLOBAL INNER JOIN"
} else if len(vars) > 1 {
formulaSubQuery += fmt.Sprintf("USING (%s)", joinUsing)
if joinUsing == "" {
for _, tag := range groupTags {
joinUsing += fmt.Sprintf("%s.%s as %s, ", var_, tag, tag)
}
joinUsing = strings.TrimSuffix(joinUsing, ", ")
}
formulaSubQuery += fmt.Sprintf("(%s) as %s ", query, var_)
if idx > 0 {
formulaSubQuery += " ON "
for _, tag := range groupTags {
formulaSubQuery += fmt.Sprintf("%s.%s = %s.%s AND ", prevVar, tag, var_, tag)
}
formulaSubQuery = strings.TrimSuffix(formulaSubQuery, " AND ")
}
if idx < len(vars)-1 {
formulaSubQuery += " GLOBAL INNER JOIN"
}
prevVar = var_
}
formulaQuery = fmt.Sprintf("SELECT %s, %s as value FROM ", joinUsing, formula.ExpressionString()) + formulaSubQuery
return formulaQuery, nil

View File

@ -1,6 +1,7 @@
package metrics
import (
"strings"
"testing"
. "github.com/smartystreets/goconvey/convey"
@ -15,19 +16,19 @@ func TestBuildQuery(t *testing.T) {
Step: 60,
CompositeMetricQuery: &model.CompositeMetricQuery{
BuilderQueries: map[string]*model.MetricQuery{
"a": {
QueryName: "a",
"A": {
QueryName: "A",
MetricName: "name",
AggregateOperator: model.RATE_MAX,
Expression: "a",
Expression: "A",
},
},
},
}
queries := PrepareBuilderMetricQueries(q, "table").Queries
So(len(queries), ShouldEqual, 1)
So(queries["a"], ShouldContainSubstring, "WHERE metric_name = 'name'")
So(queries["a"], ShouldContainSubstring, "runningDifference(value)/runningDifference(ts)")
So(queries["A"], ShouldContainSubstring, "WHERE metric_name = 'name'")
So(queries["A"], ShouldContainSubstring, "runningDifference(value)/runningDifference(ts)")
})
}
@ -39,15 +40,15 @@ func TestBuildQueryWithFilters(t *testing.T) {
Step: 60,
CompositeMetricQuery: &model.CompositeMetricQuery{
BuilderQueries: map[string]*model.MetricQuery{
"a": {
QueryName: "a",
"A": {
QueryName: "A",
MetricName: "name",
TagFilters: &model.FilterSet{Operator: "AND", Items: []model.FilterItem{
{Key: "a", Value: "b", Operator: "neq"},
{Key: "code", Value: "ERROR_*", Operator: "nmatch"},
}},
AggregateOperator: model.RATE_MAX,
Expression: "a",
Expression: "A",
},
},
},
@ -55,9 +56,9 @@ func TestBuildQueryWithFilters(t *testing.T) {
queries := PrepareBuilderMetricQueries(q, "table").Queries
So(len(queries), ShouldEqual, 1)
So(queries["a"], ShouldContainSubstring, "WHERE metric_name = 'name' AND JSONExtractString(labels, 'a') != 'b'")
So(queries["a"], ShouldContainSubstring, "runningDifference(value)/runningDifference(ts)")
So(queries["a"], ShouldContainSubstring, "not match(JSONExtractString(labels, 'code'), 'ERROR_*')")
So(queries["A"], ShouldContainSubstring, "WHERE metric_name = 'name' AND JSONExtractString(labels, 'a') != 'b'")
So(queries["A"], ShouldContainSubstring, "runningDifference(value)/runningDifference(ts)")
So(queries["A"], ShouldContainSubstring, "not match(JSONExtractString(labels, 'code'), 'ERROR_*')")
})
}
@ -69,28 +70,28 @@ func TestBuildQueryWithMultipleQueries(t *testing.T) {
Step: 60,
CompositeMetricQuery: &model.CompositeMetricQuery{
BuilderQueries: map[string]*model.MetricQuery{
"a": {
QueryName: "a",
"A": {
QueryName: "A",
MetricName: "name",
TagFilters: &model.FilterSet{Operator: "AND", Items: []model.FilterItem{
{Key: "in", Value: []interface{}{"a", "b", "c"}, Operator: "in"},
}},
AggregateOperator: model.RATE_AVG,
Expression: "a",
Expression: "A",
},
"b": {
QueryName: "b",
"B": {
QueryName: "B",
MetricName: "name2",
AggregateOperator: model.RATE_MAX,
Expression: "b",
Expression: "B",
},
},
},
}
queries := PrepareBuilderMetricQueries(q, "table").Queries
So(len(queries), ShouldEqual, 2)
So(queries["a"], ShouldContainSubstring, "WHERE metric_name = 'name' AND JSONExtractString(labels, 'in') IN ['a','b','c']")
So(queries["a"], ShouldContainSubstring, "runningDifference(value)/runningDifference(ts)")
So(queries["A"], ShouldContainSubstring, "WHERE metric_name = 'name' AND JSONExtractString(labels, 'in') IN ['a','b','c']")
So(queries["A"], ShouldContainSubstring, "runningDifference(value)/runningDifference(ts)")
})
}
@ -102,31 +103,136 @@ func TestBuildQueryWithMultipleQueriesAndFormula(t *testing.T) {
Step: 60,
CompositeMetricQuery: &model.CompositeMetricQuery{
BuilderQueries: map[string]*model.MetricQuery{
"a": {
QueryName: "a",
"A": {
QueryName: "A",
MetricName: "name",
TagFilters: &model.FilterSet{Operator: "AND", Items: []model.FilterItem{
{Key: "in", Value: []interface{}{"a", "b", "c"}, Operator: "in"},
}},
AggregateOperator: model.RATE_MAX,
Expression: "a",
Expression: "A",
},
"b": {
"B": {
MetricName: "name2",
AggregateOperator: model.RATE_AVG,
Expression: "b",
Expression: "B",
},
"c": {
QueryName: "c",
Expression: "a/b",
"C": {
QueryName: "C",
Expression: "A/B",
},
},
},
}
queries := PrepareBuilderMetricQueries(q, "table").Queries
So(len(queries), ShouldEqual, 3)
So(queries["c"], ShouldContainSubstring, "SELECT ts, a.value / b.value")
So(queries["c"], ShouldContainSubstring, "WHERE metric_name = 'name' AND JSONExtractString(labels, 'in') IN ['a','b','c']")
So(queries["c"], ShouldContainSubstring, "runningDifference(value)/runningDifference(ts)")
So(queries["C"], ShouldContainSubstring, "SELECT A.ts as ts, A.value / B.value")
So(queries["C"], ShouldContainSubstring, "WHERE metric_name = 'name' AND JSONExtractString(labels, 'in') IN ['a','b','c']")
So(queries["C"], ShouldContainSubstring, "runningDifference(value)/runningDifference(ts)")
})
}
func TestBuildQueryWithIncorrectQueryRef(t *testing.T) {
Convey("TestBuildQueryWithFilters", t, func() {
q := &model.QueryRangeParamsV2{
Start: 1650991982000,
End: 1651078382000,
Step: 60,
CompositeMetricQuery: &model.CompositeMetricQuery{
BuilderQueries: map[string]*model.MetricQuery{
"A": {
QueryName: "A",
MetricName: "name",
TagFilters: &model.FilterSet{Operator: "AND", Items: []model.FilterItem{
{Key: "in", Value: []interface{}{"a", "b", "c"}, Operator: "in"},
}},
AggregateOperator: model.RATE_MAX,
Expression: "A",
},
"C": {
QueryName: "C",
Expression: "D*2",
},
},
},
}
res := PrepareBuilderMetricQueries(q, "table")
So(res.Err, ShouldNotBeNil)
So(res.Err.Error(), ShouldContainSubstring, "variable D not found in builder queries")
})
}
func TestBuildQueryWithThreeOrMoreQueriesRefAndFormula(t *testing.T) {
Convey("TestBuildQueryWithFilters", t, func() {
q := &model.QueryRangeParamsV2{
Start: 1650991982000,
End: 1651078382000,
Step: 60,
CompositeMetricQuery: &model.CompositeMetricQuery{
BuilderQueries: map[string]*model.MetricQuery{
"A": {
QueryName: "A",
MetricName: "name",
TagFilters: &model.FilterSet{Operator: "AND", Items: []model.FilterItem{
{Key: "in", Value: []interface{}{"a", "b", "c"}, Operator: "in"},
}},
AggregateOperator: model.RATE_MAX,
Expression: "A",
Disabled: true,
},
"B": {
MetricName: "name2",
AggregateOperator: model.RATE_AVG,
Expression: "B",
Disabled: true,
},
"C": {
MetricName: "name3",
AggregateOperator: model.SUM_RATE,
Expression: "C",
Disabled: true,
},
"F1": {
QueryName: "F1",
Expression: "A/B",
},
"F2": {
QueryName: "F2",
Expression: "A/(B+C)",
},
"F3": {
QueryName: "F3",
Expression: "A*A",
},
"F4": {
QueryName: "F4",
Expression: "A*B*C",
},
"F5": {
QueryName: "F5",
Expression: "((A - B) / B) * 100",
},
},
},
}
res := PrepareBuilderMetricQueries(q, "table")
So(res.Err, ShouldBeNil)
queries := res.Queries
So(len(queries), ShouldEqual, 5)
So(queries["F1"], ShouldContainSubstring, "SELECT A.ts as ts, A.value / B.value")
So(strings.Count(queries["F1"], " ON "), ShouldEqual, 1)
So(queries["F2"], ShouldContainSubstring, "SELECT A.ts as ts, A.value / (B.value + C.value)")
So(strings.Count(queries["F2"], " ON "), ShouldEqual, 2)
// Working with same query multiple times should not join on itself
So(queries["F3"], ShouldNotContainSubstring, " ON ")
So(queries["F4"], ShouldContainSubstring, "SELECT A.ts as ts, A.value * B.value * C.value")
// Number of times JOIN ON appears is N-1 where N is number of unique queries
So(strings.Count(queries["F4"], " ON "), ShouldEqual, 2)
So(queries["F5"], ShouldContainSubstring, "SELECT A.ts as ts, ((A.value - B.value) / B.value) * 100")
So(strings.Count(queries["F5"], " ON "), ShouldEqual, 1)
})
}

View File

@ -244,25 +244,34 @@ func extractDashboardMetaData(path string, r *http.Request) (map[string]interfac
data := map[string]interface{}{}
if path == pathToExtractBodyFrom && (r.Method == "POST") {
bodyBytes, _ := ioutil.ReadAll(r.Body)
r.Body.Close() // must close
r.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))
json.Unmarshal(bodyBytes, &requestBody)
if r.Body != nil {
bodyBytes, err := ioutil.ReadAll(r.Body)
if err != nil {
return nil, false
}
r.Body.Close() // must close
r.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))
json.Unmarshal(bodyBytes, &requestBody)
} else {
return nil, false
}
} else {
return nil, false
}
compositeMetricQuery, compositeMetricQueryExists := requestBody["compositeMetricQuery"]
compositeMetricQueryMap := compositeMetricQuery.(map[string]interface{})
signozMetricFound := false
if compositeMetricQueryExists {
compositeMetricQueryMap := compositeMetricQuery.(map[string]interface{})
signozMetricFound = telemetry.GetInstance().CheckSigNozMetrics(compositeMetricQueryMap)
queryType, queryTypeExists := compositeMetricQueryMap["queryType"]
if queryTypeExists {
data["queryType"] = queryType
}
panelType, panelTypeExists := compositeMetricQueryMap["panelType"]
if panelTypeExists {

View File

@ -106,6 +106,11 @@ const (
RATE_AVG
RATE_MAX
RATE_MIN
HIST_QUANTILE_50
HIST_QUANTILE_75
HIST_QUANTILE_90
HIST_QUANTILE_95
HIST_QUANTILE_99
)
type DataSource int

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