mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-12 02:48:59 +08:00
commit
2f323056d0
4
.github/workflows/build.yaml
vendored
4
.github/workflows/build.yaml
vendored
@ -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: |
|
||||
|
3
Makefile
3
Makefile
@ -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/...
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
21
deploy/docker/clickhouse-setup/custom-function.xml
Normal file
21
deploy/docker/clickhouse-setup/custom-function.xml
Normal 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>
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
BIN
deploy/docker/clickhouse-setup/user_scripts/histogramQuantile
Executable file
BIN
deploy/docker/clickhouse-setup/user_scripts/histogramQuantile
Executable file
Binary file not shown.
237
deploy/docker/clickhouse-setup/user_scripts/histogramQuantile.go
Normal file
237
deploy/docker/clickhouse-setup/user_scripts/histogramQuantile.go
Normal 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))
|
||||
}
|
||||
}
|
@ -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)
|
||||
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
|
||||
|
@ -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",
|
||||
|
10
frontend/public/css/antd.dark.min.css
vendored
10
frontend/public/css/antd.dark.min.css
vendored
File diff suppressed because one or more lines are too long
10
frontend/public/css/antd.min.css
vendored
10
frontend/public/css/antd.min.css
vendored
File diff suppressed because one or more lines are too long
@ -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,22 +11,23 @@ import PrivateRoute from './Private';
|
||||
import routes from './routes';
|
||||
|
||||
function App(): JSX.Element {
|
||||
const themeConfig = useThemeConfig();
|
||||
|
||||
return (
|
||||
<ConfigProvider theme={themeConfig}>
|
||||
<Router history={history}>
|
||||
<PrivateRoute>
|
||||
<AppLayout>
|
||||
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
|
||||
<Switch>
|
||||
{routes.map(({ path, component, exact }) => {
|
||||
return (
|
||||
{routes.map(({ path, component, exact }) => (
|
||||
<Route
|
||||
key={`${path}`}
|
||||
exact={exact}
|
||||
path={path}
|
||||
component={component}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
))}
|
||||
|
||||
<Route path="*" component={NotFound} />
|
||||
</Switch>
|
||||
@ -32,6 +35,7 @@ function App(): JSX.Element {
|
||||
</AppLayout>
|
||||
</PrivateRoute>
|
||||
</Router>
|
||||
</ConfigProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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;
|
@ -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'}
|
||||
|
@ -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();
|
||||
},
|
||||
};
|
||||
|
@ -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
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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({
|
||||
|
@ -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%;
|
||||
`;
|
||||
|
@ -11,7 +11,7 @@ function CustomModal({
|
||||
return (
|
||||
<Modal
|
||||
title={title}
|
||||
visible={isModalVisible}
|
||||
open={isModalVisible}
|
||||
footer={footer}
|
||||
closable={closable}
|
||||
>
|
||||
|
@ -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',
|
||||
}
|
||||
|
@ -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';
|
||||
|
@ -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;
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
10
frontend/src/container/ConfigDropdown/Config/styles.ts
Normal file
10
frontend/src/container/ConfigDropdown/Config/styles.ts
Normal 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;
|
||||
}
|
||||
`;
|
@ -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(
|
||||
|
@ -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 => {
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
@ -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[]>(
|
||||
|
@ -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%;
|
||||
`;
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
3
frontend/src/container/GantChart/Trace/utils.ts
Normal file
3
frontend/src/container/GantChart/Trace/utils.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export const getIconStyles = (isDarkMode: boolean): Record<string, string> => ({
|
||||
color: isDarkMode ? 'white' : 'black',
|
||||
});
|
@ -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>
|
||||
|
@ -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 (
|
||||
<>
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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'],
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
`;
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -131,7 +131,7 @@ function ImportJSON({
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={isImportJSONModalVisible}
|
||||
open={isImportJSONModalVisible}
|
||||
centered
|
||||
maskClosable
|
||||
destroyOnClose
|
||||
|
@ -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} />
|
||||
))}
|
||||
|
@ -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;
|
||||
|
@ -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])) {
|
||||
|
@ -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}
|
||||
open={detailedLog !== null}
|
||||
style={{ overscrollBehavior: 'contain' }}
|
||||
destroyOnClose
|
||||
>
|
||||
{detailedLog && (
|
||||
<Tabs defaultActiveKey="1">
|
||||
<TabPane tab="Table" key="1">
|
||||
<TableView logData={detailedLog} />
|
||||
</TabPane>
|
||||
<TabPane tab="JSON" key="2">
|
||||
<JSONView logData={detailedLog} />
|
||||
</TabPane>
|
||||
<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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
);
|
||||
|
@ -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 => {
|
||||
|
@ -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} />)
|
||||
|
@ -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%',
|
||||
|
@ -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;
|
||||
|
@ -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 (
|
||||
|
@ -165,7 +165,7 @@ function VariablesSetting({
|
||||
<Modal
|
||||
title="Delete variable"
|
||||
centered
|
||||
visible={deleteVariableModal}
|
||||
open={deleteVariableModal}
|
||||
onOk={handleDeleteConfirm}
|
||||
onCancel={handleDeleteCancel}
|
||||
>
|
||||
|
@ -83,7 +83,7 @@ function ShareModal({
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={isJSONModalVisible}
|
||||
open={isJSONModalVisible}
|
||||
onCancel={(): void => {
|
||||
onToggleHandler();
|
||||
setIsViewJSON(false);
|
||||
|
@ -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[]>([]);
|
||||
|
@ -213,7 +213,7 @@ function NewWidget({
|
||||
onClickSaveHandler();
|
||||
}}
|
||||
centered
|
||||
visible={saveModal}
|
||||
open={saveModal}
|
||||
width={600}
|
||||
>
|
||||
{hasUnstagedChanges ? (
|
||||
|
@ -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)}
|
||||
>
|
||||
|
@ -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)}
|
||||
|
@ -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"
|
||||
<Form
|
||||
initialValues={{ name }}
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={onSubmit}
|
||||
autoComplete="off"
|
||||
>
|
||||
Change Org Name
|
||||
<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;
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
1
frontend/src/container/SideNav/config.ts
Normal file
1
frontend/src/container/SideNav/config.ts
Normal file
@ -0,0 +1 @@
|
||||
export const styles = { background: '#1f1f1f' };
|
@ -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
|
||||
|
@ -10,18 +10,19 @@ interface LogoProps {
|
||||
}
|
||||
|
||||
export const Sider = styled(SiderComponent)`
|
||||
z-index: 999;
|
||||
.ant-typography {
|
||||
color: white;
|
||||
}
|
||||
&&& {
|
||||
background: #1f1f1f;
|
||||
|
||||
.ant-layout-sider-trigger {
|
||||
background-color: #1f1f1f;
|
||||
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 {
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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,
|
||||
|
@ -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 (
|
||||
<>
|
||||
|
@ -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',
|
||||
|
@ -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),
|
||||
|
@ -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;
|
||||
|
@ -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"
|
||||
|
@ -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 (
|
||||
|
4
frontend/src/hooks/useDarkMode/constant.ts
Normal file
4
frontend/src/hooks/useDarkMode/constant.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export const THEME_MODE = {
|
||||
LIGHT: 'light',
|
||||
DARK: 'dark',
|
||||
};
|
74
frontend/src/hooks/useDarkMode/index.tsx
Normal file
74
frontend/src/hooks/useDarkMode/index.tsx
Normal 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;
|
@ -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;
|
@ -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>
|
||||
|
@ -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,6 +24,7 @@ const queryClient = new QueryClient({
|
||||
});
|
||||
|
||||
ReactDOM.render(
|
||||
<ThemeProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Provider store={store}>
|
||||
<React.StrictMode>
|
||||
@ -32,6 +34,7 @@ ReactDOM.render(
|
||||
{process.env.NODE_ENV === 'development' && (
|
||||
<ReactQueryDevtools initialIsOpen />
|
||||
)}
|
||||
</QueryClientProvider>,
|
||||
</QueryClientProvider>
|
||||
</ThemeProvider>,
|
||||
document.querySelector('#root'),
|
||||
);
|
||||
|
@ -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;
|
@ -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;
|
@ -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>
|
||||
> Size of circles is proportial to the number of requests served by
|
||||
each node
|
||||
</div>
|
||||
<div>> Click on node name to reposition the node</div>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export default SelectService;
|
@ -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,
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
export default withRouter(
|
||||
connect(mapStateToProps, {
|
||||
|
@ -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">
|
||||
|
@ -1,2 +1 @@
|
||||
export * from './sideBarCollapse';
|
||||
export * from './toggleDarkMode';
|
||||
|
@ -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',
|
||||
});
|
||||
};
|
||||
};
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -102,7 +102,7 @@ export interface SetLogsAggregateSeries {
|
||||
}
|
||||
export interface SetDetailedLogData {
|
||||
type: typeof SET_DETAILED_LOG_DATA;
|
||||
payload: ILog;
|
||||
payload: ILog | null;
|
||||
}
|
||||
|
||||
export interface ToggleLiveTail {
|
||||
|
@ -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 {
|
||||
|
@ -15,7 +15,6 @@ export interface User {
|
||||
}
|
||||
|
||||
export default interface AppReducer {
|
||||
isDarkMode: boolean;
|
||||
isLoggedIn: boolean;
|
||||
isSideBarCollapsed: boolean;
|
||||
currentVersion: string;
|
||||
|
7593
frontend/yarn.lock
7593
frontend/yarn.lock
File diff suppressed because it is too large
Load Diff
@ -26,6 +26,11 @@ var AggregateOperatorToPercentile = map[model.AggregateOperator]float64{
|
||||
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
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
@ -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)
|
||||
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 {
|
||||
|
@ -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
Loading…
x
Reference in New Issue
Block a user