mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2026-06-08 11:23:53 +03:00
Compare commits
1 Commits
v1.10.0-vi
...
test-templ
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e237e35c9c |
2
.github/workflows/main.yml
vendored
2
.github/workflows/main.yml
vendored
@@ -90,7 +90,7 @@ jobs:
|
||||
- name: Publish coverage
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
files: ./coverage.txt
|
||||
file: ./coverage.txt
|
||||
|
||||
integration-test:
|
||||
name: integration-test
|
||||
|
||||
10
Makefile
10
Makefile
@@ -513,19 +513,19 @@ check-all: fmt vet golangci-lint govulncheck
|
||||
clean-checkers: remove-golangci-lint remove-govulncheck
|
||||
|
||||
test:
|
||||
go test ./lib/... ./app/...
|
||||
DISABLE_FSYNC_FOR_TESTING=1 go test ./lib/... ./app/...
|
||||
|
||||
test-race:
|
||||
go test -race ./lib/... ./app/...
|
||||
DISABLE_FSYNC_FOR_TESTING=1 go test -race ./lib/... ./app/...
|
||||
|
||||
test-pure:
|
||||
CGO_ENABLED=0 go test ./lib/... ./app/...
|
||||
DISABLE_FSYNC_FOR_TESTING=1 CGO_ENABLED=0 go test ./lib/... ./app/...
|
||||
|
||||
test-full:
|
||||
go test -coverprofile=coverage.txt -covermode=atomic ./lib/... ./app/...
|
||||
DISABLE_FSYNC_FOR_TESTING=1 go test -coverprofile=coverage.txt -covermode=atomic ./lib/... ./app/...
|
||||
|
||||
test-full-386:
|
||||
GOARCH=386 go test -coverprofile=coverage.txt -covermode=atomic ./lib/... ./app/...
|
||||
DISABLE_FSYNC_FOR_TESTING=1 GOARCH=386 go test -coverprofile=coverage.txt -covermode=atomic ./lib/... ./app/...
|
||||
|
||||
integration-test: victoria-metrics vmagent vmalert vmauth
|
||||
go test ./apptest/... -skip="^TestCluster.*"
|
||||
|
||||
@@ -199,8 +199,8 @@ func (lmp *logMessageProcessor) AddRow(timestamp int64, fields, streamFields []l
|
||||
lmp.bytesIngestedTotal.Add(n)
|
||||
|
||||
if len(fields) > *MaxFieldsPerLine {
|
||||
line := logstorage.MarshalFieldsToJSON(nil, fields)
|
||||
logger.Warnf("dropping log line with %d fields; it exceeds -insert.maxFieldsPerLine=%d; %s", len(fields), *MaxFieldsPerLine, line)
|
||||
rf := logstorage.RowFormatter(fields)
|
||||
logger.Warnf("dropping log line with %d fields; it exceeds -insert.maxFieldsPerLine=%d; %s", len(fields), *MaxFieldsPerLine, rf)
|
||||
rowsDroppedTotalTooManyFields.Inc()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -8,10 +8,8 @@ import (
|
||||
|
||||
var (
|
||||
// MaxLineSizeBytes is the maximum length of a single line for /insert/* handlers
|
||||
MaxLineSizeBytes = flagutil.NewBytes("insert.maxLineSizeBytes", 256*1024, "The maximum size of a single line, which can be read by /insert/* handlers; "+
|
||||
"see https://docs.victoriametrics.com/victorialogs/faq/#what-length-a-log-record-is-expected-to-have")
|
||||
MaxLineSizeBytes = flagutil.NewBytes("insert.maxLineSizeBytes", 256*1024, "The maximum size of a single line, which can be read by /insert/* handlers")
|
||||
|
||||
// MaxFieldsPerLine is the maximum number of fields per line for /insert/* handlers
|
||||
MaxFieldsPerLine = flag.Int("insert.maxFieldsPerLine", 1000, "The maximum number of log fields per line, which can be read by /insert/* handlers; "+
|
||||
"see https://docs.victoriametrics.com/victorialogs/faq/#how-many-fields-a-single-log-entry-may-contain")
|
||||
MaxFieldsPerLine = flag.Int("insert.maxFieldsPerLine", 1000, "The maximum number of log fields per line, which can be read by /insert/* handlers")
|
||||
)
|
||||
|
||||
@@ -2,19 +2,20 @@ package insertutils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logstorage"
|
||||
)
|
||||
|
||||
// ExtractTimestampFromFields extracts timestamp in nanoseconds from the field with the name timeField at fields.
|
||||
// ExtractTimestampRFC3339NanoFromFields extracts RFC3339 timestamp in nanoseconds from the field with the name timeField at fields.
|
||||
//
|
||||
// The value for the timeField is set to empty string after returning from the function,
|
||||
// so it could be ignored during data ingestion.
|
||||
//
|
||||
// The current timestamp is returned if fields do not contain a field with timeField name or if the timeField value is empty.
|
||||
func ExtractTimestampFromFields(timeField string, fields []logstorage.Field) (int64, error) {
|
||||
func ExtractTimestampRFC3339NanoFromFields(timeField string, fields []logstorage.Field) (int64, error) {
|
||||
for i := range fields {
|
||||
f := &fields[i]
|
||||
if f.Name != timeField {
|
||||
@@ -47,24 +48,22 @@ func parseTimestamp(s string) (int64, error) {
|
||||
return nsecs, nil
|
||||
}
|
||||
|
||||
// ParseUnixTimestamp parses s as unix timestamp in seconds, milliseconds, microseconds or nanoseconds and returns the parsed timestamp in nanoseconds.
|
||||
// ParseUnixTimestamp parses s as unix timestamp in either seconds or milliseconds and returns the parsed timestamp in nanoseconds.
|
||||
func ParseUnixTimestamp(s string) (int64, error) {
|
||||
n, err := strconv.ParseInt(s, 10, 64)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("cannot parse unix timestamp from %q: %w", s, err)
|
||||
}
|
||||
if n < (1<<31) && n >= (-1<<31) {
|
||||
// The timestamp is in seconds.
|
||||
return n * 1e9, nil
|
||||
// The timestamp is in seconds. Convert it to milliseconds
|
||||
n *= 1e3
|
||||
}
|
||||
if n < 1e3*(1<<31) && n >= 1e3*(-1<<31) {
|
||||
// The timestamp is in milliseconds.
|
||||
return n * 1e6, nil
|
||||
if n > int64(math.MaxInt64)/1e6 {
|
||||
return 0, fmt.Errorf("too big timestamp in milliseconds: %d; mustn't exceed %d", n, int64(math.MaxInt64)/1e6)
|
||||
}
|
||||
if n < 1e6*(1<<31) && n >= 1e6*(-1<<31) {
|
||||
// The timestamp is in microseconds.
|
||||
return n * 1e3, nil
|
||||
if n < int64(math.MinInt64)/1e6 {
|
||||
return 0, fmt.Errorf("too small timestamp in milliseconds: %d; must be bigger than %d", n, int64(math.MinInt64)/1e6)
|
||||
}
|
||||
// The timestamp is in nanoseconds
|
||||
n *= 1e6
|
||||
return n, nil
|
||||
}
|
||||
|
||||
@@ -6,11 +6,11 @@ import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logstorage"
|
||||
)
|
||||
|
||||
func TestExtractTimestampFromFields_Success(t *testing.T) {
|
||||
func TestExtractTimestampRFC3339NanoFromFields_Success(t *testing.T) {
|
||||
f := func(timeField string, fields []logstorage.Field, nsecsExpected int64) {
|
||||
t.Helper()
|
||||
|
||||
nsecs, err := ExtractTimestampFromFields(timeField, fields)
|
||||
nsecs, err := ExtractTimestampRFC3339NanoFromFields(timeField, fields)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
@@ -51,18 +51,6 @@ func TestExtractTimestampFromFields_Success(t *testing.T) {
|
||||
{Name: "foo", Value: "bar"},
|
||||
}, 1718773640123456789)
|
||||
|
||||
// Unix timestamp in nanoseconds
|
||||
f("time", []logstorage.Field{
|
||||
{Name: "foo", Value: "bar"},
|
||||
{Name: "time", Value: "1718773640123456789"},
|
||||
}, 1718773640123456789)
|
||||
|
||||
// Unix timestamp in microseconds
|
||||
f("time", []logstorage.Field{
|
||||
{Name: "foo", Value: "bar"},
|
||||
{Name: "time", Value: "1718773640123456"},
|
||||
}, 1718773640123456000)
|
||||
|
||||
// Unix timestamp in milliseconds
|
||||
f("time", []logstorage.Field{
|
||||
{Name: "foo", Value: "bar"},
|
||||
@@ -76,14 +64,14 @@ func TestExtractTimestampFromFields_Success(t *testing.T) {
|
||||
}, 1718773640000000000)
|
||||
}
|
||||
|
||||
func TestExtractTimestampFromFields_Error(t *testing.T) {
|
||||
func TestExtractTimestampRFC3339NanoFromFields_Error(t *testing.T) {
|
||||
f := func(s string) {
|
||||
t.Helper()
|
||||
|
||||
fields := []logstorage.Field{
|
||||
{Name: "time", Value: s},
|
||||
}
|
||||
nsecs, err := ExtractTimestampFromFields("time", fields)
|
||||
nsecs, err := ExtractTimestampRFC3339NanoFromFields("time", fields)
|
||||
if err == nil {
|
||||
t.Fatalf("expecting non-nil error")
|
||||
}
|
||||
@@ -92,7 +80,6 @@ func TestExtractTimestampFromFields_Error(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// invalid time
|
||||
f("foobar")
|
||||
|
||||
// incomplete time
|
||||
|
||||
@@ -129,11 +129,6 @@ func handleJournald(r *http.Request, w http.ResponseWriter) {
|
||||
return
|
||||
}
|
||||
|
||||
// systemd starting release v258 will support compression, which starts working after negotiation: it expects supported compression
|
||||
// algorithms list in Accept-Encoding response header in a format "<algorithm_1>[:<priority_1>][;<algorithm_2>:<priority_2>]"
|
||||
// See https://github.com/systemd/systemd/pull/34822
|
||||
w.Header().Set("Accept-Encoding", "zstd")
|
||||
|
||||
// update requestJournaldDuration only for successfully parsed requests
|
||||
// There is no need in updating requestJournaldDuration for request errors,
|
||||
// since their timings are usually much smaller than the timing for successful request parsing.
|
||||
|
||||
@@ -51,13 +51,20 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
lmp := cp.NewLogMessageProcessor("jsonline")
|
||||
streamName := fmt.Sprintf("remoteAddr=%s, requestURI=%q", httpserver.GetQuotedRemoteAddr(r), r.RequestURI)
|
||||
processStreamInternal(streamName, reader, cp.TimeField, cp.MsgFields, lmp)
|
||||
err = processStreamInternal(streamName, reader, cp.TimeField, cp.MsgFields, lmp)
|
||||
lmp.MustClose()
|
||||
|
||||
requestDuration.UpdateDuration(startTime)
|
||||
if err != nil {
|
||||
logger.Errorf("jsonline: %s", err)
|
||||
} else {
|
||||
// update requestDuration only for successfully parsed requests.
|
||||
// There is no need in updating requestDuration for request errors,
|
||||
// since their timings are usually much smaller than the timing for successful request parsing.
|
||||
requestDuration.UpdateDuration(startTime)
|
||||
}
|
||||
}
|
||||
|
||||
func processStreamInternal(streamName string, r io.Reader, timeField string, msgFields []string, lmp insertutils.LogMessageProcessor) {
|
||||
func processStreamInternal(streamName string, r io.Reader, timeField string, msgFields []string, lmp insertutils.LogMessageProcessor) error {
|
||||
wcr := writeconcurrencylimiter.GetReader(r)
|
||||
defer writeconcurrencylimiter.PutReader(wcr)
|
||||
|
||||
@@ -69,10 +76,10 @@ func processStreamInternal(streamName string, r io.Reader, timeField string, msg
|
||||
wcr.DecConcurrency()
|
||||
if err != nil {
|
||||
errorsTotal.Inc()
|
||||
logger.Warnf("jsonline: cannot read line #%d in /jsonline request: %s", n, err)
|
||||
return fmt.Errorf("cannot read line #%d in /jsonline request: %s", n, err)
|
||||
}
|
||||
if !ok {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
n++
|
||||
}
|
||||
@@ -89,17 +96,16 @@ func readLine(lr *insertutils.LineReader, timeField string, msgFields []string,
|
||||
}
|
||||
|
||||
p := logstorage.GetJSONParser()
|
||||
defer logstorage.PutJSONParser(p)
|
||||
|
||||
if err := p.ParseLogMessage(line); err != nil {
|
||||
return true, fmt.Errorf("cannot parse json-encoded line: %w; line contents: %q", err, line)
|
||||
return false, fmt.Errorf("cannot parse json-encoded log entry: %w", err)
|
||||
}
|
||||
ts, err := insertutils.ExtractTimestampFromFields(timeField, p.Fields)
|
||||
ts, err := insertutils.ExtractTimestampRFC3339NanoFromFields(timeField, p.Fields)
|
||||
if err != nil {
|
||||
return true, fmt.Errorf("cannot get timestamp from json-encoded line: %w; line contents: %q", err, line)
|
||||
return false, fmt.Errorf("cannot get timestamp: %w", err)
|
||||
}
|
||||
logstorage.RenameField(p.Fields, msgFields, "_msg")
|
||||
lmp.AddRow(ts, p.Fields, nil)
|
||||
logstorage.PutJSONParser(p)
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
@@ -7,14 +7,16 @@ import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vlinsert/insertutils"
|
||||
)
|
||||
|
||||
func TestProcessStreamInternal(t *testing.T) {
|
||||
func TestProcessStreamInternal_Success(t *testing.T) {
|
||||
f := func(data, timeField, msgField string, timestampsExpected []int64, resultExpected string) {
|
||||
t.Helper()
|
||||
|
||||
msgFields := []string{msgField}
|
||||
tlp := &insertutils.TestLogMessageProcessor{}
|
||||
r := bytes.NewBufferString(data)
|
||||
processStreamInternal("test", r, timeField, msgFields, tlp)
|
||||
if err := processStreamInternal("test", r, timeField, msgFields, tlp); err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
|
||||
if err := tlp.Verify(timestampsExpected, resultExpected); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -43,37 +45,22 @@ func TestProcessStreamInternal(t *testing.T) {
|
||||
resultExpected = `{"log.offset":"71770","log.file.path":"/var/log/auth.log","message":"foobar"}
|
||||
{"message":"baz"}`
|
||||
f(data, timeField, msgField, timestampsExpected, resultExpected)
|
||||
}
|
||||
|
||||
func TestProcessStreamInternal_Failure(t *testing.T) {
|
||||
f := func(data string) {
|
||||
t.Helper()
|
||||
|
||||
tlp := &insertutils.TestLogMessageProcessor{}
|
||||
r := bytes.NewBufferString(data)
|
||||
if err := processStreamInternal("test", r, "time", nil, tlp); err == nil {
|
||||
t.Fatalf("expecting non-nil error")
|
||||
}
|
||||
}
|
||||
|
||||
// invalid json
|
||||
data = "foobar"
|
||||
timeField = "@timestamp"
|
||||
msgField = "aaa"
|
||||
timestampsExpected = nil
|
||||
resultExpected = ``
|
||||
f(data, timeField, msgField, timestampsExpected, resultExpected)
|
||||
f("foobar")
|
||||
|
||||
// invalid timestamp field
|
||||
data = `{"time":"foobar"}`
|
||||
timeField = "time"
|
||||
msgField = "abc"
|
||||
timestampsExpected = nil
|
||||
resultExpected = ``
|
||||
f(data, timeField, msgField, timestampsExpected, resultExpected)
|
||||
|
||||
// invalid lines among valid lines
|
||||
data = `
|
||||
dsfodmasd
|
||||
|
||||
{"time":"2023-06-06T04:48:11.735Z","log":{"offset":71770,"file":{"path":"/var/log/auth.log"}},"message":"foobar"}
|
||||
invalid line
|
||||
{"time":"2023-06-06T04:48:12.735+01:00","message":"baz"}
|
||||
asbsdf
|
||||
|
||||
`
|
||||
timeField = "time"
|
||||
msgField = "message"
|
||||
timestampsExpected = []int64{1686026891735000000, 1686023292735000000}
|
||||
resultExpected = `{"log.offset":"71770","log.file.path":"/var/log/auth.log","_msg":"foobar"}
|
||||
{"_msg":"baz"}`
|
||||
f(data, timeField, msgField, timestampsExpected, resultExpected)
|
||||
f(`{"time":"foobar"}`)
|
||||
}
|
||||
|
||||
@@ -126,18 +126,6 @@ func pushFieldsFromScopeLogs(sc *pb.ScopeLogs, commonFields []logstorage.Field,
|
||||
Value: attr.Value.FormatString(),
|
||||
})
|
||||
}
|
||||
if len(lr.TraceID) > 0 {
|
||||
fields = append(fields, logstorage.Field{
|
||||
Name: "trace_id",
|
||||
Value: lr.TraceID,
|
||||
})
|
||||
}
|
||||
if len(lr.SpanID) > 0 {
|
||||
fields = append(fields, logstorage.Field{
|
||||
Name: "span_id",
|
||||
Value: lr.SpanID,
|
||||
})
|
||||
}
|
||||
fields = append(fields, logstorage.Field{
|
||||
Name: "severity",
|
||||
Value: lr.FormatSeverity(),
|
||||
|
||||
@@ -560,7 +560,7 @@ func processLine(line []byte, currentYear int, timezone *time.Location, useLocal
|
||||
if useLocalTimestamp {
|
||||
ts = time.Now().UnixNano()
|
||||
} else {
|
||||
nsecs, err := insertutils.ExtractTimestampFromFields("timestamp", p.Fields)
|
||||
nsecs, err := insertutils.ExtractTimestampRFC3339NanoFromFields("timestamp", p.Fields)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot get timestamp from syslog line %q: %w", line, err)
|
||||
}
|
||||
|
||||
@@ -270,7 +270,7 @@ func printCommandsHelp(w io.Writer) {
|
||||
\h - show this help
|
||||
\s - singleline json output mode
|
||||
\m - multiline json output mode
|
||||
\c - compact output mode
|
||||
\c - compact output
|
||||
\logfmt - logfmt output mode
|
||||
\wrap_long_lines - toggles wrapping long lines
|
||||
\tail <query> - live tail <query> results
|
||||
|
||||
@@ -688,13 +688,13 @@ func ProcessStatsQueryRangeRequest(ctx context.Context, w http.ResponseWriter, r
|
||||
m := make(map[string]*statsSeries)
|
||||
var mLock sync.Mutex
|
||||
|
||||
timestamp := q.GetTimestamp()
|
||||
writeBlock := func(_ uint, timestamps []int64, columns []logstorage.BlockColumn) {
|
||||
clonedColumnNames := make([]string, len(columns))
|
||||
for i, c := range columns {
|
||||
clonedColumnNames[i] = strings.Clone(c.Name)
|
||||
}
|
||||
for i := range timestamps {
|
||||
timestamp := q.GetTimestamp()
|
||||
labels := make([]logstorage.Field, 0, len(byFields))
|
||||
for j, c := range columns {
|
||||
if c.Name == "_time" {
|
||||
|
||||
@@ -28,7 +28,7 @@ func TestParseExtraFilters_Success(t *testing.T) {
|
||||
// LogsQL filter
|
||||
f(`foobar`, `foobar`)
|
||||
f(`foo:bar`, `foo:bar`)
|
||||
f(`foo:(bar or baz) error _time:5m {"foo"=bar,baz="z"}`, `{foo="bar",baz="z"} (foo:bar or foo:baz) error _time:5m`)
|
||||
f(`foo:(bar or baz) error _time:5m {"foo"=bar,baz="z"}`, `(foo:bar or foo:baz) error _time:5m {foo="bar",baz="z"}`)
|
||||
}
|
||||
|
||||
func TestParseExtraFilters_Failure(t *testing.T) {
|
||||
@@ -77,7 +77,7 @@ func TestParseExtraStreamFilters_Success(t *testing.T) {
|
||||
// LogsQL filter
|
||||
f(`foobar`, `foobar`)
|
||||
f(`foo:bar`, `foo:bar`)
|
||||
f(`foo:(bar or baz) error _time:5m {"foo"=bar,baz="z"}`, `{foo="bar",baz="z"} (foo:bar or foo:baz) error _time:5m`)
|
||||
f(`foo:(bar or baz) error _time:5m {"foo"=bar,baz="z"}`, `(foo:bar or foo:baz) error _time:5m {foo="bar",baz="z"}`)
|
||||
}
|
||||
|
||||
func TestParseExtraStreamFilters_Failure(t *testing.T) {
|
||||
|
||||
12
app/vlselect/vmui/asset-manifest.json
Normal file
12
app/vlselect/vmui/asset-manifest.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"files": {
|
||||
"main.css": "./static/css/main.4aacd559.css",
|
||||
"main.js": "./static/js/main.5ce54a05.js",
|
||||
"static/js/685.f772060c.chunk.js": "./static/js/685.f772060c.chunk.js",
|
||||
"index.html": "./index.html"
|
||||
},
|
||||
"entrypoints": [
|
||||
"static/css/main.4aacd559.css",
|
||||
"static/js/main.5ce54a05.js"
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
.uplot,.uplot *,.uplot *:before,.uplot *:after{box-sizing:border-box}.uplot{font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";line-height:1.5;width:min-content}.u-title{text-align:center;font-size:18px;font-weight:700}.u-wrap{position:relative;-webkit-user-select:none;user-select:none}.u-over,.u-under{position:absolute}.u-under{overflow:hidden}.uplot canvas{display:block;position:relative;width:100%;height:100%}.u-axis{position:absolute}.u-legend{font-size:14px;margin:auto;text-align:center}.u-inline{display:block}.u-inline *{display:inline-block}.u-inline tr{margin-right:16px}.u-legend th{font-weight:600}.u-legend th>*{vertical-align:middle;display:inline-block}.u-legend .u-marker{width:1em;height:1em;margin-right:4px;background-clip:padding-box!important}.u-inline.u-live th:after{content:":";vertical-align:middle}.u-inline:not(.u-live) .u-value{display:none}.u-series>*{padding:4px}.u-series th{cursor:pointer}.u-legend .u-off>*{opacity:.3}.u-select{background:#00000012;position:absolute;pointer-events:none}.u-cursor-x,.u-cursor-y{position:absolute;left:0;top:0;pointer-events:none;will-change:transform}.u-hz .u-cursor-x,.u-vt .u-cursor-y{height:100%;border-right:1px dashed #607D8B}.u-hz .u-cursor-y,.u-vt .u-cursor-x{width:100%;border-bottom:1px dashed #607D8B}.u-cursor-pt{position:absolute;top:0;left:0;border-radius:50%;border:0 solid;pointer-events:none;will-change:transform;background-clip:padding-box!important}.u-axis.u-off,.u-select.u-off,.u-cursor-x.u-off,.u-cursor-y.u-off,.u-cursor-pt.u-off{display:none}
|
||||
File diff suppressed because one or more lines are too long
@@ -1,57 +1 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<link rel="icon" href="./favicon.svg" />
|
||||
<link rel="apple-touch-icon" href="./favicon.svg" />
|
||||
<link rel="mask-icon" href="./favicon.svg" color="#000000">
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=5"/>
|
||||
<meta name="theme-color" content="#000000"/>
|
||||
<meta name="description" content="Explore your log data with VictoriaLogs UI"/>
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="./manifest.json"/>
|
||||
<!--
|
||||
Notice the use of in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>UI for VictoriaLogs</title>
|
||||
|
||||
<meta name="twitter:card" content="summary">
|
||||
<meta name="twitter:title" content="UI for VictoriaLogs">
|
||||
<meta name="twitter:site" content="@https://victoriametrics.com/products/victorialogs/">
|
||||
<meta name="twitter:description" content="Explore your log data with VictoriaLogs UI">
|
||||
<meta name="twitter:image" content="./preview.jpg">
|
||||
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:title" content="UI for VictoriaLogs">
|
||||
<meta property="og:url" content="https://victoriametrics.com/products/victorialogs/">
|
||||
<meta property="og:description" content="Explore your log data with VictoriaLogs UI">
|
||||
<script type="module" crossorigin src="./assets/index-DokhsDQR.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="./assets/vendor-DojlIpLz.js">
|
||||
<link rel="stylesheet" crossorigin href="./assets/vendor-D1GxaB_c.css">
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-Do1w9kcl.css">
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="./favicon.svg"/><link rel="apple-touch-icon" href="./favicon.svg"/><link rel="mask-icon" href="./favicon.svg" color="#000000"><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=5"/><meta name="theme-color" content="#000000"/><meta name="description" content="Explore your log data with VictoriaLogs UI"/><link rel="manifest" href="./manifest.json"/><title>UI for VictoriaLogs</title><meta name="twitter:card" content="summary"><meta name="twitter:title" content="UI for VictoriaLogs"><meta name="twitter:site" content="@https://victoriametrics.com/products/victorialogs/"><meta name="twitter:description" content="Explore your log data with VictoriaLogs UI"><meta name="twitter:image" content="./preview.jpg"><meta property="og:type" content="website"><meta property="og:title" content="UI for VictoriaLogs"><meta property="og:url" content="https://victoriametrics.com/products/victorialogs/"><meta property="og:description" content="Explore your log data with VictoriaLogs UI"><script defer="defer" src="./static/js/main.5ce54a05.js"></script><link href="./static/css/main.4aacd559.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
||||
1
app/vlselect/vmui/static/css/main.4aacd559.css
Normal file
1
app/vlselect/vmui/static/css/main.4aacd559.css
Normal file
File diff suppressed because one or more lines are too long
1
app/vlselect/vmui/static/js/685.f772060c.chunk.js
Normal file
1
app/vlselect/vmui/static/js/685.f772060c.chunk.js
Normal file
File diff suppressed because one or more lines are too long
2
app/vlselect/vmui/static/js/main.5ce54a05.js
Normal file
2
app/vlselect/vmui/static/js/main.5ce54a05.js
Normal file
File diff suppressed because one or more lines are too long
@@ -70,7 +70,7 @@ func (am *AlertManager) Send(ctx context.Context, alerts []Alert, headers map[st
|
||||
|
||||
func (am *AlertManager) send(ctx context.Context, alerts []Alert, headers map[string]string) error {
|
||||
b := &bytes.Buffer{}
|
||||
alertsToSend := make([]Alert, 0, len(alerts))
|
||||
alertsToSend := alerts[:0]
|
||||
lblss := make([][]prompbmarshal.Label, 0, len(alerts))
|
||||
for _, a := range alerts {
|
||||
lbls := a.applyRelabelingIfNeeded(am.relabelConfigs)
|
||||
|
||||
@@ -36,7 +36,7 @@ var (
|
||||
|
||||
maxQueueSize = flag.Int("remoteWrite.maxQueueSize", defaultMaxQueueSize, "Defines the max number of pending datapoints to remote write endpoint")
|
||||
maxBatchSize = flag.Int("remoteWrite.maxBatchSize", defaultMaxBatchSize, "Defines max number of timeseries to be flushed at once")
|
||||
concurrency = flag.Int("remoteWrite.concurrency", defaultConcurrency, "Defines number of writers for concurrent writing into remote write endpoint. Default value depends on the number of available CPU cores.")
|
||||
concurrency = flag.Int("remoteWrite.concurrency", defaultConcurrency, "Defines number of writers for concurrent writing into remote write endpoint")
|
||||
flushInterval = flag.Duration("remoteWrite.flushInterval", defaultFlushInterval, "Defines interval of flushes to remote write endpoint")
|
||||
|
||||
tlsInsecureSkipVerify = flag.Bool("remoteWrite.tlsInsecureSkipVerify", false, "Whether to skip tls verification when connecting to -remoteWrite.url")
|
||||
|
||||
@@ -31,11 +31,7 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
httpListenAddrs = flagutil.NewArrayString("httpListenAddr", "TCP address to listen for incoming http requests. "+
|
||||
"By default, serves internal API and proxy requests. "+
|
||||
" See also -tls, -httpListenAddr.useProxyProtocol and -httpInternalListenAddr.")
|
||||
httpInternalListenAddr = flagutil.NewArrayString("httpInternalListenAddr", "TCP address to listen for incoming internal API http requests. Such as /health, /-/reload, /debug/pprof, etc. "+
|
||||
"If flag is set, vmauth no longer serves internal API at -httpListenAddr.")
|
||||
httpListenAddrs = flagutil.NewArrayString("httpListenAddr", "TCP address to listen for incoming http requests. See also -tls and -httpListenAddr.useProxyProtocol")
|
||||
useProxyProtocol = flagutil.NewArrayBool("httpListenAddr.useProxyProtocol", "Whether to use proxy protocol for connections accepted at the corresponding -httpListenAddr . "+
|
||||
"See https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt . "+
|
||||
"With enabled proxy protocol http server cannot serve regular /metrics endpoint. Use -pushmetrics.url for metrics pushing")
|
||||
@@ -95,21 +91,7 @@ func main() {
|
||||
logger.Infof("starting vmauth at %q...", listenAddrs)
|
||||
startTime := time.Now()
|
||||
initAuthConfig()
|
||||
disableInternalRoutes := len(*httpInternalListenAddr) > 0
|
||||
rh := requestHandlerWithInternalRoutes
|
||||
if disableInternalRoutes {
|
||||
rh = requestHandler
|
||||
}
|
||||
|
||||
serveOpts := httpserver.ServeOptions{
|
||||
UseProxyProtocol: useProxyProtocol,
|
||||
DisableBuiltinRoutes: disableInternalRoutes,
|
||||
}
|
||||
go httpserver.ServeWithOpts(listenAddrs, rh, serveOpts)
|
||||
|
||||
if len(*httpInternalListenAddr) > 0 {
|
||||
go httpserver.Serve(*httpInternalListenAddr, nil, internalRequestHandler)
|
||||
}
|
||||
go httpserver.Serve(listenAddrs, useProxyProtocol, requestHandler)
|
||||
logger.Infof("started vmauth in %.3f seconds", time.Since(startTime).Seconds())
|
||||
|
||||
pushmetrics.Init()
|
||||
@@ -127,7 +109,7 @@ func main() {
|
||||
logger.Infof("successfully stopped vmauth in %.3f seconds", time.Since(startTime).Seconds())
|
||||
}
|
||||
|
||||
func internalRequestHandler(w http.ResponseWriter, r *http.Request) bool {
|
||||
func requestHandler(w http.ResponseWriter, r *http.Request) bool {
|
||||
switch r.URL.Path {
|
||||
case "/-/reload":
|
||||
if !httpserver.CheckAuthFlag(w, r, reloadAuthKey) {
|
||||
@@ -138,17 +120,6 @@ func internalRequestHandler(w http.ResponseWriter, r *http.Request) bool {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func requestHandlerWithInternalRoutes(w http.ResponseWriter, r *http.Request) bool {
|
||||
if internalRequestHandler(w, r) {
|
||||
return true
|
||||
}
|
||||
return requestHandler(w, r)
|
||||
}
|
||||
|
||||
func requestHandler(w http.ResponseWriter, r *http.Request) bool {
|
||||
|
||||
ats := getAuthTokensFromRequest(r)
|
||||
if len(ats) == 0 {
|
||||
@@ -251,7 +222,8 @@ func processRequest(w http.ResponseWriter, r *http.Request, ui *UserInfo) {
|
||||
isDefault = true
|
||||
}
|
||||
|
||||
rtb := newReadTrackingBody(r.Body, maxRequestBodySizeToRetry.IntN())
|
||||
rtb := getReadTrackingBody(r.Body, maxRequestBodySizeToRetry.IntN())
|
||||
defer putReadTrackingBody(rtb)
|
||||
r.Body = rtb
|
||||
|
||||
maxAttempts := up.getBackendsCount()
|
||||
@@ -587,11 +559,22 @@ type readTrackingBody struct {
|
||||
bufComplete bool
|
||||
}
|
||||
|
||||
func newReadTrackingBody(r io.ReadCloser, maxBodySize int) *readTrackingBody {
|
||||
// do not use sync.Pool there
|
||||
// since http.RoundTrip may still use request body after return
|
||||
// See this issue for details https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8051
|
||||
rtb := &readTrackingBody{}
|
||||
func (rtb *readTrackingBody) reset() {
|
||||
rtb.maxBodySize = 0
|
||||
rtb.r = nil
|
||||
rtb.buf = rtb.buf[:0]
|
||||
rtb.readBuf = nil
|
||||
rtb.cannotRetry = false
|
||||
rtb.bufComplete = false
|
||||
}
|
||||
|
||||
func getReadTrackingBody(r io.ReadCloser, maxBodySize int) *readTrackingBody {
|
||||
v := readTrackingBodyPool.Get()
|
||||
if v == nil {
|
||||
v = &readTrackingBody{}
|
||||
}
|
||||
rtb := v.(*readTrackingBody)
|
||||
|
||||
if maxBodySize < 0 {
|
||||
maxBodySize = 0
|
||||
}
|
||||
@@ -614,6 +597,13 @@ func (r *zeroReader) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func putReadTrackingBody(rtb *readTrackingBody) {
|
||||
rtb.reset()
|
||||
readTrackingBodyPool.Put(rtb)
|
||||
}
|
||||
|
||||
var readTrackingBodyPool sync.Pool
|
||||
|
||||
// Read implements io.Reader interface.
|
||||
func (rtb *readTrackingBody) Read(p []byte) (int, error) {
|
||||
if len(rtb.readBuf) > 0 {
|
||||
|
||||
@@ -52,7 +52,7 @@ func TestRequestHandler(t *testing.T) {
|
||||
r.Header.Set("Pass-Header", "abc")
|
||||
|
||||
w := &fakeResponseWriter{}
|
||||
if !requestHandlerWithInternalRoutes(w, r) {
|
||||
if !requestHandler(w, r) {
|
||||
t.Fatalf("unexpected false is returned from requestHandler")
|
||||
}
|
||||
|
||||
@@ -195,7 +195,7 @@ unauthorized_user:
|
||||
}
|
||||
responseExpected = `
|
||||
statusCode=401
|
||||
Expected to receive non-empty authKey when -reloadAuthKey is set`
|
||||
The provided authKey doesn't match -reloadAuthKey`
|
||||
f(cfgStr, requestURL, backendHandler, responseExpected)
|
||||
if err := reloadAuthKey.Set(origAuthKey); err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
@@ -545,7 +545,8 @@ func TestReadTrackingBody_RetrySuccess(t *testing.T) {
|
||||
f := func(s string, maxBodySize int) {
|
||||
t.Helper()
|
||||
|
||||
rtb := newReadTrackingBody(io.NopCloser(bytes.NewBufferString(s)), maxBodySize)
|
||||
rtb := getReadTrackingBody(io.NopCloser(bytes.NewBufferString(s)), maxBodySize)
|
||||
defer putReadTrackingBody(rtb)
|
||||
|
||||
if !rtb.canRetry() {
|
||||
t.Fatalf("canRetry() must return true before reading anything")
|
||||
@@ -580,7 +581,8 @@ func TestReadTrackingBody_RetrySuccessPartialRead(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
// Check the case with partial read
|
||||
rtb := newReadTrackingBody(io.NopCloser(bytes.NewBufferString(s)), maxBodySize)
|
||||
rtb := getReadTrackingBody(io.NopCloser(bytes.NewBufferString(s)), maxBodySize)
|
||||
defer putReadTrackingBody(rtb)
|
||||
|
||||
for i := 0; i < len(s); i++ {
|
||||
buf := make([]byte, i)
|
||||
@@ -629,7 +631,8 @@ func TestReadTrackingBody_RetryFailureTooBigBody(t *testing.T) {
|
||||
f := func(s string, maxBodySize int) {
|
||||
t.Helper()
|
||||
|
||||
rtb := newReadTrackingBody(io.NopCloser(bytes.NewBufferString(s)), maxBodySize)
|
||||
rtb := getReadTrackingBody(io.NopCloser(bytes.NewBufferString(s)), maxBodySize)
|
||||
defer putReadTrackingBody(rtb)
|
||||
|
||||
if !rtb.canRetry() {
|
||||
t.Fatalf("canRetry() must return true before reading anything")
|
||||
@@ -678,7 +681,8 @@ func TestReadTrackingBody_RetryFailureZeroOrNegativeMaxBodySize(t *testing.T) {
|
||||
f := func(s string, maxBodySize int) {
|
||||
t.Helper()
|
||||
|
||||
rtb := newReadTrackingBody(io.NopCloser(bytes.NewBufferString(s)), maxBodySize)
|
||||
rtb := getReadTrackingBody(io.NopCloser(bytes.NewBufferString(s)), maxBodySize)
|
||||
defer putReadTrackingBody(rtb)
|
||||
|
||||
if !rtb.canRetry() {
|
||||
t.Fatalf("canRetry() must return true before reading anything")
|
||||
|
||||
@@ -596,8 +596,7 @@ var (
|
||||
&cli.Int64Flag{
|
||||
Name: vmRateLimit,
|
||||
Usage: "Optional data transfer rate limit in bytes per second.\n" +
|
||||
"By default, the rate limit is disabled. It can be useful for limiting load on source or destination databases. \n" +
|
||||
"Rate limit is applied per worker, see `--vm-concurrency`.",
|
||||
"By default, the rate limit is disabled. It can be useful for limiting load on source or destination databases.",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: vmInterCluster,
|
||||
|
||||
@@ -29,13 +29,13 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
deleteAuthKey = flagutil.NewPassword("deleteAuthKey", "authKey for metrics' deletion via /api/v1/admin/tsdb/delete_series and /tags/delSeries. It could be passed via authKey query arg. It overrides -httpAuth.*")
|
||||
deleteAuthKey = flagutil.NewPassword("deleteAuthKey", "authKey for metrics' deletion via /api/v1/admin/tsdb/delete_series and /tags/delSeries. It overrides -httpAuth.*")
|
||||
maxConcurrentRequests = flag.Int("search.maxConcurrentRequests", getDefaultMaxConcurrentRequests(), "The maximum number of concurrent search requests. "+
|
||||
"It shouldn't be high, since a single request can saturate all the CPU cores, while many concurrently executed requests may require high amounts of memory. "+
|
||||
"See also -search.maxQueueDuration and -search.maxMemoryPerQuery")
|
||||
maxQueueDuration = flag.Duration("search.maxQueueDuration", 10*time.Second, "The maximum time the request waits for execution when -search.maxConcurrentRequests "+
|
||||
"limit is reached; see also -search.maxQueryDuration")
|
||||
resetCacheAuthKey = flagutil.NewPassword("search.resetCacheAuthKey", "Optional authKey for resetting rollup cache via /internal/resetRollupResultCache call. It could be passed via authKey query arg. It overrides -httpAuth.*")
|
||||
resetCacheAuthKey = flagutil.NewPassword("search.resetCacheAuthKey", "Optional authKey for resetting rollup cache via /internal/resetRollupResultCache call. It overrides -httpAuth.*")
|
||||
logSlowQueryDuration = flag.Duration("search.logSlowQueryDuration", 5*time.Second, "Log queries with execution time exceeding this value. Zero disables slow query logging. "+
|
||||
"See also -search.logQueryMemoryUsage")
|
||||
vmalertProxyURL = flag.String("vmalert.proxyURL", "", "Optional URL for proxying requests to vmalert. For example, if -vmalert.proxyURL=http://vmalert:8880 , then alerting API requests such as /api/v1/rules from Grafana will be proxied to http://vmalert:8880/api/v1/rules")
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"sort"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/VictoriaMetrics/metrics"
|
||||
@@ -1001,7 +1002,9 @@ func ExportBlocks(qt *querytracer.Tracer, sq *storage.SearchQuery, deadline sear
|
||||
|
||||
sr := getStorageSearch()
|
||||
defer putStorageSearch(sr)
|
||||
startTime := time.Now()
|
||||
sr.Init(qt, vmstorage.Storage, tfss, tr, sq.MaxMetrics, deadline.Deadline())
|
||||
indexSearchDuration.UpdateDuration(startTime)
|
||||
|
||||
// Start workers that call f in parallel on available CPU cores.
|
||||
workCh := make(chan *exportWork, gomaxprocs*8)
|
||||
@@ -1139,7 +1142,9 @@ func ProcessSearchQuery(qt *querytracer.Tracer, sq *storage.SearchQuery, deadlin
|
||||
defer vmstorage.WG.Done()
|
||||
|
||||
sr := getStorageSearch()
|
||||
startTime := time.Now()
|
||||
maxSeriesCount := sr.Init(qt, vmstorage.Storage, tfss, tr, sq.MaxMetrics, deadline.Deadline())
|
||||
indexSearchDuration.UpdateDuration(startTime)
|
||||
type blockRefs struct {
|
||||
brs []blockRef
|
||||
}
|
||||
@@ -1291,6 +1296,8 @@ func ProcessSearchQuery(qt *querytracer.Tracer, sq *storage.SearchQuery, deadlin
|
||||
return &rss, nil
|
||||
}
|
||||
|
||||
var indexSearchDuration = metrics.NewHistogram(`vm_index_search_duration_seconds`)
|
||||
|
||||
type blockRef struct {
|
||||
partRef storage.PartRef
|
||||
addr tmpBlockAddr
|
||||
|
||||
@@ -29,7 +29,6 @@ import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httputils"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/memory"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/netutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/querytracer"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
|
||||
)
|
||||
@@ -39,7 +38,7 @@ var (
|
||||
"It can be overridden on per-query basis via latency_offset arg. "+
|
||||
"Too small value can result in incomplete last points for query results")
|
||||
maxQueryLen = flagutil.NewBytes("search.maxQueryLen", 16*1024, "The maximum search query length in bytes")
|
||||
maxLookback = flag.Duration("search.maxLookback", 0, "Synonym to -query.lookback-delta from Prometheus. "+
|
||||
maxLookback = flag.Duration("search.maxLookback", 0, "Synonym to -search.lookback-delta from Prometheus. "+
|
||||
"The value is dynamically detected from interval between time series datapoints if not set. It can be overridden on per-query basis via max_lookback arg. "+
|
||||
"See also '-search.maxStalenessInterval' flag, which has the same meaning due to historical reasons")
|
||||
maxStalenessInterval = flag.Duration("search.maxStalenessInterval", 0, "The maximum interval for staleness calculations. "+
|
||||
@@ -143,13 +142,10 @@ func FederateHandler(startTime time.Time, w http.ResponseWriter, r *http.Request
|
||||
WriteFederate(bb, rs)
|
||||
return sw.maybeFlushBuffer(bb)
|
||||
})
|
||||
if err == nil {
|
||||
err = sw.flush()
|
||||
}
|
||||
if err != nil && !netutil.IsTrivialNetworkError(err) {
|
||||
if err != nil {
|
||||
return fmt.Errorf("error during sending data to remote client: %w", err)
|
||||
}
|
||||
return nil
|
||||
return sw.flush()
|
||||
}
|
||||
|
||||
var federateDuration = metrics.NewSummary(`vm_request_duration_seconds{path="/federate"}`)
|
||||
@@ -230,13 +226,10 @@ func ExportCSVHandler(startTime time.Time, w http.ResponseWriter, r *http.Reques
|
||||
}()
|
||||
}
|
||||
err = <-doneCh
|
||||
if err == nil {
|
||||
err = sw.flush()
|
||||
}
|
||||
if err != nil && !netutil.IsTrivialNetworkError(err) {
|
||||
if err != nil {
|
||||
return fmt.Errorf("error during sending the exported csv data to remote client: %w", err)
|
||||
}
|
||||
return nil
|
||||
return sw.flush()
|
||||
}
|
||||
|
||||
var exportCSVDuration = metrics.NewSummary(`vm_request_duration_seconds{path="/api/v1/export/csv"}`)
|
||||
@@ -288,13 +281,10 @@ func ExportNativeHandler(startTime time.Time, w http.ResponseWriter, r *http.Req
|
||||
bb.B = dst
|
||||
return sw.maybeFlushBuffer(bb)
|
||||
})
|
||||
if err == nil {
|
||||
err = sw.flush()
|
||||
}
|
||||
if err != nil && !netutil.IsTrivialNetworkError(err) {
|
||||
if err != nil {
|
||||
return fmt.Errorf("error during sending native data to remote client: %w", err)
|
||||
}
|
||||
return nil
|
||||
return sw.flush()
|
||||
}
|
||||
|
||||
var exportNativeDuration = metrics.NewSummary(`vm_request_duration_seconds{path="/api/v1/export/native"}`)
|
||||
@@ -451,19 +441,16 @@ func exportHandler(qt *querytracer.Tracer, w http.ResponseWriter, cp *commonPara
|
||||
}()
|
||||
}
|
||||
err := <-doneCh
|
||||
if err == nil {
|
||||
err = sw.flush()
|
||||
}
|
||||
if err == nil {
|
||||
if format == "promapi" {
|
||||
WriteExportPromAPIFooter(bw, qt)
|
||||
}
|
||||
err = bw.Flush()
|
||||
}
|
||||
if err != nil && !netutil.IsTrivialNetworkError(err) {
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot send data to remote client: %w", err)
|
||||
}
|
||||
return nil
|
||||
if err := sw.flush(); err != nil {
|
||||
return fmt.Errorf("cannot send data to remote client: %w", err)
|
||||
}
|
||||
if format == "promapi" {
|
||||
WriteExportPromAPIFooter(bw, qt)
|
||||
}
|
||||
return bw.Flush()
|
||||
}
|
||||
|
||||
type exportBlock struct {
|
||||
@@ -494,8 +481,6 @@ func DeleteHandler(startTime time.Time, r *http.Request) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cp.deadline = searchutils.GetDeadlineForDelete(r, startTime)
|
||||
|
||||
if !cp.IsDefaultTimeRange() {
|
||||
return fmt.Errorf("start=%d and end=%d args aren't supported. Remove these args from the query in order to delete all the matching metrics", cp.start, cp.end)
|
||||
}
|
||||
|
||||
@@ -483,11 +483,8 @@ func binaryOpOr(bfa *binaryOpFuncArg) ([]*timeseries, error) {
|
||||
mLeft, mRight := createTimeseriesMapByTagSet(bfa.be, bfa.left, bfa.right)
|
||||
var rvs []*timeseries
|
||||
|
||||
for k, tss := range mLeft {
|
||||
tssLeft := removeEmptySeries(tss)
|
||||
// re-assign modified slice to map, since it can be referred later
|
||||
mLeft[k] = tssLeft
|
||||
rvs = append(rvs, tssLeft...)
|
||||
for _, tss := range mLeft {
|
||||
rvs = append(rvs, tss...)
|
||||
}
|
||||
// Sort left-hand-side series by metric name as Prometheus does.
|
||||
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/5393
|
||||
@@ -500,10 +497,7 @@ func binaryOpOr(bfa *binaryOpFuncArg) ([]*timeseries, error) {
|
||||
rvs = append(rvs, tssRight...)
|
||||
continue
|
||||
}
|
||||
fillLeftNaNsWithRightValuesOrMerge(tssLeft, tssRight)
|
||||
// tssRight might be filled with NaNs after merge
|
||||
tssRight = removeEmptySeries(tssRight)
|
||||
rvs = append(rvs, tssRight...)
|
||||
fillLeftNaNsWithRightValues(tssLeft, tssRight)
|
||||
}
|
||||
// Sort the added right-hand-side series by metric name as Prometheus does.
|
||||
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/5393
|
||||
@@ -532,35 +526,6 @@ func fillLeftNaNsWithRightValues(tssLeft, tssRight []*timeseries) {
|
||||
}
|
||||
}
|
||||
|
||||
// fill gaps in tssLeft with values from tssRight when labels match
|
||||
// Set NaNs to tssRight when tssLeft has corresponding values
|
||||
// or if tssLeft and tssRight can be merged.
|
||||
//
|
||||
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/7759
|
||||
// https://github.com/VictoriaMetrics/VictoriaMetrics/issues/7640
|
||||
func fillLeftNaNsWithRightValuesOrMerge(tssLeft, tssRight []*timeseries) {
|
||||
for _, tsLeft := range tssLeft {
|
||||
valuesLeft := tsLeft.Values
|
||||
nameLeft := tsLeft.MetricName.String()
|
||||
for i, v := range valuesLeft {
|
||||
leftIsNaN := math.IsNaN(v)
|
||||
for _, tsRight := range tssRight {
|
||||
canBeMerged := nameLeft == tsRight.MetricName.String()
|
||||
valueRight := tsRight.Values[i]
|
||||
if leftIsNaN && canBeMerged {
|
||||
// fill NaNs with valueRight if labels match
|
||||
valuesLeft[i] = valueRight
|
||||
}
|
||||
if !leftIsNaN || canBeMerged {
|
||||
// set NaN to valueRight if valueLeft is not NaN
|
||||
// or if left and right can be merged
|
||||
tsRight.Values[i] = nan
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func binaryOpIfnot(bfa *binaryOpFuncArg) ([]*timeseries, error) {
|
||||
mLeft, mRight := createTimeseriesMapByTagSet(bfa.be, bfa.left, bfa.right)
|
||||
var rvs []*timeseries
|
||||
|
||||
@@ -4461,9 +4461,9 @@ func TestExecSuccess(t *testing.T) {
|
||||
t.Run(`histogram_quantile(nan-bucket-count-some)`, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
q := `round(histogram_quantile(0.6,
|
||||
union(label_set(90, "foo", "bar", "le", "10"),
|
||||
label_set(NaN, "foo", "bar", "le", "30"),
|
||||
label_set(300, "foo", "bar", "le", "+Inf"))
|
||||
label_set(90, "foo", "bar", "le", "10")
|
||||
or label_set(NaN, "foo", "bar", "le", "30")
|
||||
or label_set(300, "foo", "bar", "le", "+Inf")
|
||||
),0.01)`
|
||||
r := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
@@ -9409,384 +9409,7 @@ func TestExecSuccess(t *testing.T) {
|
||||
resultExpected := []netstorage.Result{r}
|
||||
f(q, resultExpected)
|
||||
})
|
||||
t.Run(`nan or on() series`, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
// left side returns NaNs only, so the right side should replace its values and labels
|
||||
// https://github.com/VictoriaMetrics/VictoriaMetrics/issues/7759
|
||||
q := `(label_set(1, "a", "a", "b", "b1") == 0) or on(a) label_set(2, "a", "a", "b", "b2")`
|
||||
r := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
Values: []float64{2, 2, 2, 2, 2, 2},
|
||||
Timestamps: timestampsExpected,
|
||||
}
|
||||
r.MetricName.Tags = []storage.Tag{{
|
||||
Key: []byte("a"),
|
||||
Value: []byte("a"),
|
||||
}, {
|
||||
Key: []byte("b"),
|
||||
Value: []byte("b2"),
|
||||
}}
|
||||
resultExpected := []netstorage.Result{r}
|
||||
f(q, resultExpected)
|
||||
})
|
||||
t.Run(`series with NaNs or scalar`, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
q := `(label_set(time() >= 1600, "a", "a", "b", "b1")) or 1`
|
||||
r1 := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
Values: []float64{nan, nan, nan, 1600, 1800, 2000},
|
||||
Timestamps: timestampsExpected,
|
||||
}
|
||||
r1.MetricName.Tags = []storage.Tag{{
|
||||
Key: []byte("a"),
|
||||
Value: []byte("a"),
|
||||
}, {
|
||||
Key: []byte("b"),
|
||||
Value: []byte("b1"),
|
||||
}}
|
||||
|
||||
r2 := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
Values: []float64{1, 1, 1, 1, 1, 1},
|
||||
Timestamps: timestampsExpected,
|
||||
}
|
||||
resultExpected := []netstorage.Result{r1, r2}
|
||||
f(q, resultExpected)
|
||||
})
|
||||
t.Run(`series or on() scalar`, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
// https://github.com/VictoriaMetrics/VictoriaMetrics/issues/7640
|
||||
q := `(label_set(time() > 1200, "a", "a", "b", "b1")) or on() vector(0)`
|
||||
r1 := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
Values: []float64{nan, nan, 1400, 1600, 1800, 2000},
|
||||
Timestamps: timestampsExpected,
|
||||
}
|
||||
r1.MetricName.Tags = []storage.Tag{{
|
||||
Key: []byte("a"),
|
||||
Value: []byte("a"),
|
||||
}, {
|
||||
Key: []byte("b"),
|
||||
Value: []byte("b1"),
|
||||
}}
|
||||
|
||||
r2 := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
Values: []float64{0, 0, nan, nan, nan, nan},
|
||||
Timestamps: timestampsExpected,
|
||||
}
|
||||
resultExpected := []netstorage.Result{r1, r2}
|
||||
f(q, resultExpected)
|
||||
})
|
||||
t.Run(`series or on() series`, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
// left side + right side
|
||||
q := `(label_set(time() <= 1200, "a", "a", "b", "b1")) or on(a) label_set(time() > 1200, "a", "a", "b", "b2")`
|
||||
r1 := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
Values: []float64{1000, 1200, nan, nan, nan, nan},
|
||||
Timestamps: timestampsExpected,
|
||||
}
|
||||
r1.MetricName.Tags = []storage.Tag{{
|
||||
Key: []byte("a"),
|
||||
Value: []byte("a"),
|
||||
}, {
|
||||
Key: []byte("b"),
|
||||
Value: []byte("b1"),
|
||||
}}
|
||||
|
||||
r2 := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
Values: []float64{nan, nan, 1400, 1600, 1800, 2000},
|
||||
Timestamps: timestampsExpected,
|
||||
}
|
||||
r2.MetricName.Tags = []storage.Tag{{
|
||||
Key: []byte("a"),
|
||||
Value: []byte("a"),
|
||||
}, {
|
||||
Key: []byte("b"),
|
||||
Value: []byte("b2"),
|
||||
}}
|
||||
resultExpected := []netstorage.Result{r1, r2}
|
||||
f(q, resultExpected)
|
||||
})
|
||||
t.Run(`series with no NaNs or on() series`, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
// left side contains all needed values, so the right side should be dropped
|
||||
q := `(label_set(time() < 3000, "a", "a", "b", "b1")) or on(a) label_set(time() > 3000, "a", "a", "b", "b2")`
|
||||
r := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
Values: []float64{1000, 1200, 1400, 1600, 1800, 2000},
|
||||
Timestamps: timestampsExpected,
|
||||
}
|
||||
r.MetricName.Tags = []storage.Tag{{
|
||||
Key: []byte("a"),
|
||||
Value: []byte("a"),
|
||||
}, {
|
||||
Key: []byte("b"),
|
||||
Value: []byte("b1"),
|
||||
}}
|
||||
|
||||
resultExpected := []netstorage.Result{r}
|
||||
f(q, resultExpected)
|
||||
})
|
||||
t.Run(`series or on() series with overlap`, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
// left overlap with right
|
||||
q := `(label_set(time() <= 1500, "a", "a", "b", "b1")) or on(a) label_set(time() > 1100, "a", "a", "b", "b2")`
|
||||
r1 := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
Values: []float64{1000, 1200, 1400, nan, nan, nan},
|
||||
Timestamps: timestampsExpected,
|
||||
}
|
||||
r1.MetricName.Tags = []storage.Tag{{
|
||||
Key: []byte("a"),
|
||||
Value: []byte("a"),
|
||||
}, {
|
||||
Key: []byte("b"),
|
||||
Value: []byte("b1"),
|
||||
}}
|
||||
|
||||
r2 := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
Values: []float64{nan, nan, nan, 1600, 1800, 2000},
|
||||
Timestamps: timestampsExpected,
|
||||
}
|
||||
r2.MetricName.Tags = []storage.Tag{{
|
||||
Key: []byte("a"),
|
||||
Value: []byte("a"),
|
||||
}, {
|
||||
Key: []byte("b"),
|
||||
Value: []byte("b2"),
|
||||
}}
|
||||
|
||||
resultExpected := []netstorage.Result{r1, r2}
|
||||
f(q, resultExpected)
|
||||
})
|
||||
t.Run(`series or on() series merge`, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
// left + right for same series
|
||||
q := `(label_set(time() <= 1200, "a", "a", "b", "b1")) or on(a) label_set(time() > 1400, "a", "a", "b", "b1")`
|
||||
r := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
Values: []float64{1000, 1200, nan, 1600, 1800, 2000},
|
||||
Timestamps: timestampsExpected,
|
||||
}
|
||||
r.MetricName.Tags = []storage.Tag{{
|
||||
Key: []byte("a"),
|
||||
Value: []byte("a"),
|
||||
}, {
|
||||
Key: []byte("b"),
|
||||
Value: []byte("b1"),
|
||||
}}
|
||||
|
||||
resultExpected := []netstorage.Result{r}
|
||||
f(q, resultExpected)
|
||||
})
|
||||
t.Run(`scalar or timeseries`, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
q := `time() > 1400 or label_set(123, "foo", "bar")`
|
||||
r1 := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
Values: []float64{nan, nan, nan, 1600, 1800, 2000},
|
||||
Timestamps: timestampsExpected,
|
||||
}
|
||||
r2 := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
Values: []float64{123, 123, 123, 123, 123, 123},
|
||||
Timestamps: timestampsExpected,
|
||||
}
|
||||
r2.MetricName.Tags = []storage.Tag{{
|
||||
Key: []byte("foo"),
|
||||
Value: []byte("bar"),
|
||||
}}
|
||||
resultExpected := []netstorage.Result{r1, r2}
|
||||
f(q, resultExpected)
|
||||
})
|
||||
t.Run(`series or many series`, func(t *testing.T) {
|
||||
//load 1m
|
||||
// foo{a="a", b="1"} 1 0 1 1 1
|
||||
// bar{a="a", b="2"} 2 2 2 2 2
|
||||
// bar{a="a", b="3"} 3 3 3 3 3
|
||||
//
|
||||
//eval range from 0 to 4m step 1m foo!=0 or on (a) bar
|
||||
// foo{a="a", b="1"} 1 _ 1 1 1
|
||||
// bar{a="a", b="2"} _ 2 _ _ _
|
||||
// bar{a="a", b="3"} _ 3 _ _ _
|
||||
// https://github.com/prometheus/prometheus/tree/main/promql/promqltest
|
||||
t.Parallel()
|
||||
q := `(
|
||||
label_set(time()!=1200, "x", "foo"),
|
||||
) or on(x) (
|
||||
label_set(time()+1, "x", "foo", "y", "bar"),
|
||||
label_set(time()+2, "y", "baz", "x", "foo"),
|
||||
)`
|
||||
r1 := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
Values: []float64{1000, nan, 1400, 1600, 1800, 2000},
|
||||
Timestamps: timestampsExpected,
|
||||
}
|
||||
r1.MetricName.Tags = []storage.Tag{
|
||||
{Key: []byte("x"), Value: []byte("foo")},
|
||||
}
|
||||
r2 := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
Values: []float64{nan, 1201, nan, nan, nan, nan},
|
||||
Timestamps: timestampsExpected,
|
||||
}
|
||||
r2.MetricName.Tags = []storage.Tag{
|
||||
{Key: []byte("x"), Value: []byte("foo")},
|
||||
{Key: []byte("y"), Value: []byte("bar")},
|
||||
}
|
||||
r3 := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
Values: []float64{nan, 1202, nan, nan, nan, nan},
|
||||
Timestamps: timestampsExpected,
|
||||
}
|
||||
r3.MetricName.Tags = []storage.Tag{
|
||||
{Key: []byte("x"), Value: []byte("foo")},
|
||||
{Key: []byte("y"), Value: []byte("baz")},
|
||||
}
|
||||
resultExpected := []netstorage.Result{r1, r2, r3}
|
||||
f(q, resultExpected)
|
||||
})
|
||||
t.Run(`many series or series`, func(t *testing.T) {
|
||||
//load 1m
|
||||
// foo{a="a", b="1"} 1 0 1 1 1
|
||||
// foo{a="a", b="2"} 2 2 2 2 2
|
||||
// bar{a="a", b="3"} 3 3 3 3 3
|
||||
//
|
||||
//eval range from 0 to 4m step 1m foo!=0 or on (a) bar
|
||||
// foo{a="a", b="1"} 1 _ 1 1 1
|
||||
// foo{a="a", b="2"} 2 2 2 2 2
|
||||
// https://github.com/prometheus/prometheus/tree/main/promql/promqltest
|
||||
t.Parallel()
|
||||
q := `(
|
||||
label_set(time()!=1200, "x", "foo"),
|
||||
label_set(time()+1, "x", "foo", "y","baz"),
|
||||
) or on(x) (
|
||||
label_set(time()+2, "x", "foo", "y", "bar"),
|
||||
)`
|
||||
r1 := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
Values: []float64{1000, nan, 1400, 1600, 1800, 2000},
|
||||
Timestamps: timestampsExpected,
|
||||
}
|
||||
r1.MetricName.Tags = []storage.Tag{
|
||||
{Key: []byte("x"), Value: []byte("foo")},
|
||||
}
|
||||
r2 := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
Values: []float64{1001, 1201, 1401, 1601, 1801, 2001},
|
||||
Timestamps: timestampsExpected,
|
||||
}
|
||||
r2.MetricName.Tags = []storage.Tag{
|
||||
{Key: []byte("x"), Value: []byte("foo")},
|
||||
{Key: []byte("y"), Value: []byte("baz")},
|
||||
}
|
||||
resultExpected := []netstorage.Result{r1, r2}
|
||||
f(q, resultExpected)
|
||||
})
|
||||
t.Run(`many series or series with no merge`, func(t *testing.T) {
|
||||
// load 1m
|
||||
// foo{job="a1", a="a"} 0 0 1 1 0
|
||||
// foo{job="a2", a="a"} 1 1 0 0 0
|
||||
// foo{job="a3", a="a"} 1 1 1 1 1
|
||||
// foo{job="a4", a="a"} 1 1 1 1 1
|
||||
//
|
||||
//eval range from 0 to 4m step 1m (foo{job=~"a1|a2"} == 0) or on (a) (foo{job=~"a3|a4"} == 1)
|
||||
// foo{job="a1", a="a"} 0 0 _ _ 0
|
||||
// foo{job="a2", a="a"} _ _ 0 0 0
|
||||
// https://github.com/prometheus/prometheus/tree/main/promql/promqltest
|
||||
t.Parallel()
|
||||
q := `(
|
||||
label_set(time()!=1400, "job", "a1", "a", "a"),
|
||||
label_set(time()>=1400, "job", "a2", "a", "a"),
|
||||
) or on(a) (
|
||||
label_set(time(), "job", "a3", "a", "a"),
|
||||
label_set(time(), "job", "a4", "a", "a"),
|
||||
)`
|
||||
r1 := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
Values: []float64{1000, 1200, nan, 1600, 1800, 2000},
|
||||
Timestamps: timestampsExpected,
|
||||
}
|
||||
r1.MetricName.Tags = []storage.Tag{
|
||||
{Key: []byte("a"), Value: []byte("a")},
|
||||
{Key: []byte("job"), Value: []byte("a1")},
|
||||
}
|
||||
r2 := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
Values: []float64{nan, nan, 1400, 1600, 1800, 2000},
|
||||
Timestamps: timestampsExpected,
|
||||
}
|
||||
r2.MetricName.Tags = []storage.Tag{
|
||||
{Key: []byte("a"), Value: []byte("a")},
|
||||
{Key: []byte("job"), Value: []byte("a2")},
|
||||
}
|
||||
resultExpected := []netstorage.Result{r1, r2}
|
||||
f(q, resultExpected)
|
||||
})
|
||||
t.Run(`many series or series with merge`, func(t *testing.T) {
|
||||
// load 1m
|
||||
// foo{job="a1", a="a"} 0 0 1 1 0
|
||||
// foo{job="a2", a="a"} 1 1 1 0 0
|
||||
// foo{job="a3", a="a"} 1 1 1 1 1
|
||||
// foo{job="a4", a="a"} 1 1 1 1 1
|
||||
//
|
||||
//eval range from 0 to 4m step 1m (foo{job=~"a1|a2"} == 0) or on (a) (foo{job=~"a3|a4"} == 1)
|
||||
// foo{job="a1", a="a"} 0 0 _ _ 0
|
||||
// foo{job="a2", a="a"} _ _ _ 0 0
|
||||
// foo{job="a3", a="a"} _ _ 1 _ _
|
||||
// foo{job="a4", a="a"} _ _ 1 _ _
|
||||
// https://github.com/prometheus/prometheus/tree/main/promql/promqltest
|
||||
t.Parallel()
|
||||
q := `(
|
||||
label_set(time()!=1400, "job", "a1", "a", "a"),
|
||||
label_set(time()>=1600, "job", "a2", "a", "a"),
|
||||
) or on(a) (
|
||||
label_set(time(), "job", "a3", "a", "a"),
|
||||
label_set(time(), "job", "a4", "a", "a"),
|
||||
)`
|
||||
r1 := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
Values: []float64{1000, 1200, nan, 1600, 1800, 2000},
|
||||
Timestamps: timestampsExpected,
|
||||
}
|
||||
r1.MetricName.Tags = []storage.Tag{
|
||||
{Key: []byte("a"), Value: []byte("a")},
|
||||
{Key: []byte("job"), Value: []byte("a1")},
|
||||
}
|
||||
r2 := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
Values: []float64{nan, nan, nan, 1600, 1800, 2000},
|
||||
Timestamps: timestampsExpected,
|
||||
}
|
||||
r2.MetricName.Tags = []storage.Tag{
|
||||
{Key: []byte("a"), Value: []byte("a")},
|
||||
{Key: []byte("job"), Value: []byte("a2")},
|
||||
}
|
||||
r3 := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
Values: []float64{nan, nan, 1400, nan, nan, nan},
|
||||
Timestamps: timestampsExpected,
|
||||
}
|
||||
r3.MetricName.Tags = []storage.Tag{
|
||||
{Key: []byte("a"), Value: []byte("a")},
|
||||
{Key: []byte("job"), Value: []byte("a3")},
|
||||
}
|
||||
r4 := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
Values: []float64{nan, nan, 1400, nan, nan, nan},
|
||||
Timestamps: timestampsExpected,
|
||||
}
|
||||
r4.MetricName.Tags = []storage.Tag{
|
||||
{Key: []byte("a"), Value: []byte("a")},
|
||||
{Key: []byte("job"), Value: []byte("a4")},
|
||||
}
|
||||
resultExpected := []netstorage.Result{r1, r2, r3, r4}
|
||||
f(q, resultExpected)
|
||||
})
|
||||
}
|
||||
|
||||
func TestExecError(t *testing.T) {
|
||||
|
||||
@@ -374,8 +374,8 @@ func getRollupConfigs(funcName string, rf rollupFunc, expr metricsql.Expr, start
|
||||
preFunc := func(_ []float64, _ []int64) {}
|
||||
funcName = strings.ToLower(funcName)
|
||||
if rollupFuncsRemoveCounterResets[funcName] {
|
||||
preFunc = func(values []float64, timestamps []int64) {
|
||||
removeCounterResets(values, timestamps, lookbackDelta)
|
||||
preFunc = func(values []float64, _ []int64) {
|
||||
removeCounterResets(values)
|
||||
}
|
||||
}
|
||||
samplesScannedPerCall := rollupFuncsSamplesScannedPerCall[funcName]
|
||||
@@ -486,8 +486,8 @@ func getRollupConfigs(funcName string, rf rollupFunc, expr metricsql.Expr, start
|
||||
for _, aggrFuncName := range aggrFuncNames {
|
||||
if rollupFuncsRemoveCounterResets[aggrFuncName] {
|
||||
// There is no need to save the previous preFunc, since it is either empty or the same.
|
||||
preFunc = func(values []float64, timestamps []int64) {
|
||||
removeCounterResets(values, timestamps, lookbackDelta)
|
||||
preFunc = func(values []float64, _ []int64) {
|
||||
removeCounterResets(values)
|
||||
}
|
||||
}
|
||||
rf := rollupAggrFuncs[aggrFuncName]
|
||||
@@ -521,7 +521,7 @@ type rollupFuncArg struct {
|
||||
timestamps []int64
|
||||
|
||||
// Real value preceding values.
|
||||
// Is populated if preceding value is within the -search.maxStalenessInterval (rc.LookbackDelta).
|
||||
// Is populated if preceding value is within the staleness interval.
|
||||
realPrevValue float64
|
||||
|
||||
// Real value which goes after values.
|
||||
@@ -768,13 +768,7 @@ func (rc *rollupConfig) doInternal(dstValues []float64, tsm *timeseriesMap, valu
|
||||
rfa.realPrevValue = nan
|
||||
if i > 0 {
|
||||
prevValue, prevTimestamp := values[i-1], timestamps[i-1]
|
||||
// set realPrevValue if rc.LookbackDelta == 0
|
||||
// or if distance between datapoint in prev interval and beginning of this interval
|
||||
// doesn't exceed LookbackDelta.
|
||||
// https://github.com/VictoriaMetrics/VictoriaMetrics/pull/1381
|
||||
// https://github.com/VictoriaMetrics/VictoriaMetrics/issues/894
|
||||
// https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8045
|
||||
if rc.LookbackDelta == 0 || (tStart-prevTimestamp) < rc.LookbackDelta {
|
||||
if (tEnd - prevTimestamp) < maxPrevInterval {
|
||||
rfa.realPrevValue = prevValue
|
||||
}
|
||||
}
|
||||
@@ -900,7 +894,7 @@ func getMaxPrevInterval(scrapeInterval int64) int64 {
|
||||
return scrapeInterval + scrapeInterval/8
|
||||
}
|
||||
|
||||
func removeCounterResets(values []float64, timestamps []int64, maxStalenessInterval int64) {
|
||||
func removeCounterResets(values []float64) {
|
||||
// There is no need in handling NaNs here, since they are impossible
|
||||
// on values from vmstorage.
|
||||
if len(values) == 0 {
|
||||
@@ -919,16 +913,6 @@ func removeCounterResets(values []float64, timestamps []int64, maxStalenessInter
|
||||
correction += prevValue
|
||||
}
|
||||
}
|
||||
if i > 0 && maxStalenessInterval > 0 {
|
||||
gap := timestamps[i] - timestamps[i-1]
|
||||
if gap > maxStalenessInterval {
|
||||
// reset correction if gap between samples exceeds staleness interval
|
||||
// see https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8072
|
||||
correction = 0
|
||||
prevValue = v
|
||||
continue
|
||||
}
|
||||
}
|
||||
prevValue = v
|
||||
values[i] = v + correction
|
||||
// Check again, there could be precision error in float operations,
|
||||
|
||||
@@ -117,49 +117,31 @@ func TestRollupIderivDuplicateTimestamps(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRemoveCounterResets(t *testing.T) {
|
||||
removeCounterResets(nil, nil, 0)
|
||||
removeCounterResets(nil)
|
||||
|
||||
values := append([]float64{}, testValues...)
|
||||
timestamps := append([]int64{}, testTimestamps...)
|
||||
removeCounterResets(values, timestamps, 0)
|
||||
removeCounterResets(values)
|
||||
valuesExpected := []float64{123, 157, 167, 188, 221, 255, 320, 332, 364, 396, 398, 398}
|
||||
testRowsEqual(t, values, testTimestamps, valuesExpected, testTimestamps)
|
||||
|
||||
// removeCounterResets doesn't expect negative values, so it doesn't work properly with them.
|
||||
values = []float64{-100, -200, -300, -400}
|
||||
timestampsExpected := []int64{0, 1, 2, 3}
|
||||
removeCounterResets(values, timestampsExpected, 0)
|
||||
removeCounterResets(values)
|
||||
valuesExpected = []float64{-100, -100, -100, -100}
|
||||
timestampsExpected := []int64{0, 1, 2, 3}
|
||||
testRowsEqual(t, values, timestampsExpected, valuesExpected, timestampsExpected)
|
||||
|
||||
// verify how partial counter reset is handled.
|
||||
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2787
|
||||
values = []float64{100, 95, 120, 119, 139, 50}
|
||||
timestampsExpected = []int64{0, 1, 2, 3, 4, 5}
|
||||
removeCounterResets(values, timestampsExpected, 0)
|
||||
removeCounterResets(values)
|
||||
valuesExpected = []float64{100, 100, 125, 125, 145, 195}
|
||||
timestampsExpected = []int64{0, 1, 2, 3, 4, 5}
|
||||
testRowsEqual(t, values, timestampsExpected, valuesExpected, timestampsExpected)
|
||||
|
||||
// verify that staleness interval is respected during resets
|
||||
// see https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8072
|
||||
values = []float64{10, 12, 14, 4, 6, 8, 6, 8, 4, 6}
|
||||
timestamps = []int64{10, 20, 30, 60, 70, 80, 90, 100, 120, 130}
|
||||
valuesExpected = []float64{10, 12, 14, 4, 6, 8, 14, 16, 4, 6}
|
||||
removeCounterResets(values, timestamps, 10)
|
||||
testRowsEqual(t, values, timestamps, valuesExpected, timestamps)
|
||||
|
||||
// verify that staleness is respected if there was no counter reset
|
||||
// but correction was made previously
|
||||
values = []float64{10, 12, 2, 4}
|
||||
timestamps = []int64{10, 20, 30, 60}
|
||||
valuesExpected = []float64{10, 12, 14, 4}
|
||||
removeCounterResets(values, timestamps, 10)
|
||||
testRowsEqual(t, values, timestamps, valuesExpected, timestamps)
|
||||
|
||||
// verify results always increase monotonically with possible float operations precision error
|
||||
values = []float64{34.094223, 2.7518, 2.140669, 0.044878, 1.887095, 2.546569, 2.490149, 0.045, 0.035684, 0.062454, 0.058296}
|
||||
timestampsExpected = []int64{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
|
||||
removeCounterResets(values, timestampsExpected, 0)
|
||||
removeCounterResets(values)
|
||||
var prev float64
|
||||
for i, v := range values {
|
||||
if v < prev {
|
||||
@@ -184,7 +166,7 @@ func TestDeltaValues(t *testing.T) {
|
||||
|
||||
// remove counter resets
|
||||
values = append([]float64{}, testValues...)
|
||||
removeCounterResets(values, testTimestamps, 0)
|
||||
removeCounterResets(values)
|
||||
deltaValues(values)
|
||||
valuesExpected = []float64{34, 10, 21, 33, 34, 65, 12, 32, 32, 2, 0, 0}
|
||||
testRowsEqual(t, values, testTimestamps, valuesExpected, testTimestamps)
|
||||
@@ -206,7 +188,7 @@ func TestDerivValues(t *testing.T) {
|
||||
|
||||
// remove counter resets
|
||||
values = append([]float64{}, testValues...)
|
||||
removeCounterResets(values, testTimestamps, 0)
|
||||
removeCounterResets(values)
|
||||
derivValues(values, testTimestamps)
|
||||
valuesExpected = []float64{3400, 1111.111111111111, 1750, 2538.4615384615386, 3090.909090909091, 3611.1111111111113,
|
||||
6000, 1882.3529411764705, 1777.7777777777778, 400, 0, 0}
|
||||
@@ -237,7 +219,7 @@ func testRollupFunc(t *testing.T, funcName string, args []any, vExpected float64
|
||||
rfa.timestamps = append(rfa.timestamps, testTimestamps...)
|
||||
rfa.window = rfa.timestamps[len(rfa.timestamps)-1] - rfa.timestamps[0]
|
||||
if rollupFuncsRemoveCounterResets[funcName] {
|
||||
removeCounterResets(rfa.values, rfa.timestamps, 0)
|
||||
removeCounterResets(rfa.values)
|
||||
}
|
||||
for i := 0; i < 5; i++ {
|
||||
v := rf(&rfa)
|
||||
@@ -1608,60 +1590,17 @@ func TestRollupDelta(t *testing.T) {
|
||||
f(100, nan, nan, nil, 0)
|
||||
}
|
||||
|
||||
func TestRollupDeltaWithStaleness(t *testing.T) {
|
||||
func TestRollupIncreaseWithStaleness(t *testing.T) {
|
||||
// there is a gap between samples in the dataset below
|
||||
timestamps := []int64{0, 15000, 30000, 70000}
|
||||
values := []float64{1, 1, 1, 1}
|
||||
|
||||
// if step > gap, then delta will always respect value before gap
|
||||
t.Run("step>gap", func(t *testing.T) {
|
||||
t.Run("step > gap", func(t *testing.T) {
|
||||
rc := rollupConfig{
|
||||
Func: rollupDelta,
|
||||
Start: 0,
|
||||
End: 70000,
|
||||
Step: 45000,
|
||||
Window: 0,
|
||||
MaxPointsPerSeries: 1e4,
|
||||
}
|
||||
rc.Timestamps = rc.getTimestamps()
|
||||
gotValues, samplesScanned := rc.Do(nil, values, timestamps)
|
||||
if samplesScanned != 7 {
|
||||
t.Fatalf("expecting 8 samplesScanned from rollupConfig.Do; got %d", samplesScanned)
|
||||
}
|
||||
valuesExpected := []float64{1, 0}
|
||||
timestampsExpected := []int64{0, 45e3}
|
||||
testRowsEqual(t, gotValues, rc.Timestamps, valuesExpected, timestampsExpected)
|
||||
})
|
||||
// even if LookbackDelta < gap
|
||||
t.Run("step>gap;LookbackDelta<gap", func(t *testing.T) {
|
||||
rc := rollupConfig{
|
||||
Func: rollupDelta,
|
||||
Start: 0,
|
||||
End: 70000,
|
||||
Step: 45000,
|
||||
LookbackDelta: 10e3,
|
||||
Window: 0,
|
||||
MaxPointsPerSeries: 1e4,
|
||||
}
|
||||
rc.Timestamps = rc.getTimestamps()
|
||||
gotValues, samplesScanned := rc.Do(nil, values, timestamps)
|
||||
if samplesScanned != 7 {
|
||||
t.Fatalf("expecting 8 samplesScanned from rollupConfig.Do; got %d", samplesScanned)
|
||||
}
|
||||
valuesExpected := []float64{1, 0}
|
||||
timestampsExpected := []int64{0, 45e3}
|
||||
testRowsEqual(t, gotValues, rc.Timestamps, valuesExpected, timestampsExpected)
|
||||
})
|
||||
|
||||
// if step < gap and LookbackDelta==0 then delta will always respect value before gap
|
||||
// as LookbackDelta=0 ignores staleness
|
||||
t.Run("step<gap;LookbackDelta=0", func(t *testing.T) {
|
||||
rc := rollupConfig{
|
||||
Func: rollupDelta,
|
||||
Start: 0,
|
||||
End: 70000,
|
||||
Step: 10000,
|
||||
LookbackDelta: 0,
|
||||
Step: 35000,
|
||||
Window: 0,
|
||||
MaxPointsPerSeries: 1e4,
|
||||
}
|
||||
@@ -1670,14 +1609,12 @@ func TestRollupDeltaWithStaleness(t *testing.T) {
|
||||
if samplesScanned != 8 {
|
||||
t.Fatalf("expecting 8 samplesScanned from rollupConfig.Do; got %d", samplesScanned)
|
||||
}
|
||||
valuesExpected := []float64{1, 0, 0, 0, 0, 0, 0, 0}
|
||||
timestampsExpected := []int64{0, 10e3, 20e3, 30e3, 40e3, 50e3, 60e3, 70e3}
|
||||
valuesExpected := []float64{1, 0, 0}
|
||||
timestampsExpected := []int64{0, 35e3, 70e3}
|
||||
testRowsEqual(t, gotValues, rc.Timestamps, valuesExpected, timestampsExpected)
|
||||
})
|
||||
|
||||
// if step < gap and LookbackDelta>0 then delta will respect value before gap
|
||||
// only if it is not stale according to LookbackDelta
|
||||
t.Run("step<gap;LookbackDelta>0", func(t *testing.T) {
|
||||
t.Run("step < gap", func(t *testing.T) {
|
||||
rc := rollupConfig{
|
||||
Func: rollupDelta,
|
||||
Start: 0,
|
||||
@@ -1685,7 +1622,6 @@ func TestRollupDeltaWithStaleness(t *testing.T) {
|
||||
Step: 10000,
|
||||
Window: 0,
|
||||
MaxPointsPerSeries: 1e4,
|
||||
LookbackDelta: 30e3,
|
||||
}
|
||||
rc.Timestamps = rc.getTimestamps()
|
||||
gotValues, samplesScanned := rc.Do(nil, values, timestamps)
|
||||
@@ -1720,116 +1656,3 @@ func TestRollupDeltaWithStaleness(t *testing.T) {
|
||||
testRowsEqual(t, gotValues, rc.Timestamps, valuesExpected, timestampsExpected)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRollupIncreasePureWithStaleness(t *testing.T) {
|
||||
// there is a gap between samples in the dataset below
|
||||
timestamps := []int64{0, 15000, 30000, 70000}
|
||||
values := []float64{1, 1, 1, 1}
|
||||
|
||||
// if step > gap, then delta will always respect value before gap
|
||||
t.Run("step>gap", func(t *testing.T) {
|
||||
rc := rollupConfig{
|
||||
Func: rollupIncreasePure,
|
||||
Start: 0,
|
||||
End: 70000,
|
||||
Step: 45000,
|
||||
Window: 0,
|
||||
MaxPointsPerSeries: 1e4,
|
||||
}
|
||||
rc.Timestamps = rc.getTimestamps()
|
||||
gotValues, samplesScanned := rc.Do(nil, values, timestamps)
|
||||
if samplesScanned != 7 {
|
||||
t.Fatalf("expecting 8 samplesScanned from rollupConfig.Do; got %d", samplesScanned)
|
||||
}
|
||||
valuesExpected := []float64{1, 0}
|
||||
timestampsExpected := []int64{0, 45e3}
|
||||
testRowsEqual(t, gotValues, rc.Timestamps, valuesExpected, timestampsExpected)
|
||||
})
|
||||
// even if LookbackDelta < gap
|
||||
t.Run("step>gap;LookbackDelta<gap", func(t *testing.T) {
|
||||
rc := rollupConfig{
|
||||
Func: rollupIncreasePure,
|
||||
Start: 0,
|
||||
End: 70000,
|
||||
Step: 45000,
|
||||
LookbackDelta: 10e3,
|
||||
Window: 0,
|
||||
MaxPointsPerSeries: 1e4,
|
||||
}
|
||||
rc.Timestamps = rc.getTimestamps()
|
||||
gotValues, samplesScanned := rc.Do(nil, values, timestamps)
|
||||
if samplesScanned != 7 {
|
||||
t.Fatalf("expecting 8 samplesScanned from rollupConfig.Do; got %d", samplesScanned)
|
||||
}
|
||||
valuesExpected := []float64{1, 0}
|
||||
timestampsExpected := []int64{0, 45e3}
|
||||
testRowsEqual(t, gotValues, rc.Timestamps, valuesExpected, timestampsExpected)
|
||||
})
|
||||
|
||||
// if step < gap and LookbackDelta==0 then delta will always respect value before gap
|
||||
// as LookbackDelta=0 ignores staleness
|
||||
t.Run("step<gap;LookbackDelta=0", func(t *testing.T) {
|
||||
rc := rollupConfig{
|
||||
Func: rollupIncreasePure,
|
||||
Start: 0,
|
||||
End: 70000,
|
||||
Step: 10000,
|
||||
LookbackDelta: 0,
|
||||
Window: 0,
|
||||
MaxPointsPerSeries: 1e4,
|
||||
}
|
||||
rc.Timestamps = rc.getTimestamps()
|
||||
gotValues, samplesScanned := rc.Do(nil, values, timestamps)
|
||||
if samplesScanned != 8 {
|
||||
t.Fatalf("expecting 8 samplesScanned from rollupConfig.Do; got %d", samplesScanned)
|
||||
}
|
||||
valuesExpected := []float64{1, 0, 0, 0, 0, 0, 0, 0}
|
||||
timestampsExpected := []int64{0, 10e3, 20e3, 30e3, 40e3, 50e3, 60e3, 70e3}
|
||||
testRowsEqual(t, gotValues, rc.Timestamps, valuesExpected, timestampsExpected)
|
||||
})
|
||||
|
||||
// if step < gap and LookbackDelta>0 then delta will respect value before gap
|
||||
// only if it is not stale according to LookbackDelta
|
||||
t.Run("step<gap;LookbackDelta>0", func(t *testing.T) {
|
||||
rc := rollupConfig{
|
||||
Func: rollupIncreasePure,
|
||||
Start: 0,
|
||||
End: 70000,
|
||||
Step: 10000,
|
||||
Window: 0,
|
||||
MaxPointsPerSeries: 1e4,
|
||||
LookbackDelta: 30e3,
|
||||
}
|
||||
rc.Timestamps = rc.getTimestamps()
|
||||
gotValues, samplesScanned := rc.Do(nil, values, timestamps)
|
||||
if samplesScanned != 8 {
|
||||
t.Fatalf("expecting 8 samplesScanned from rollupConfig.Do; got %d", samplesScanned)
|
||||
}
|
||||
valuesExpected := []float64{1, 0, 0, 0, 0, 0, 0, 1}
|
||||
timestampsExpected := []int64{0, 10e3, 20e3, 30e3, 40e3, 50e3, 60e3, 70e3}
|
||||
testRowsEqual(t, gotValues, rc.Timestamps, valuesExpected, timestampsExpected)
|
||||
})
|
||||
|
||||
// there is a staleness marker between samples in the dataset below
|
||||
timestamps = []int64{0, 10000, 20000, 30000, 40000}
|
||||
values = []float64{1, 1, 1, decimal.StaleNaN, 1}
|
||||
|
||||
t.Run("staleness marker", func(t *testing.T) {
|
||||
rc := rollupConfig{
|
||||
Func: rollupIncreasePure,
|
||||
Start: 0,
|
||||
End: 40000,
|
||||
Step: 10000,
|
||||
Window: 0,
|
||||
MaxPointsPerSeries: 1e4,
|
||||
}
|
||||
rc.Timestamps = rc.getTimestamps()
|
||||
gotValues, samplesScanned := rc.Do(nil, values, timestamps)
|
||||
if samplesScanned != 10 {
|
||||
t.Fatalf("expecting 10 samplesScanned from rollupConfig.Do; got %d", samplesScanned)
|
||||
}
|
||||
valuesExpected := []float64{1, 0, 0, nan, 1}
|
||||
timestampsExpected := []int64{0, 10e3, 20e3, 30e3, 40e3}
|
||||
testRowsEqual(t, gotValues, rc.Timestamps, valuesExpected, timestampsExpected)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ import (
|
||||
|
||||
var (
|
||||
maxExportDuration = flag.Duration("search.maxExportDuration", time.Hour*24*30, "The maximum duration for /api/v1/export call")
|
||||
maxDeleteDuration = flag.Duration("search.maxDeleteDuration", time.Minute*5, "The maximum duration for /api/v1/admin/tsdb/delete_series call")
|
||||
maxQueryDuration = flag.Duration("search.maxQueryDuration", time.Second*30, "The maximum duration for query execution. It can be overridden to a smaller value on a per-query basis via 'timeout' query arg")
|
||||
maxStatusRequestDuration = flag.Duration("search.maxStatusRequestDuration", time.Minute*5, "The maximum duration for /api/v1/status/* requests")
|
||||
maxLabelsAPIDuration = flag.Duration("search.maxLabelsAPIDuration", time.Second*5, "The maximum duration for /api/v1/labels, /api/v1/label/.../values and /api/v1/series requests. "+
|
||||
@@ -59,12 +58,6 @@ func GetDeadlineForLabelsAPI(r *http.Request, startTime time.Time) Deadline {
|
||||
return getDeadlineWithMaxDuration(r, startTime, dMax, "-search.maxLabelsAPIDuration")
|
||||
}
|
||||
|
||||
// GetDeadlineForDelete returns deadline for the given request to /api/v1/admin/tsdb/delete_series.
|
||||
func GetDeadlineForDelete(r *http.Request, startTime time.Time) Deadline {
|
||||
dMax := maxDeleteDuration.Milliseconds()
|
||||
return getDeadlineWithMaxDuration(r, startTime, dMax, "-search.maxDeleteDuration")
|
||||
}
|
||||
|
||||
func getDeadlineWithMaxDuration(r *http.Request, startTime time.Time, dMax int64, flagHint string) Deadline {
|
||||
d, err := httputils.GetDuration(r, "timeout", 0)
|
||||
if err != nil {
|
||||
|
||||
@@ -99,8 +99,7 @@ func TestParseMetricSelectorSuccess(t *testing.T) {
|
||||
f(`{foo="bar"}`)
|
||||
f(`{:f:oo=~"bar.+"}`)
|
||||
f(`foo {bar != "baz"}`)
|
||||
f(` { bar !~ "^ddd(x+)$", a="ss", __name__="sffd"} `)
|
||||
f(` { bar !~ "^ddd(x+)$", a="ss", "foo"} `)
|
||||
f(` foo { bar !~ "^ddd(x+)$", a="ss", __name__="sffd"} `)
|
||||
f(`(foo)`)
|
||||
f(`\п\р\и\в\е\т{\ы="111"}`)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"files": {
|
||||
"main.css": "./static/css/main.7fa18e1b.css",
|
||||
"main.js": "./static/js/main.ba08300f.js",
|
||||
"main.css": "./static/css/main.63479b72.css",
|
||||
"main.js": "./static/js/main.256ee243.js",
|
||||
"static/js/685.f772060c.chunk.js": "./static/js/685.f772060c.chunk.js",
|
||||
"static/media/MetricsQL.md": "./static/media/MetricsQL.a00044c91d9781cf8557.md",
|
||||
"index.html": "./index.html"
|
||||
},
|
||||
"entrypoints": [
|
||||
"static/css/main.7fa18e1b.css",
|
||||
"static/js/main.ba08300f.js"
|
||||
"static/css/main.63479b72.css",
|
||||
"static/js/main.256ee243.js"
|
||||
]
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="./favicon.svg"/><link rel="apple-touch-icon" href="./favicon.svg"/><link rel="mask-icon" href="./favicon.svg" color="#000000"><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=5"/><meta name="theme-color" content="#000000"/><meta name="description" content="Explore and troubleshoot your VictoriaMetrics data"/><link rel="manifest" href="./manifest.json"/><title>vmui</title><script src="./dashboards/index.js" type="module"></script><meta name="twitter:card" content="summary"><meta name="twitter:title" content="UI for VictoriaMetrics"><meta name="twitter:site" content="@https://victoriametrics.com/"><meta name="twitter:description" content="Explore and troubleshoot your VictoriaMetrics data"><meta name="twitter:image" content="./preview.jpg"><meta property="og:type" content="website"><meta property="og:title" content="UI for VictoriaMetrics"><meta property="og:url" content="https://victoriametrics.com/"><meta property="og:description" content="Explore and troubleshoot your VictoriaMetrics data"><script defer="defer" src="./static/js/main.ba08300f.js"></script><link href="./static/css/main.7fa18e1b.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="./favicon.svg"/><link rel="apple-touch-icon" href="./favicon.svg"/><link rel="mask-icon" href="./favicon.svg" color="#000000"><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=5"/><meta name="theme-color" content="#000000"/><meta name="description" content="Explore and troubleshoot your VictoriaMetrics data"/><link rel="manifest" href="./manifest.json"/><title>vmui</title><script src="./dashboards/index.js" type="module"></script><meta name="twitter:card" content="summary"><meta name="twitter:title" content="UI for VictoriaMetrics"><meta name="twitter:site" content="@https://victoriametrics.com/"><meta name="twitter:description" content="Explore and troubleshoot your VictoriaMetrics data"><meta name="twitter:image" content="./preview.jpg"><meta property="og:type" content="website"><meta property="og:title" content="UI for VictoriaMetrics"><meta property="og:url" content="https://victoriametrics.com/"><meta property="og:description" content="Explore and troubleshoot your VictoriaMetrics data"><script defer="defer" src="./static/js/main.256ee243.js"></script><link href="./static/css/main.63479b72.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
||||
1
app/vmselect/vmui/static/css/main.63479b72.css
Normal file
1
app/vmselect/vmui/static/css/main.63479b72.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
app/vmselect/vmui/static/js/main.256ee243.js
Normal file
2
app/vmselect/vmui/static/js/main.256ee243.js
Normal file
File diff suppressed because one or more lines are too long
38
app/vmselect/vmui/static/js/main.256ee243.js.LICENSE.txt
Normal file
38
app/vmselect/vmui/static/js/main.256ee243.js.LICENSE.txt
Normal file
@@ -0,0 +1,38 @@
|
||||
/*!
|
||||
Copyright (c) 2018 Jed Watson.
|
||||
Licensed under the MIT License (MIT), see
|
||||
http://jedwatson.github.io/classnames
|
||||
*/
|
||||
|
||||
/**
|
||||
* @remix-run/router v1.19.2
|
||||
*
|
||||
* Copyright (c) Remix Software Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE.md file in the root directory of this source tree.
|
||||
*
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
/**
|
||||
* React Router DOM v6.26.2
|
||||
*
|
||||
* Copyright (c) Remix Software Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE.md file in the root directory of this source tree.
|
||||
*
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
/**
|
||||
* React Router v6.26.2
|
||||
*
|
||||
* Copyright (c) Remix Software Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE.md file in the root directory of this source tree.
|
||||
*
|
||||
* @license MIT
|
||||
*/
|
||||
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.23.6 AS build-web-stage
|
||||
FROM golang:1.23.4 AS build-web-stage
|
||||
COPY build /build
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
@@ -1 +1 @@
|
||||
VITE_APP_TYPE=victoriametrics
|
||||
FAST_REFRESH=false
|
||||
@@ -1 +0,0 @@
|
||||
VITE_APP_TYPE=victorialogs
|
||||
@@ -1 +0,0 @@
|
||||
VITE_APP_TYPE=vmanomaly
|
||||
48
app/vmui/packages/vmui/.eslintrc.js
Normal file
48
app/vmui/packages/vmui/.eslintrc.js
Normal file
@@ -0,0 +1,48 @@
|
||||
// eslint-disable-next-line no-undef
|
||||
module.exports = {
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:react/recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaFeatures": { "jsx": true },
|
||||
"ecmaVersion": 12,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": [
|
||||
"react",
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"rules": {
|
||||
"@typescript-eslint/no-unused-vars": ["warn", { "varsIgnorePattern": "^_" }],
|
||||
"react/jsx-closing-bracket-location": [1, "line-aligned"],
|
||||
"react/jsx-max-props-per-line":[1, { "maximum": 1 }],
|
||||
"react/jsx-first-prop-new-line": [1, "multiline"],
|
||||
"object-curly-spacing": [2, "always"],
|
||||
"indent": ["error", 2, { "SwitchCase": 1 }],
|
||||
"linebreak-style": ["error", "unix"],
|
||||
"quotes": ["error", "double"],
|
||||
"semi": ["error", "always"],
|
||||
"react/prop-types": 0
|
||||
},
|
||||
"settings": {
|
||||
"react": {
|
||||
"pragma": "React", // Pragma to use, default to "React"
|
||||
"version": "detect"
|
||||
},
|
||||
"linkComponents": [
|
||||
// Components used as alternatives to <a> for linking, eg. <Link to={ url } />
|
||||
"Hyperlink",
|
||||
{
|
||||
"name": "Link", "linkAttribute": "to"
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
42
app/vmui/packages/vmui/config-overrides.js
Normal file
42
app/vmui/packages/vmui/config-overrides.js
Normal file
@@ -0,0 +1,42 @@
|
||||
/* eslint-disable */
|
||||
const { override, addExternalBabelPlugin, addWebpackAlias, addWebpackPlugin } = require("customize-cra");
|
||||
const webpack = require("webpack");
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// This will replace the default check
|
||||
const pathIndexHTML = (() => {
|
||||
switch (process.env.REACT_APP_TYPE) {
|
||||
case 'logs':
|
||||
return 'src/html/victorialogs.html';
|
||||
case 'anomaly':
|
||||
return 'src/html/vmanomaly.html';
|
||||
default:
|
||||
return 'src/html/victoriametrics.html';
|
||||
}
|
||||
})();
|
||||
const fileContent = fs.readFileSync(path.resolve(__dirname, pathIndexHTML), 'utf8');
|
||||
fs.writeFileSync(path.resolve(__dirname, 'public/index.html'), fileContent);
|
||||
|
||||
module.exports = override(
|
||||
addExternalBabelPlugin("@babel/plugin-proposal-nullish-coalescing-operator"),
|
||||
addWebpackAlias({
|
||||
"react": "preact/compat",
|
||||
"react-dom/test-utils": "preact/test-utils",
|
||||
"react-dom": "preact/compat", // Must be below test-utils
|
||||
"react/jsx-runtime": "preact/jsx-runtime"
|
||||
}),
|
||||
addWebpackPlugin(
|
||||
new webpack.NormalModuleReplacementPlugin(
|
||||
/\.\/App/,
|
||||
function (resource) {
|
||||
if (process.env.REACT_APP_TYPE === "logs") {
|
||||
resource.request = "./AppLogs";
|
||||
}
|
||||
if (process.env.REACT_APP_TYPE === "anomaly") {
|
||||
resource.request = "./AppAnomaly";
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
@@ -1,23 +0,0 @@
|
||||
import { readFile } from "fs/promises";
|
||||
import { IndexHtmlTransform } from "vite";
|
||||
|
||||
/**
|
||||
* Vite plugin to dynamically load index.html based on the current mode.
|
||||
* If a specific mode-based index file (e.g., index.victorialogs.html) exists, it is used.
|
||||
* Otherwise, the default index.html is loaded.
|
||||
*/
|
||||
export default function dynamicIndexHtmlPlugin({ mode }) {
|
||||
return {
|
||||
name: "vm-dynamic-index-html",
|
||||
transformIndexHtml: {
|
||||
order: "pre",
|
||||
handler: async () => {
|
||||
try {
|
||||
return await readFile(`./index.${mode}.html`, "utf8");
|
||||
} catch (error) {
|
||||
return await readFile("./index.html", "utf8");
|
||||
}
|
||||
}
|
||||
} as IndexHtmlTransform
|
||||
};
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
import react from "eslint-plugin-react";
|
||||
import typescriptEslint from "@typescript-eslint/eslint-plugin";
|
||||
import globals from "globals";
|
||||
import tsParser from "@typescript-eslint/parser";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import js from "@eslint/js";
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
allConfig: js.configs.all
|
||||
});
|
||||
|
||||
export default [...compat.extends(
|
||||
"eslint:recommended",
|
||||
"plugin:react/recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
), {
|
||||
plugins: {
|
||||
react,
|
||||
"@typescript-eslint": typescriptEslint,
|
||||
},
|
||||
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
},
|
||||
|
||||
parser: tsParser,
|
||||
ecmaVersion: 12,
|
||||
sourceType: "module",
|
||||
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
settings: {
|
||||
react: {
|
||||
pragma: "React",
|
||||
version: "detect",
|
||||
},
|
||||
|
||||
linkComponents: ["Hyperlink", {
|
||||
name: "Link",
|
||||
linkAttribute: "to",
|
||||
}],
|
||||
},
|
||||
|
||||
rules: {
|
||||
"@typescript-eslint/no-unused-expressions": ["error", {
|
||||
allowShortCircuit: true,
|
||||
allowTernary: true
|
||||
}],
|
||||
|
||||
"@typescript-eslint/no-unused-vars": ["warn", {
|
||||
"argsIgnorePattern": "^_",
|
||||
"caughtErrors": "none",
|
||||
"caughtErrorsIgnorePattern": "^_",
|
||||
"destructuredArrayIgnorePattern": "^_",
|
||||
"varsIgnorePattern": "^_",
|
||||
"ignoreRestSiblings": true
|
||||
}],
|
||||
|
||||
"react/jsx-closing-bracket-location": [1, "line-aligned"],
|
||||
|
||||
"react/jsx-max-props-per-line": [1, {
|
||||
maximum: 1,
|
||||
}],
|
||||
|
||||
"react/jsx-first-prop-new-line": [1, "multiline"],
|
||||
"object-curly-spacing": [2, "always"],
|
||||
|
||||
indent: ["error", 2, {
|
||||
SwitchCase: 1,
|
||||
}],
|
||||
|
||||
"linebreak-style": ["error", "unix"],
|
||||
quotes: ["error", "double"],
|
||||
semi: ["error", "always"],
|
||||
"react/prop-types": 0,
|
||||
|
||||
},
|
||||
}];
|
||||
20119
app/vmui/packages/vmui/package-lock.json
generated
20119
app/vmui/packages/vmui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -3,40 +3,50 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"homepage": "./",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@types/lodash.debounce": "^4.0.9",
|
||||
"@types/lodash.get": "^4.4.9",
|
||||
"@types/qs": "^6.9.18",
|
||||
"@types/react": "^19.0.8",
|
||||
"@types/react-input-mask": "^3.0.6",
|
||||
"@types/lodash.throttle": "^4.1.9",
|
||||
"@types/node": "^22.5.4",
|
||||
"@types/qs": "^6.9.15",
|
||||
"@types/react-input-mask": "^3.0.5",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"@types/webpack-env": "^1.18.5",
|
||||
"classnames": "^2.5.1",
|
||||
"dayjs": "^1.11.13",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lodash.get": "^4.4.2",
|
||||
"marked": "^15.0.6",
|
||||
"marked-emoji": "^1.4.3",
|
||||
"preact": "^10.25.4",
|
||||
"qs": "^6.14.0",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"marked": "^14.1.2",
|
||||
"marked-emoji": "^1.4.2",
|
||||
"preact": "^10.23.2",
|
||||
"qs": "^6.13.0",
|
||||
"react-input-mask": "^2.0.4",
|
||||
"react-router-dom": "^7.1.5",
|
||||
"uplot": "^1.6.31",
|
||||
"vite": "^6.0.11",
|
||||
"web-vitals": "^4.2.4"
|
||||
"react-router-dom": "^6.26.2",
|
||||
"sass": "^1.78.0",
|
||||
"source-map-explorer": "^2.5.3",
|
||||
"typescript": "~4.6.2",
|
||||
"uplot": "^1.6.30",
|
||||
"web-vitals": "^4.2.3"
|
||||
},
|
||||
"scripts": {
|
||||
"prestart": "npm run copy-metricsql-docs",
|
||||
"start": "vite",
|
||||
"start:logs": "vite --mode victorialogs",
|
||||
"start:anomaly": "vite --mode vmanomaly",
|
||||
"build": "vite build",
|
||||
"build:logs": "vite build --mode victorialogs",
|
||||
"build:anomaly": "vite build --mode vmanomaly",
|
||||
"lint": "eslint 'src/**/*.{ts,tsx}'",
|
||||
"lint:fix": "eslint 'src/**/*.{ts,tsx}' --fix",
|
||||
"copy-metricsql-docs": "cp ../../../../docs/MetricsQL.md src/assets/MetricsQL.md || true",
|
||||
"preview": "vite preview"
|
||||
"start": "react-app-rewired start",
|
||||
"start:logs": "cross-env REACT_APP_TYPE=logs npm run start",
|
||||
"start:anomaly": "cross-env REACT_APP_TYPE=anomaly npm run start",
|
||||
"build": "GENERATE_SOURCEMAP=false react-app-rewired build",
|
||||
"build:logs": "cross-env REACT_APP_TYPE=logs npm run build",
|
||||
"build:anomaly": "cross-env REACT_APP_TYPE=anomaly npm run build",
|
||||
"lint": "eslint src --ext tsx,ts",
|
||||
"lint:fix": "eslint src --ext tsx,ts --fix",
|
||||
"analyze": "source-map-explorer 'build/static/js/*.js'",
|
||||
"copy-metricsql-docs": "cp ../../../../docs/MetricsQL.md src/assets/MetricsQL.md || true"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
@@ -51,24 +61,26 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.16.7",
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
"@eslint/js": "^9.19.0",
|
||||
"@preact/preset-vite": "^2.10.1",
|
||||
"@types/node": "^22.13.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.22.0",
|
||||
"@typescript-eslint/parser": "^8.22.0",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.15.0",
|
||||
"@typescript-eslint/parser": "^5.15.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^9.19.0",
|
||||
"eslint-plugin-react": "^7.37.4",
|
||||
"globals": "^15.14.0",
|
||||
"http-proxy-middleware": "^3.0.3",
|
||||
"postcss": "^8.5.1",
|
||||
"rollup-plugin-visualizer": "^5.14.0",
|
||||
"sass": "^1.83.4",
|
||||
"sass-embedded": "^1.83.4",
|
||||
"typescript": "^5.7.3",
|
||||
"webpack": "^5.97.1"
|
||||
"customize-cra": "^1.0.0",
|
||||
"eslint": "^8.44.0",
|
||||
"eslint-config-react-app": "^7.0.1",
|
||||
"eslint-plugin-react": "^7.36.1",
|
||||
"http-proxy-middleware": "^3.0.2",
|
||||
"react-app-rewired": "^2.2.1",
|
||||
"webpack": "^5.94.0"
|
||||
},
|
||||
"overrides": {
|
||||
"react-app-rewired": {
|
||||
"nth-check": "^2.0.1"
|
||||
},
|
||||
"css-select": {
|
||||
"nth-check": "^2.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,4 +38,4 @@ const AppAnomaly: FC = () => {
|
||||
</>;
|
||||
};
|
||||
|
||||
export default AppAnomaly;
|
||||
export default AppAnomaly;
|
||||
@@ -1,6 +1,3 @@
|
||||
import uPlot from "uplot";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
export interface MetricBase {
|
||||
group: number;
|
||||
metric: {
|
||||
@@ -9,13 +6,13 @@ export interface MetricBase {
|
||||
}
|
||||
|
||||
export interface MetricResult extends MetricBase {
|
||||
values: [number, string][];
|
||||
values: [number, string][]
|
||||
}
|
||||
|
||||
|
||||
export interface InstantMetricResult extends MetricBase {
|
||||
value?: [number, string];
|
||||
values?: [number, string][];
|
||||
value?: [number, string]
|
||||
values?: [number, string][]
|
||||
}
|
||||
|
||||
export interface ExportMetricResult extends MetricBase {
|
||||
@@ -46,24 +43,10 @@ export interface Logs {
|
||||
export interface LogHits {
|
||||
timestamps: string[];
|
||||
values: number[];
|
||||
total: number;
|
||||
fields: { [key: string]: string; };
|
||||
_isOther: boolean;
|
||||
}
|
||||
|
||||
export interface LegendLogHits {
|
||||
label: string;
|
||||
total: number;
|
||||
totalHits: number;
|
||||
isOther: boolean;
|
||||
fields: { [key: string]: string; };
|
||||
stroke?: uPlot.Series.Stroke;
|
||||
}
|
||||
|
||||
export interface LegendLogHitsMenu {
|
||||
title: string;
|
||||
icon?: ReactNode;
|
||||
handler?: () => void;
|
||||
total?: number;
|
||||
fields: {
|
||||
[key: string]: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ReportMetaData {
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
import React, { FC, useCallback, useMemo, useRef, useState } from "preact/compat";
|
||||
import React, { FC, useMemo, useRef, useState } from "preact/compat";
|
||||
import "./style.scss";
|
||||
import "uplot/dist/uPlot.min.css";
|
||||
import useElementSize from "../../../hooks/useElementSize";
|
||||
import uPlot, { AlignedData } from "uplot";
|
||||
import { useEffect } from "react";
|
||||
import useBarHitsOptions, { getLabelFromLogHit } from "./hooks/useBarHitsOptions";
|
||||
import useBarHitsOptions from "./hooks/useBarHitsOptions";
|
||||
import BarHitsTooltip from "./BarHitsTooltip/BarHitsTooltip";
|
||||
import { TimeParams } from "../../../types";
|
||||
import usePlotScale from "../../../hooks/uplot/usePlotScale";
|
||||
import useReadyChart from "../../../hooks/uplot/useReadyChart";
|
||||
import useZoomChart from "../../../hooks/uplot/useZoomChart";
|
||||
import classNames from "classnames";
|
||||
import { LegendLogHits, LogHits } from "../../../api/types";
|
||||
import { LogHits } from "../../../api/types";
|
||||
import { addSeries, delSeries, setBand } from "../../../utils/uplot";
|
||||
import { GraphOptions, GRAPH_STYLES } from "./types";
|
||||
import BarHitsOptions from "./BarHitsOptions/BarHitsOptions";
|
||||
import stack from "../../../utils/uplot/stack";
|
||||
import BarHitsLegend from "./BarHitsLegend/BarHitsLegend";
|
||||
import { calculateTotalHits, sortLogHits } from "../../../utils/logs";
|
||||
|
||||
interface Props {
|
||||
logHits: LogHits[];
|
||||
@@ -58,29 +57,6 @@ const BarHitsChart: FC<Props> = ({ logHits, data: _data, period, setPeriod, onAp
|
||||
graphOptions
|
||||
});
|
||||
|
||||
const prepareLegend = useCallback((hits: LogHits[], totalHits: number): LegendLogHits[] => {
|
||||
return hits.map((hit) => {
|
||||
const label = getLabelFromLogHit(hit);
|
||||
|
||||
const legendItem: LegendLogHits = {
|
||||
label,
|
||||
isOther: hit._isOther,
|
||||
fields: hit.fields,
|
||||
total: hit.total || 0,
|
||||
totalHits,
|
||||
stroke: series.find((s) => s.label === label)?.stroke,
|
||||
};
|
||||
|
||||
return legendItem;
|
||||
}).sort(sortLogHits("total"));
|
||||
}, [series]);
|
||||
|
||||
|
||||
const legendDetails: LegendLogHits[] = useMemo(() => {
|
||||
const totalHits = calculateTotalHits(logHits);
|
||||
return prepareLegend(logHits, totalHits);
|
||||
}, [logHits, prepareLegend]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!uPlotInst) return;
|
||||
delSeries(uPlotInst);
|
||||
@@ -145,7 +121,6 @@ const BarHitsChart: FC<Props> = ({ logHits, data: _data, period, setPeriod, onAp
|
||||
<BarHitsLegend
|
||||
uPlotInst={uPlotInst}
|
||||
onApplyFilter={onApplyFilter}
|
||||
legendDetails={legendDetails}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,53 +1,83 @@
|
||||
import React, { FC, useEffect, useState } from "preact/compat";
|
||||
import React, { FC, useCallback, useEffect, useState } from "preact/compat";
|
||||
import uPlot, { Series } from "uplot";
|
||||
import "./style.scss";
|
||||
import "../../Line/Legend/style.scss";
|
||||
import BarHitsLegendItem from "./BarHitsLegendItem";
|
||||
import { LegendLogHits } from "../../../../api/types";
|
||||
import classNames from "classnames";
|
||||
import { MouseEvent } from "react";
|
||||
import { isMacOs } from "../../../../utils/detect-device";
|
||||
import Tooltip from "../../../Main/Tooltip/Tooltip";
|
||||
import { getStreamPairs } from "../../../../utils/logs";
|
||||
|
||||
interface Props {
|
||||
uPlotInst: uPlot;
|
||||
legendDetails: LegendLogHits[];
|
||||
onApplyFilter: (value: string) => void;
|
||||
}
|
||||
|
||||
const BarHitsLegend: FC<Props> = ({ uPlotInst, legendDetails, onApplyFilter }) => {
|
||||
const BarHitsLegend: FC<Props> = ({ uPlotInst, onApplyFilter }) => {
|
||||
const [series, setSeries] = useState<Series[]>([]);
|
||||
const totalHits = legendDetails[0]?.totalHits || 0;
|
||||
const [pairs, setPairs] = useState<string[][]>([]);
|
||||
|
||||
const getSeries = () => {
|
||||
return uPlotInst.series.filter(s => s.scale !== "x");
|
||||
};
|
||||
|
||||
const handleRedrawGraph = () => {
|
||||
uPlotInst.redraw();
|
||||
setSeries(getSeries());
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setSeries(getSeries());
|
||||
const updateSeries = useCallback(() => {
|
||||
const series = uPlotInst.series.filter(s => s.scale !== "x");
|
||||
setSeries(series);
|
||||
setPairs(series.map(s => getStreamPairs(s.label || "")));
|
||||
}, [uPlotInst]);
|
||||
|
||||
const handleClickByValue = (value: string) => (e: MouseEvent<HTMLDivElement>) => {
|
||||
const metaKey = e.metaKey || e.ctrlKey;
|
||||
if (!metaKey) return;
|
||||
onApplyFilter(`{${value}}` || "");
|
||||
updateSeries();
|
||||
uPlotInst.redraw();
|
||||
};
|
||||
|
||||
const handleClickByStream = (target: Series) => (e: MouseEvent<HTMLDivElement>) => {
|
||||
const metaKey = e.metaKey || e.ctrlKey;
|
||||
if (metaKey) return;
|
||||
target.show = !target.show;
|
||||
updateSeries();
|
||||
uPlotInst.redraw();
|
||||
};
|
||||
|
||||
useEffect(updateSeries, [uPlotInst]);
|
||||
|
||||
return (
|
||||
<div className="vm-bar-hits-legend">
|
||||
{legendDetails.map((legend) => (
|
||||
<BarHitsLegendItem
|
||||
key={legend.label}
|
||||
legend={legend}
|
||||
series={series}
|
||||
onRedrawGraph={handleRedrawGraph}
|
||||
onApplyFilter={onApplyFilter}
|
||||
/>
|
||||
{series.map((s, i) => (
|
||||
<Tooltip
|
||||
key={s.label}
|
||||
title={(
|
||||
<ul className="vm-bar-hits-legend-info">
|
||||
<li>Click to {s.show ? "hide" : "show"} the _stream.</li>
|
||||
<li>{isMacOs() ? "Cmd" : "Ctrl"} + Click to filter by the _stream.</li>
|
||||
</ul>
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-bar-hits-legend-item": true,
|
||||
"vm-bar-hits-legend-item_hide": !s.show,
|
||||
})}
|
||||
onClick={handleClickByStream(s)}
|
||||
>
|
||||
<div
|
||||
className="vm-bar-hits-legend-item__marker"
|
||||
style={{ backgroundColor: `${(s?.stroke as () => string)?.()}` }}
|
||||
/>
|
||||
<div className="vm-bar-hits-legend-item-pairs">
|
||||
{pairs[i].map(value => (
|
||||
<span
|
||||
className="vm-bar-hits-legend-item-pairs__value"
|
||||
key={value}
|
||||
onClick={handleClickByValue(value)}
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
))}
|
||||
<div className="vm-bar-hits-legend-info">
|
||||
<div>
|
||||
Total hits: <b>{totalHits.toLocaleString("en-US")}</b>
|
||||
</div>
|
||||
<div>
|
||||
<code>L-Click</code> toggles visibility.
|
||||
<code>R-Click</code> opens menu.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
import React, { FC, useMemo, useRef, useState } from "preact/compat";
|
||||
import classNames from "classnames";
|
||||
import { Series } from "uplot";
|
||||
import { MouseEvent } from "react";
|
||||
import { LegendLogHits } from "../../../../api/types";
|
||||
import { getStreamPairs } from "../../../../utils/logs";
|
||||
import { formatNumberShort } from "../../../../utils/math";
|
||||
import Popper from "../../../Main/Popper/Popper";
|
||||
import useBoolean from "../../../../hooks/useBoolean";
|
||||
import LegendHitsMenu from "../LegendHitsMenu/LegendHitsMenu";
|
||||
|
||||
interface Props {
|
||||
legend: LegendLogHits;
|
||||
series: Series[];
|
||||
onRedrawGraph: () => void;
|
||||
onApplyFilter: (value: string) => void;
|
||||
}
|
||||
|
||||
const BarHitsLegendItem: FC<Props> = ({ legend, series, onRedrawGraph, onApplyFilter }) => {
|
||||
const {
|
||||
value: openContextMenu,
|
||||
setTrue: handleOpenContextMenu,
|
||||
setFalse: handleCloseContextMenu,
|
||||
} = useBoolean(false);
|
||||
|
||||
const legendRef = useRef<HTMLDivElement>(null);
|
||||
const [clickPosition, setClickPosition] = useState<{ top: number; left: number } | null>(null);
|
||||
|
||||
const targetSeries = useMemo(() => series.find(s => s.label === legend.label), [series]);
|
||||
|
||||
const fields = useMemo(() => getStreamPairs(legend.label), [legend.label]);
|
||||
|
||||
const label = fields.join(", ");
|
||||
const totalShortFormatted = formatNumberShort(legend.total);
|
||||
|
||||
const handleClickByStream = (e: MouseEvent<HTMLDivElement>) => {
|
||||
if (!targetSeries) return;
|
||||
|
||||
if (e.metaKey || e.ctrlKey) {
|
||||
targetSeries.show = !targetSeries.show;
|
||||
} else {
|
||||
const isOnlyTargetVisible = series.every(s => s === targetSeries || !s.show);
|
||||
series.forEach(s => {
|
||||
s.show = isOnlyTargetVisible || (s === targetSeries);
|
||||
});
|
||||
}
|
||||
|
||||
onRedrawGraph();
|
||||
};
|
||||
|
||||
const handleContextMenu = (e: MouseEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
setClickPosition({ top: e.clientY, left: e.clientX });
|
||||
handleOpenContextMenu();
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={legendRef}
|
||||
className={classNames({
|
||||
"vm-bar-hits-legend-item": true,
|
||||
"vm-bar-hits-legend-item_other": legend.isOther,
|
||||
"vm-bar-hits-legend-item_hide": !targetSeries?.show,
|
||||
})}
|
||||
onClick={handleClickByStream}
|
||||
onContextMenu={handleContextMenu}
|
||||
>
|
||||
<div
|
||||
className="vm-bar-hits-legend-item__marker"
|
||||
style={{ backgroundColor: `${legend.stroke}` }}
|
||||
/>
|
||||
<div className="vm-bar-hits-legend-item__label">{label}</div>
|
||||
<span className="vm-bar-hits-legend-item__total">({totalShortFormatted})</span>
|
||||
<Popper
|
||||
placement="fixed"
|
||||
open={openContextMenu}
|
||||
buttonRef={legendRef}
|
||||
placementPosition={clickPosition}
|
||||
onClose={handleCloseContextMenu}
|
||||
>
|
||||
<LegendHitsMenu
|
||||
legend={legend}
|
||||
fields={fields}
|
||||
onApplyFilter={onApplyFilter}
|
||||
onClose={handleCloseContextMenu}
|
||||
/>
|
||||
</Popper>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BarHitsLegendItem;
|
||||
@@ -3,16 +3,16 @@
|
||||
.vm-bar-hits-legend {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: $padding-small;
|
||||
padding: 0 $padding-small $padding-small;
|
||||
color: $color-text;
|
||||
|
||||
&-item {
|
||||
max-width: 50%;
|
||||
display: flex;
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
align-items: center;
|
||||
gap: $padding-small;
|
||||
font-size: $font-size-small;
|
||||
padding: $padding-small $padding-global;
|
||||
font-size: 12px;
|
||||
padding: 0 $padding-small;
|
||||
border-radius: $border-radius-small;
|
||||
cursor: pointer;
|
||||
transition: 0.2s;
|
||||
@@ -27,44 +27,34 @@
|
||||
}
|
||||
|
||||
&__marker {
|
||||
min-width: 14px;
|
||||
max-width: 14px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border: $color-background-block;
|
||||
}
|
||||
|
||||
&__label {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
&-pairs {
|
||||
display: flex;
|
||||
gap: $padding-small;
|
||||
|
||||
&__total {
|
||||
color: $color-text-secondary;
|
||||
font-style: italic;
|
||||
grid-column: 2;
|
||||
&__value {
|
||||
padding: $padding-small 0;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
&:after {
|
||||
content: ",";
|
||||
}
|
||||
|
||||
&:last-child:after {
|
||||
content: "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-info {
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-top: $padding-small;
|
||||
color: $color-text-secondary;
|
||||
font-size: $font-size-small;
|
||||
|
||||
code {
|
||||
display: inline-block;
|
||||
padding: calc($padding-small / 2) $padding-small;
|
||||
font-size: $font-size-small;
|
||||
text-align: center;
|
||||
background-color: $color-background-body;
|
||||
background-repeat: repeat-x;
|
||||
border: $border-divider;
|
||||
border-radius: 4px;
|
||||
}
|
||||
list-style-position: inside;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import "./style.scss";
|
||||
import useStateSearchParams from "../../../../hooks/useStateSearchParams";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import Button from "../../../Main/Button/Button";
|
||||
import classNames from "classnames";
|
||||
import { SettingsIcon, VisibilityIcon, VisibilityOffIcon } from "../../../Main/Icons";
|
||||
import Tooltip from "../../../Main/Tooltip/Tooltip";
|
||||
import Popper from "../../../Main/Popper/Popper";
|
||||
@@ -23,20 +24,27 @@ const BarHitsOptions: FC<Props> = ({ onChange }) => {
|
||||
setFalse: handleCloseOptions,
|
||||
} = useBoolean(false);
|
||||
|
||||
const [graphStyle, setGraphStyle] = useStateSearchParams(GRAPH_STYLES.LINE_STEPPED, "graph");
|
||||
const [stacked, setStacked] = useStateSearchParams(false, "stacked");
|
||||
const [fill, setFill] = useStateSearchParams("true", "fill");
|
||||
const [fill, setFill] = useStateSearchParams(false, "fill");
|
||||
const [hideChart, setHideChart] = useStateSearchParams(false, "hide_chart");
|
||||
|
||||
const options: GraphOptions = useMemo(() => ({
|
||||
graphStyle: GRAPH_STYLES.BAR,
|
||||
graphStyle,
|
||||
stacked,
|
||||
fill: fill === "true",
|
||||
fill,
|
||||
hideChart,
|
||||
}), [stacked, fill, hideChart]);
|
||||
}), [graphStyle, stacked, fill, hideChart]);
|
||||
|
||||
const handleChangeGraphStyle = (val: string) => () => {
|
||||
setGraphStyle(val as GRAPH_STYLES);
|
||||
searchParams.set("graph", val);
|
||||
setSearchParams(searchParams);
|
||||
};
|
||||
|
||||
const handleChangeFill = (val: boolean) => {
|
||||
setFill(`${val}`);
|
||||
searchParams.set("fill", `${val}`);
|
||||
setFill(val);
|
||||
val ? searchParams.set("fill", "true") : searchParams.delete("fill");
|
||||
setSearchParams(searchParams);
|
||||
};
|
||||
|
||||
@@ -89,6 +97,21 @@ const BarHitsOptions: FC<Props> = ({ onChange }) => {
|
||||
title={"Graph settings"}
|
||||
>
|
||||
<div className="vm-bar-hits-options-settings">
|
||||
<div className="vm-bar-hits-options-settings-item vm-bar-hits-options-settings-item_list">
|
||||
<p className="vm-bar-hits-options-settings-item__title">Graph style:</p>
|
||||
{Object.values(GRAPH_STYLES).map(style => (
|
||||
<div
|
||||
key={style}
|
||||
className={classNames({
|
||||
"vm-list-item": true,
|
||||
"vm-list-item_active": graphStyle === style,
|
||||
})}
|
||||
onClick={handleChangeGraphStyle(style)}
|
||||
>
|
||||
{style}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="vm-bar-hits-options-settings-item">
|
||||
<Switch
|
||||
label={"Stacked"}
|
||||
@@ -99,7 +122,7 @@ const BarHitsOptions: FC<Props> = ({ onChange }) => {
|
||||
<div className="vm-bar-hits-options-settings-item">
|
||||
<Switch
|
||||
label={"Fill"}
|
||||
value={fill === "true"}
|
||||
value={fill}
|
||||
onChange={handleChangeFill}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -11,12 +11,12 @@
|
||||
&-settings {
|
||||
display: grid;
|
||||
align-items: flex-start;
|
||||
min-width: 200px;
|
||||
gap: $padding-global;
|
||||
padding-bottom: $padding-global;
|
||||
min-width: 200px;
|
||||
|
||||
&-item {
|
||||
padding: 0 $padding-global;
|
||||
border-bottom: $border-divider;
|
||||
padding: 0 $padding-global $padding-global;
|
||||
|
||||
&_list {
|
||||
padding: 0;
|
||||
|
||||
@@ -5,7 +5,6 @@ import { DATE_TIME_FORMAT } from "../../../../constants/date";
|
||||
import classNames from "classnames";
|
||||
import "./style.scss";
|
||||
import "../../ChartTooltip/style.scss";
|
||||
import { sortLogHits } from "../../../../utils/logs";
|
||||
|
||||
interface Props {
|
||||
data: AlignedData;
|
||||
@@ -27,7 +26,7 @@ const BarHitsTooltip: FC<Props> = ({ data, focusDataIdx, uPlotInst }) => {
|
||||
const tooltipItems = values.map((value, i) => {
|
||||
const targetSeries = series[i + 1];
|
||||
const stroke = (targetSeries?.stroke as () => string)?.();
|
||||
const label = targetSeries?.label;
|
||||
const label = targetSeries?.label || "other";
|
||||
const show = targetSeries?.show;
|
||||
return {
|
||||
label,
|
||||
@@ -35,7 +34,7 @@ const BarHitsTooltip: FC<Props> = ({ data, focusDataIdx, uPlotInst }) => {
|
||||
value,
|
||||
show
|
||||
};
|
||||
}).filter(item => item.value > 0 && item.show).sort(sortLogHits("value"));
|
||||
}).filter(item => item.value > 0 && item.show).sort((a, b) => b.value - a.value);
|
||||
|
||||
const point = {
|
||||
top: tooltipItems[0] ? uPlotInst?.valToPos?.(tooltipItems[0].value, "y") || 0 : 0,
|
||||
@@ -105,24 +104,21 @@ const BarHitsTooltip: FC<Props> = ({ data, focusDataIdx, uPlotInst }) => {
|
||||
className="vm-chart-tooltip-data__marker"
|
||||
style={{ background: item.stroke }}
|
||||
/>
|
||||
<p className="vm-bar-hits-tooltip-item">
|
||||
<span className="vm-bar-hits-tooltip-item__label">{item.label}</span>
|
||||
<span>{item.value.toLocaleString("en-US")}</span>
|
||||
<p>
|
||||
{item.label}: <b>{item.value}</b>
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{tooltipData.values.length > 1 && (
|
||||
<div className="vm-chart-tooltip-data">
|
||||
<span/>
|
||||
<p className="vm-bar-hits-tooltip-item">
|
||||
<span className="vm-bar-hits-tooltip-item__label">Total</span>
|
||||
<span>{tooltipData.total.toLocaleString("en-US")}</span>
|
||||
<p>
|
||||
Total records: <b>{tooltipData.total}</b>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="vm-chart-tooltip-header">
|
||||
<div className="vm-chart-tooltip-header__title vm-bar-hits-tooltip__date">
|
||||
<div className="vm-chart-tooltip-header__title">
|
||||
{tooltipData.timestamp}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,23 +9,4 @@
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
&-item {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
align-items: center;
|
||||
gap: $padding-global;
|
||||
max-width: 100%;
|
||||
|
||||
&__label {
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
&__date {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
import React, { FC } from "preact/compat";
|
||||
import "./style.scss";
|
||||
import { LegendLogHits } from "../../../../api/types";
|
||||
import LegendHitsMenuStats from "./LegendHitsMenuStats";
|
||||
import LegendHitsMenuBase from "./LegendHitsMenuBase";
|
||||
import LegendHitsMenuRow from "./LegendHitsMenuRow";
|
||||
import LegendHitsMenuFields from "./LegendHitsMenuFields";
|
||||
import { LOGS_LIMIT_HITS } from "../../../../constants/logs";
|
||||
|
||||
const otherDescription = `aggregated results for fields not in the top ${LOGS_LIMIT_HITS}`;
|
||||
|
||||
interface Props {
|
||||
legend: LegendLogHits;
|
||||
fields: string[];
|
||||
onApplyFilter: (value: string) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const LegendHitsMenu: FC<Props> = ({ legend, fields, onApplyFilter, onClose }) => {
|
||||
return (
|
||||
<div className="vm-legend-hits-menu">
|
||||
<div className="vm-legend-hits-menu-section">
|
||||
<LegendHitsMenuRow
|
||||
className="vm-legend-hits-menu-row_info"
|
||||
title={legend.isOther ? otherDescription : legend.label}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!legend.isOther && (
|
||||
<LegendHitsMenuBase
|
||||
legend={legend}
|
||||
onApplyFilter={onApplyFilter}
|
||||
onClose={onClose}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!legend.isOther && (
|
||||
<LegendHitsMenuFields
|
||||
fields={fields}
|
||||
onApplyFilter={onApplyFilter}
|
||||
onClose={onClose}
|
||||
/>
|
||||
)}
|
||||
|
||||
<LegendHitsMenuStats legend={legend}/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LegendHitsMenu;
|
||||
@@ -1,64 +0,0 @@
|
||||
import React, { FC } from "preact/compat";
|
||||
import LegendHitsMenuRow from "./LegendHitsMenuRow";
|
||||
import useCopyToClipboard from "../../../../hooks/useCopyToClipboard";
|
||||
import { CopyIcon, FilterIcon, FilterOffIcon } from "../../../Main/Icons";
|
||||
import { LegendLogHits, LegendLogHitsMenu } from "../../../../api/types";
|
||||
import { LOGS_GROUP_BY } from "../../../../constants/logs";
|
||||
|
||||
interface Props {
|
||||
legend: LegendLogHits;
|
||||
onApplyFilter: (value: string) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const LegendHitsMenuBase: FC<Props> = ({ legend, onApplyFilter, onClose }) => {
|
||||
const copyToClipboard = useCopyToClipboard();
|
||||
|
||||
const handleAddStreamToFilter = () => {
|
||||
onApplyFilter(`${LOGS_GROUP_BY}: ${legend.label}`);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleExcludeStreamToFilter = () => {
|
||||
onApplyFilter(`(NOT ${LOGS_GROUP_BY}: ${legend.label})`);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handlerCopyLabel = async () => {
|
||||
await copyToClipboard(legend.label, `${legend.label} has been copied`);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const options: LegendLogHitsMenu[] = [
|
||||
{
|
||||
title: `Copy ${LOGS_GROUP_BY} name`,
|
||||
icon: <CopyIcon/>,
|
||||
handler: handlerCopyLabel,
|
||||
},
|
||||
{
|
||||
title: `Add ${LOGS_GROUP_BY} to filter`,
|
||||
icon: <FilterIcon/>,
|
||||
handler: handleAddStreamToFilter,
|
||||
},
|
||||
{
|
||||
title: `Exclude ${LOGS_GROUP_BY} to filter`,
|
||||
icon: <FilterOffIcon/>,
|
||||
handler: handleExcludeStreamToFilter,
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="vm-legend-hits-menu-section">
|
||||
{options.map(({ icon, title, handler }) => (
|
||||
<LegendHitsMenuRow
|
||||
key={title}
|
||||
iconStart={icon}
|
||||
title={title}
|
||||
handler={handler}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LegendHitsMenuBase;
|
||||
@@ -1,74 +0,0 @@
|
||||
import React, { FC, useMemo } from "preact/compat";
|
||||
import LegendHitsMenuRow from "./LegendHitsMenuRow";
|
||||
import { CopyIcon, FilterIcon, FilterOffIcon } from "../../../Main/Icons";
|
||||
import { convertToFieldFilter } from "../../../../utils/logs";
|
||||
import { LegendLogHitsMenu } from "../../../../api/types";
|
||||
import useCopyToClipboard from "../../../../hooks/useCopyToClipboard";
|
||||
|
||||
interface Props {
|
||||
fields: string[];
|
||||
onApplyFilter: (value: string) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const LegendHitsMenuFields: FC<Props> = ({ fields, onApplyFilter, onClose }) => {
|
||||
const copyToClipboard = useCopyToClipboard();
|
||||
|
||||
const handleCopy = (field: string) => async () => {
|
||||
await copyToClipboard(field, `${field} has been copied`);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleAddToFilter = (field: string) => () => {
|
||||
onApplyFilter(field);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleExcludeToFilter = (field: string) => () => {
|
||||
onApplyFilter(`-${field}`);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const generateFieldMenu = (field: string): LegendLogHitsMenu[] => {
|
||||
return [
|
||||
{
|
||||
title: "Copy",
|
||||
icon: <CopyIcon/>,
|
||||
handler: handleCopy(field),
|
||||
},
|
||||
{
|
||||
title: "Add to filter",
|
||||
icon: <FilterIcon/>,
|
||||
handler: handleAddToFilter(field),
|
||||
},
|
||||
{
|
||||
title: "Exclude to filter",
|
||||
icon: <FilterOffIcon/>,
|
||||
handler: handleExcludeToFilter(field),
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
const fieldsWithMenu: LegendLogHitsMenu[] = useMemo(() => {
|
||||
return fields.map(field => {
|
||||
const title = convertToFieldFilter(field);
|
||||
return {
|
||||
title,
|
||||
submenu: generateFieldMenu(title),
|
||||
};
|
||||
});
|
||||
}, [fields]);
|
||||
|
||||
return (
|
||||
<div className="vm-legend-hits-menu-section">
|
||||
{fieldsWithMenu?.map((field) => (
|
||||
<LegendHitsMenuRow
|
||||
key={field.title}
|
||||
{...field}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LegendHitsMenuFields;
|
||||
@@ -1,116 +0,0 @@
|
||||
import React, { FC, useRef, useState } from "preact/compat";
|
||||
import classNames from "classnames";
|
||||
import { ReactNode, useEffect } from "react";
|
||||
import Tooltip from "../../../Main/Tooltip/Tooltip";
|
||||
import { LegendLogHitsMenu } from "../../../../api/types";
|
||||
import { ArrowDropDownIcon } from "../../../Main/Icons";
|
||||
import useClickOutside from "../../../../hooks/useClickOutside";
|
||||
|
||||
interface Props {
|
||||
title: string | ReactNode;
|
||||
handler?: () => void;
|
||||
iconStart?: ReactNode;
|
||||
iconEnd?: ReactNode;
|
||||
className?: string;
|
||||
submenu?: LegendLogHitsMenu[];
|
||||
}
|
||||
|
||||
const LegendHitsMenuRow: FC<Props> = ({ title, handler, iconStart, iconEnd, className, submenu }) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const titleRef = useRef<HTMLDivElement>(null);
|
||||
const submenuRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [isOverflownTitle, setIsOverflownTitle] = useState(false);
|
||||
|
||||
const [openSubmenu, setOpenSubmenu] = useState(false);
|
||||
const [posSubmenuLeft, setPosSubmenuLeft] = useState(false);
|
||||
const hasSubmenu = !!submenu?.length;
|
||||
|
||||
const handleToggleContextMenu = () => {
|
||||
setOpenSubmenu(prev => !prev);
|
||||
};
|
||||
|
||||
const handleCloseContextMenu = () => {
|
||||
setOpenSubmenu(false);
|
||||
};
|
||||
|
||||
const handleClick = () => {
|
||||
handler && handler();
|
||||
hasSubmenu && handleToggleContextMenu();
|
||||
};
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (!titleRef.current) return;
|
||||
setIsOverflownTitle(titleRef.current.scrollWidth > titleRef.current.clientWidth);
|
||||
}, [title, titleRef]);
|
||||
|
||||
useEffect(() => {
|
||||
requestAnimationFrame(() => {
|
||||
if (!openSubmenu || !submenuRef.current) {
|
||||
setPosSubmenuLeft(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const { left, width } = submenuRef.current.getBoundingClientRect();
|
||||
setPosSubmenuLeft(left + width > window.innerWidth);
|
||||
});
|
||||
}, [submenuRef, openSubmenu]);
|
||||
|
||||
useClickOutside(containerRef, handleCloseContextMenu);
|
||||
|
||||
const titleContent = (
|
||||
<div
|
||||
ref={titleRef}
|
||||
className="vm-legend-hits-menu-row__title"
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={classNames({
|
||||
"vm-legend-hits-menu-row": true,
|
||||
"vm-legend-hits-menu-row_interactive": !!handler || hasSubmenu,
|
||||
[`${className}`]: className
|
||||
})}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{iconStart && <div className="vm-legend-hits-menu-row__icon">{iconStart}</div>}
|
||||
{isOverflownTitle ? (<Tooltip title={title}>{titleContent}</Tooltip>) : titleContent}
|
||||
{iconEnd && !hasSubmenu && <div className="vm-legend-hits-menu-row__icon">{iconEnd}</div>}
|
||||
|
||||
{hasSubmenu && (
|
||||
<div className="vm-legend-hits-menu-row__icon vm-legend-hits-menu-row__icon_drop">
|
||||
<ArrowDropDownIcon/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{openSubmenu && submenu && (
|
||||
<div
|
||||
ref={submenuRef}
|
||||
className={classNames({
|
||||
"vm-legend-hits-menu": true,
|
||||
"vm-legend-hits-menu_submenu": true,
|
||||
"vm-legend-hits-menu_submenu_left": posSubmenuLeft
|
||||
})}
|
||||
>
|
||||
<div className="vm-legend-hits-menu-section">
|
||||
{submenu.map(({ icon, title, handler }) => (
|
||||
<LegendHitsMenuRow
|
||||
key={title}
|
||||
iconStart={icon}
|
||||
title={title}
|
||||
handler={handler}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LegendHitsMenuRow;
|
||||
@@ -1,23 +0,0 @@
|
||||
import React, { FC } from "preact/compat";
|
||||
import { LegendLogHits } from "../../../../api/types";
|
||||
|
||||
interface Props {
|
||||
legend: LegendLogHits;
|
||||
}
|
||||
|
||||
const LegendHitsMenuStats: FC<Props> = ({ legend }) => {
|
||||
const totalFormatted = legend.total.toLocaleString("en-US");
|
||||
const percentage = Math.round((legend.total / legend.totalHits) * 100);
|
||||
|
||||
return (
|
||||
<div className="vm-legend-hits-menu-section">
|
||||
<div className="vm-legend-hits-menu-row">
|
||||
<div className="vm-legend-hits-menu-row__title">
|
||||
Total: {totalFormatted} ({percentage}%)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LegendHitsMenuStats;
|
||||
@@ -1,178 +0,0 @@
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-legend-hits-menu {
|
||||
min-width: 160px;
|
||||
z-index: 1;
|
||||
|
||||
&_submenu {
|
||||
position: absolute;
|
||||
top: calc(-1 * $padding-small);
|
||||
background-color: $color-background-block;
|
||||
left: calc(100% + ($padding-small / 2));
|
||||
box-shadow: $box-shadow-popper;
|
||||
border-radius: $border-radius-small;
|
||||
animation: vm-submenu-show 150ms cubic-bezier(0.280, 0.840, 0.2, 1);
|
||||
transform-origin: top left;
|
||||
|
||||
&_left {
|
||||
left: auto;
|
||||
right: calc(100% + ($padding-small / 2));
|
||||
transform-origin: top right;
|
||||
}
|
||||
}
|
||||
|
||||
&-section {
|
||||
border-bottom: $border-divider;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
&-row {
|
||||
position: relative;
|
||||
display: flex;
|
||||
gap: $padding-small;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
padding: 0 $padding-global;
|
||||
transition: background-color 0.3s;
|
||||
color: $color-text;
|
||||
|
||||
&_interactive {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
&_info {
|
||||
font-size: $font-size-small;
|
||||
font-weight: 500;
|
||||
padding-block: $padding-small;
|
||||
}
|
||||
|
||||
&_info &__icon {
|
||||
color: $color-info;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
|
||||
&_drop {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
}
|
||||
|
||||
&__title {
|
||||
flex-grow: 1;
|
||||
padding: $padding-global 0;
|
||||
position: relative;
|
||||
max-width: 400px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
&-other-list {
|
||||
width: 80vw;
|
||||
height: 80vh;
|
||||
overflow: auto;
|
||||
|
||||
&__search {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
padding: $padding-small 0;
|
||||
background-color: $color-background-block;
|
||||
border-bottom: $border-divider;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
&-row {
|
||||
border-bottom: $border-divider;
|
||||
|
||||
&_header {
|
||||
border-bottom: none;
|
||||
position: sticky;
|
||||
top: 65px;
|
||||
background-color: $color-background-block;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
border-bottom: $border-divider;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-cell {
|
||||
padding: calc($padding-small / 2) 0;
|
||||
text-align: left;
|
||||
|
||||
&_header {
|
||||
padding: $padding-small;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&_number {
|
||||
padding: $padding-small;
|
||||
text-align: right;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
&_fields {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&-fields {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
|
||||
&__field {
|
||||
padding: calc($padding-small / 2) $padding-small;
|
||||
border-radius: $border-radius-small;
|
||||
transition: background-color 0.3s;
|
||||
|
||||
&:hover {
|
||||
background-color: $color-hover-black;
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
&:after {
|
||||
content: ',';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes vm-submenu-show {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@@ -36,14 +36,6 @@ interface UseGetBarHitsOptionsArgs {
|
||||
graphOptions: GraphOptions;
|
||||
}
|
||||
|
||||
export const OTHER_HITS_LABEL = "other";
|
||||
|
||||
export const getLabelFromLogHit = (logHit: LogHits) => {
|
||||
if (logHit?._isOther) return OTHER_HITS_LABEL;
|
||||
const fields = Object.values(logHit?.fields || {});
|
||||
return fields.map((value) => value || "\"\"").join(", ");
|
||||
};
|
||||
|
||||
const useBarHitsOptions = ({
|
||||
data,
|
||||
logHits,
|
||||
@@ -67,16 +59,16 @@ const useBarHitsOptions = ({
|
||||
let colorN = 0;
|
||||
return data.map((_d, i) => {
|
||||
if (i === 0) return {}; // 0 index is xAxis(timestamps)
|
||||
const target = logHits?.[i - 1];
|
||||
const label = getLabelFromLogHit(target);
|
||||
const color = getCssVariable(target?._isOther ? "color-log-hits-bar-0" : seriesColors[colorN]);
|
||||
if (!target?._isOther) colorN++;
|
||||
const fields = Object.values(logHits?.[i - 1]?.fields || {});
|
||||
const label = fields.map((value) => value || "\"\"").join(", ");
|
||||
const color = getCssVariable(label ? seriesColors[colorN] : "color-log-hits-bar-0");
|
||||
if (label) colorN++;
|
||||
return {
|
||||
label,
|
||||
label: label || "other",
|
||||
width: strokeWidth[graphOptions.graphStyle],
|
||||
spanGaps: true,
|
||||
stroke: color,
|
||||
fill: graphOptions.fill ? color + (target?._isOther ? "" : "80") : "",
|
||||
fill: graphOptions.fill ? color + "80" : "",
|
||||
paths: getSeriesPaths(graphOptions.graphStyle),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { FC, useCallback, useEffect, useRef, useState, createPortal } from "preact/compat";
|
||||
import React, { FC, useCallback, useEffect, useRef, useState } from "preact/compat";
|
||||
import { MouseEvent as ReactMouseEvent } from "react";
|
||||
import useEventListener from "../../../hooks/useEventListener";
|
||||
import ReactDOM from "react-dom";
|
||||
import classNames from "classnames";
|
||||
import uPlot from "uplot";
|
||||
import Button from "../../Main/Button/Button";
|
||||
@@ -48,7 +49,7 @@ const ChartTooltip: FC<ChartTooltipProps> = ({
|
||||
onClose && onClose(id);
|
||||
};
|
||||
|
||||
const handleMouseDown = (e: ReactMouseEvent<HTMLButtonElement>) => {
|
||||
const handleMouseDown = (e: ReactMouseEvent) => {
|
||||
setMoved(true);
|
||||
setMoving(true);
|
||||
const { clientX, clientY } = e;
|
||||
@@ -106,7 +107,7 @@ const ChartTooltip: FC<ChartTooltipProps> = ({
|
||||
|
||||
if (!u) return null;
|
||||
|
||||
return createPortal((
|
||||
return ReactDOM.createPortal((
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-chart-tooltip": true,
|
||||
|
||||
@@ -32,11 +32,6 @@ $chart-tooltip-y: -1 * ($padding-global + $chart-tooltip-half-icon);
|
||||
max-width: calc(100vw/3);
|
||||
}
|
||||
|
||||
&_hits &-data {
|
||||
display: grid;
|
||||
grid-template-columns: $font-size 1fr;
|
||||
}
|
||||
|
||||
&_sticky {
|
||||
pointer-events: auto;
|
||||
z-index: 99;
|
||||
@@ -95,8 +90,6 @@ $chart-tooltip-y: -1 * ($padding-global + $chart-tooltip-half-icon);
|
||||
}
|
||||
|
||||
&__marker {
|
||||
min-width: $font-size;
|
||||
max-width: $font-size;
|
||||
width: $font-size;
|
||||
height: $font-size;
|
||||
border: 1px solid rgba($color-white, 0.5);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
@use "src/styles/variables" as *;
|
||||
@use 'sass:color';
|
||||
|
||||
$color-bar: #33BB55;
|
||||
$color-bar-highest: #F79420;
|
||||
@@ -8,7 +7,7 @@ $color-bar-highest: #F79420;
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
height: 100%;
|
||||
padding-bottom: calc($font-size-small / 2);
|
||||
padding-bottom: calc($font-size-small/2);
|
||||
overflow: hidden;
|
||||
|
||||
&-y-axis {
|
||||
@@ -55,19 +54,19 @@ $color-bar-highest: #F79420;
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
min-width: 1px;
|
||||
height: calc(100% - ($font-size-small * 4));
|
||||
height: calc(100% - ($font-size-small*4));
|
||||
background-color: $color-bar;
|
||||
transition: background-color 200ms ease-in;
|
||||
|
||||
&:hover {
|
||||
background-color: color.scale($color-bar, $lightness: 40%);
|
||||
background-color: lighten($color-bar, 10%);
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
background-color: $color-bar-highest;
|
||||
|
||||
&:hover {
|
||||
background-color: color.scale($color-bar-highest, $lightness: 40%);
|
||||
background-color: lighten($color-bar-highest, 10%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,11 +12,14 @@ import Timezones from "./Timezones/Timezones";
|
||||
import ThemeControl from "../ThemeControl/ThemeControl";
|
||||
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||
import useBoolean from "../../../hooks/useBoolean";
|
||||
import { AppType } from "../../../types/appType";
|
||||
import SwitchMarkdownParsing from "../LogsSettings/MarkdownParsing/SwitchMarkdownParsing";
|
||||
import { APP_TYPE_LOGS } from "../../../constants/appType";
|
||||
|
||||
const title = "Settings";
|
||||
|
||||
const { REACT_APP_TYPE } = process.env;
|
||||
const isLogsApp = REACT_APP_TYPE === AppType.logs;
|
||||
|
||||
export interface ChildComponentHandle {
|
||||
handleApply: () => void;
|
||||
}
|
||||
@@ -45,21 +48,21 @@ const GlobalSettings: FC = () => {
|
||||
|
||||
const controls = [
|
||||
{
|
||||
show: !appModeEnable && !APP_TYPE_LOGS,
|
||||
show: !appModeEnable && !isLogsApp,
|
||||
component: <ServerConfigurator
|
||||
ref={serverSettingRef}
|
||||
onClose={handleClose}
|
||||
/>
|
||||
},
|
||||
{
|
||||
show: !APP_TYPE_LOGS,
|
||||
show: !isLogsApp,
|
||||
component: <LimitsConfigurator
|
||||
ref={limitsSettingRef}
|
||||
onClose={handleClose}
|
||||
/>
|
||||
},
|
||||
{
|
||||
show: APP_TYPE_LOGS,
|
||||
show: isLogsApp,
|
||||
component: <SwitchMarkdownParsing/>
|
||||
},
|
||||
{
|
||||
|
||||
@@ -36,40 +36,35 @@ const AxesLimitsConfigurator: FC<AxesLimitsConfiguratorProps> = ({ yaxis, setYax
|
||||
"vm-axes-limits_mobile": isMobile
|
||||
})}
|
||||
>
|
||||
<div className="vm-graph-settings-row">
|
||||
<span className="vm-graph-settings-row__label">Fixed Y-axis limits</span>
|
||||
<Switch
|
||||
value={yaxis.limits.enable}
|
||||
onChange={toggleEnableLimits}
|
||||
label={`${yaxis.limits.enable ? "Fixed" : "Auto"} limits`}
|
||||
fullWidth={isMobile}
|
||||
/>
|
||||
<Switch
|
||||
value={yaxis.limits.enable}
|
||||
onChange={toggleEnableLimits}
|
||||
label="Fix the limits for y-axis"
|
||||
fullWidth={isMobile}
|
||||
/>
|
||||
<div className="vm-axes-limits-list">
|
||||
{axes.map(axis => (
|
||||
<div
|
||||
className="vm-axes-limits-list__inputs"
|
||||
key={axis}
|
||||
>
|
||||
<TextField
|
||||
label={`Min ${axis}`}
|
||||
type="number"
|
||||
disabled={!yaxis.limits.enable}
|
||||
value={yaxis.limits.range[axis][0]}
|
||||
onChange={createHandlerOnchangeAxis(axis, 0)}
|
||||
/>
|
||||
<TextField
|
||||
label={`Max ${axis}`}
|
||||
type="number"
|
||||
disabled={!yaxis.limits.enable}
|
||||
value={yaxis.limits.range[axis][1]}
|
||||
onChange={createHandlerOnchangeAxis(axis, 1)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{yaxis.limits.enable && (
|
||||
<div className="vm-axes-limits-list">
|
||||
{axes.map(axis => (
|
||||
<div
|
||||
className="vm-axes-limits-list__inputs"
|
||||
key={axis}
|
||||
>
|
||||
<TextField
|
||||
label={`Min ${axis}`}
|
||||
type="number"
|
||||
disabled={!yaxis.limits.enable}
|
||||
value={yaxis.limits.range[axis][0]}
|
||||
onChange={createHandlerOnchangeAxis(axis, 0)}
|
||||
/>
|
||||
<TextField
|
||||
label={`Max ${axis}`}
|
||||
type="number"
|
||||
disabled={!yaxis.limits.enable}
|
||||
value={yaxis.limits.range[axis][1]}
|
||||
onChange={createHandlerOnchangeAxis(axis, 1)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>;
|
||||
};
|
||||
|
||||
|
||||
@@ -8,14 +8,10 @@ import "./style.scss";
|
||||
import Tooltip from "../../Main/Tooltip/Tooltip";
|
||||
import useBoolean from "../../../hooks/useBoolean";
|
||||
import LinesConfigurator from "./LinesConfigurator/LinesConfigurator";
|
||||
import GraphTypeSwitcher from "./GraphTypeSwitcher/GraphTypeSwitcher";
|
||||
import { MetricResult } from "../../../api/types";
|
||||
import { isHistogramData } from "../../../utils/metric";
|
||||
|
||||
const title = "Graph settings";
|
||||
|
||||
interface GraphSettingsProps {
|
||||
data: MetricResult[],
|
||||
yaxis: YaxisState,
|
||||
setYaxisLimits: (limits: AxisRange) => void,
|
||||
toggleEnableLimits: () => void,
|
||||
@@ -23,13 +19,11 @@ interface GraphSettingsProps {
|
||||
value: boolean,
|
||||
onChange: (value: boolean) => void,
|
||||
},
|
||||
isHistogram?: boolean,
|
||||
}
|
||||
|
||||
const GraphSettings: FC<GraphSettingsProps> = ({ data, yaxis, setYaxisLimits, toggleEnableLimits, spanGaps }) => {
|
||||
const GraphSettings: FC<GraphSettingsProps> = ({ yaxis, setYaxisLimits, toggleEnableLimits, spanGaps }) => {
|
||||
const popperRef = useRef<HTMLDivElement>(null);
|
||||
const buttonRef = useRef<HTMLDivElement>(null);
|
||||
const displayHistogramMode = isHistogramData(data);
|
||||
|
||||
const {
|
||||
value: openPopper,
|
||||
@@ -70,7 +64,6 @@ const GraphSettings: FC<GraphSettingsProps> = ({ data, yaxis, setYaxisLimits, to
|
||||
spanGaps={spanGaps.value}
|
||||
onChange={spanGaps.onChange}
|
||||
/>
|
||||
{displayHistogramMode && <GraphTypeSwitcher onChange={handleClose}/>}
|
||||
</div>
|
||||
</div>
|
||||
</Popper>
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import React, { FC } from "preact/compat";
|
||||
import Switch from "../../../Main/Switch/Switch";
|
||||
import useDeviceDetect from "../../../../hooks/useDeviceDetect";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { useChangeDisplayMode } from "./useChangeDisplayMode";
|
||||
|
||||
type Props = {
|
||||
onChange: () => void;
|
||||
}
|
||||
|
||||
const GraphTypeSwitcher: FC<Props> = ({ onChange }) => {
|
||||
const { isMobile } = useDeviceDetect();
|
||||
|
||||
const { handleChange } = useChangeDisplayMode();
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const value = !searchParams.get("display_mode");
|
||||
|
||||
const handleChangeMode = (val: boolean) => {
|
||||
handleChange(val, onChange);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="vm-graph-settings-row">
|
||||
<span className="vm-graph-settings-row__label">Histogram mode</span>
|
||||
<Switch
|
||||
value={value}
|
||||
onChange={handleChangeMode}
|
||||
label={value ? "Enabled" : "Disabled"}
|
||||
fullWidth={isMobile}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GraphTypeSwitcher;
|
||||
@@ -1,16 +0,0 @@
|
||||
import { useTimeDispatch } from "../../../../state/time/TimeStateContext";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
|
||||
export const useChangeDisplayMode = () => {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const dispatch = useTimeDispatch();
|
||||
|
||||
const handleChange = (val: boolean, callback?: () => void) => {
|
||||
val ? searchParams.delete("display_mode") : searchParams.set("display_mode", "lines");
|
||||
setSearchParams(searchParams);
|
||||
dispatch({ type: "RUN_QUERY" });
|
||||
callback && callback();
|
||||
};
|
||||
|
||||
return { handleChange };
|
||||
};
|
||||
@@ -10,17 +10,14 @@ interface Props {
|
||||
const LinesConfigurator: FC<Props> = ({ spanGaps, onChange }) => {
|
||||
const { isMobile } = useDeviceDetect();
|
||||
|
||||
return (
|
||||
<div className="vm-graph-settings-row">
|
||||
<span className="vm-graph-settings-row__label">Connect null values</span>
|
||||
<Switch
|
||||
value={spanGaps}
|
||||
onChange={onChange}
|
||||
label={spanGaps ? "Enabled" : "Disabled"}
|
||||
fullWidth={isMobile}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
return <div>
|
||||
<Switch
|
||||
value={spanGaps}
|
||||
onChange={onChange}
|
||||
label="Connect null values"
|
||||
fullWidth={isMobile}
|
||||
/>
|
||||
</div>;
|
||||
};
|
||||
|
||||
export default LinesConfigurator;
|
||||
|
||||
@@ -1,31 +1,15 @@
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-graph-settings {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $padding-small;
|
||||
|
||||
&-popper {
|
||||
display: grid;
|
||||
gap: $padding-global;
|
||||
padding: $padding-small $padding-large $padding-large;
|
||||
min-width: 300px;
|
||||
padding: 0 0 $padding-global;
|
||||
|
||||
&__body {
|
||||
display: grid;
|
||||
gap: $padding-large;
|
||||
}
|
||||
}
|
||||
|
||||
&-row {
|
||||
display: grid;
|
||||
gap: $padding-small;
|
||||
grid-template-columns: minmax(150px, max-content) 1fr;
|
||||
|
||||
&__label {
|
||||
&:after{
|
||||
content: ":";
|
||||
}
|
||||
padding: 0 $padding-global;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import { useCallback } from "react";
|
||||
import { AUTOCOMPLETE_LIMITS } from "../../../../constants/queryAutocomplete";
|
||||
import { LogsFiledValues } from "../../../../api/types";
|
||||
import { useLogsDispatch, useLogsState } from "../../../../state/logsPanel/LogsStateContext";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
|
||||
type FetchDataArgs = {
|
||||
urlSuffix: string;
|
||||
@@ -28,8 +27,6 @@ const icons = {
|
||||
};
|
||||
|
||||
export const useFetchLogsQLOptions = (contextData?: ContextData) => {
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const { serverUrl } = useAppState();
|
||||
const { period: { start, end } } = useTimeState();
|
||||
const { autocompleteCache } = useLogsState();
|
||||
@@ -63,18 +60,11 @@ export const useFetchLogsQLOptions = (contextData?: ContextData) => {
|
||||
};
|
||||
|
||||
const fetchData = async ({ urlSuffix, setter, type, params }: FetchDataArgs) => {
|
||||
// if (!value && type === TypeData.metric) return;
|
||||
abortControllerRef.current.abort();
|
||||
abortControllerRef.current = new AbortController();
|
||||
const { signal } = abortControllerRef.current;
|
||||
|
||||
const tenant = {
|
||||
AccountID: searchParams.get("accountID") || "0",
|
||||
ProjectID: searchParams.get("projectID") || "0"
|
||||
};
|
||||
const tenantString = new URLSearchParams(tenant).toString();
|
||||
|
||||
const key = `${urlSuffix}?${params?.toString()}&${tenantString}`;
|
||||
|
||||
const key = `${urlSuffix}?${params?.toString()}`;
|
||||
setLoading(true);
|
||||
try {
|
||||
const cachedData = autocompleteCache.get(key);
|
||||
@@ -83,12 +73,7 @@ export const useFetchLogsQLOptions = (contextData?: ContextData) => {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch(`${serverUrl}/select/logsql/${urlSuffix}?${params}`, {
|
||||
signal,
|
||||
headers: { ...tenant }
|
||||
});
|
||||
|
||||
const response = await fetch(`${serverUrl}/select/logsql/${urlSuffix}?${params}`, { signal });
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
const value = (data?.values || []) as LogsFiledValues[];
|
||||
|
||||
@@ -36,7 +36,7 @@ export class QueryAutocompleteCache {
|
||||
put(key: QueryAutocompleteCacheItem, value: string[]) {
|
||||
if (this.map.size >= this.maxSize) {
|
||||
const firstKey = this.map.keys().next().value;
|
||||
firstKey && this.map.delete(firstKey);
|
||||
this.map.delete(firstKey);
|
||||
}
|
||||
this.map.set(JSON.stringify(key), value);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { FC, useEffect, useRef, useState } from "preact/compat";
|
||||
import { KeyboardEvent } from "react";
|
||||
import { ErrorTypes } from "../../../types";
|
||||
import TextField, { TextFieldKeyboardEvent } from "../../Main/TextField/TextField";
|
||||
import TextField from "../../Main/TextField/TextField";
|
||||
import "./style.scss";
|
||||
import { QueryStats } from "../../../api/types";
|
||||
import { partialWarning, seriesFetchedWarning } from "./warningText";
|
||||
@@ -80,7 +81,7 @@ const QueryEditor: FC<QueryEditorProps> = ({
|
||||
setCaretPositionInput([caretPosition, caretPosition]);
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: TextFieldKeyboardEvent) => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
const { key, ctrlKey, metaKey, shiftKey } = e;
|
||||
|
||||
const value = (e.target as HTMLTextAreaElement).value || "";
|
||||
@@ -123,7 +124,7 @@ const QueryEditor: FC<QueryEditorProps> = ({
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setOpenAutocomplete(!!AutocompleteEl && autocompleteQuick);
|
||||
setOpenAutocomplete(!!AutocompleteEl);
|
||||
}, [autocompleteQuick]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -14,8 +14,6 @@ import Popper from "../../Main/Popper/Popper";
|
||||
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||
import classNames from "classnames";
|
||||
import useBoolean from "../../../hooks/useBoolean";
|
||||
import { useCustomPanelState } from "../../../state/customPanel/CustomPanelStateContext";
|
||||
import Hyperlink from "../../Main/Hyperlink/Hyperlink";
|
||||
|
||||
const StepConfigurator: FC = () => {
|
||||
const appModeEnable = getAppModeEnable();
|
||||
@@ -24,20 +22,16 @@ const StepConfigurator: FC = () => {
|
||||
const { customStep: value, isHistogram } = useGraphState();
|
||||
const { period: { step, end, start } } = useTimeState();
|
||||
const graphDispatch = useGraphDispatch();
|
||||
const { displayType } = useCustomPanelState();
|
||||
|
||||
const prevDuration = usePrevious(end - start);
|
||||
|
||||
const defaultStep = useMemo(() => {
|
||||
return getStepFromDuration(end - start, isHistogram, displayType);
|
||||
}, [end, start, isHistogram, displayType]);
|
||||
const prevDefaultStep = usePrevious(defaultStep);
|
||||
return getStepFromDuration(end - start, isHistogram);
|
||||
}, [step, isHistogram]);
|
||||
|
||||
const [customStep, setCustomStep] = useState(value || defaultStep);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const isAutoStep = value === defaultStep;
|
||||
|
||||
const {
|
||||
value: openOptions,
|
||||
toggle: toggleOpenOptions,
|
||||
@@ -107,15 +101,15 @@ const StepConfigurator: FC = () => {
|
||||
|
||||
useEffect(() => {
|
||||
const dur = end - start;
|
||||
if (dur === prevDuration || !prevDuration || value !== prevDefaultStep) return;
|
||||
if (dur === prevDuration || !prevDuration) return;
|
||||
if (defaultStep) {
|
||||
handleApply(defaultStep);
|
||||
}
|
||||
}, [prevDuration, defaultStep]);
|
||||
}, [end, start, prevDuration, defaultStep]);
|
||||
|
||||
useEffect(() => {
|
||||
if (step === value || step === defaultStep) handleApply(defaultStep);
|
||||
}, [isHistogram, displayType]);
|
||||
}, [isHistogram]);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -135,15 +129,22 @@ const StepConfigurator: FC = () => {
|
||||
<span className="vm-mobile-option__arrow"><ArrowDownIcon/></span>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
className={appModeEnable ? "" : "vm-header-button"}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={<TimelineIcon/>}
|
||||
onClick={toggleOpenOptions}
|
||||
>
|
||||
Step: {isAutoStep ? `auto (${customStep})` : customStep}
|
||||
</Button>
|
||||
<Tooltip title="Query resolution step width">
|
||||
<Button
|
||||
className={appModeEnable ? "" : "vm-header-button"}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={<TimelineIcon/>}
|
||||
onClick={toggleOpenOptions}
|
||||
>
|
||||
<p>
|
||||
STEP
|
||||
<p className="vm-step-control__value">
|
||||
{customStep}
|
||||
</p>
|
||||
</p>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Popper
|
||||
open={openOptions}
|
||||
@@ -168,7 +169,7 @@ const StepConfigurator: FC = () => {
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleApply}
|
||||
endIcon={(
|
||||
<Tooltip title={`Reset to auto step (${defaultStep})`}>
|
||||
<Tooltip title={`Set default step value: ${defaultStep}`}>
|
||||
<Button
|
||||
size="small"
|
||||
variant="text"
|
||||
@@ -181,23 +182,25 @@ const StepConfigurator: FC = () => {
|
||||
)}
|
||||
/>
|
||||
<div className="vm-step-control-popper-info">
|
||||
<p>
|
||||
<code>step</code> - the <Hyperlink
|
||||
href="https://prometheus.io/docs/prometheus/latest/querying/basics/#time-durations"
|
||||
text="interval"
|
||||
/> between datapoints, which must be returned from the range query.
|
||||
The <code>query</code> is executed
|
||||
at <code>start</code>, <code>start+step</code>, <code>start+2*step</code>, …, <code>end</code> timestamps.
|
||||
</p>
|
||||
<p>
|
||||
Read more about <Hyperlink
|
||||
href="https://docs.victoriametrics.com/keyConcepts.html#range-query"
|
||||
text="Range"
|
||||
/> and <Hyperlink
|
||||
href="https://docs.victoriametrics.com/keyconcepts/#instant-query"
|
||||
text="Instant"
|
||||
/> queries.
|
||||
</p>
|
||||
<code>step</code> - the <a
|
||||
className="vm-link vm-link_colored"
|
||||
href="https://prometheus.io/docs/prometheus/latest/querying/basics/#time-durations"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
interval
|
||||
</a>
|
||||
between datapoints, which must be returned from the range query.
|
||||
The <code>query</code> is executed at
|
||||
<code>start</code>, <code>start+step</code>, <code>start+2*step</code>, …, <code>end</code> timestamps.
|
||||
<a
|
||||
className="vm-link vm-link_colored"
|
||||
href="https://docs.victoriametrics.com/keyConcepts.html#range-query"
|
||||
target="_blank"
|
||||
rel="help noreferrer"
|
||||
>
|
||||
Read more about Range query
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</Popper>
|
||||
|
||||
@@ -8,10 +8,16 @@
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
&__value {
|
||||
display: inline;
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
||||
&-popper {
|
||||
display: grid;
|
||||
gap: $padding-small;
|
||||
max-width: 300px;
|
||||
max-height: 208px;
|
||||
overflow: auto;
|
||||
padding: $padding-global;
|
||||
font-size: $font-size;
|
||||
@@ -30,11 +36,13 @@
|
||||
font-size: $font-size-small;
|
||||
line-height: 1.8;
|
||||
|
||||
a {
|
||||
margin: 0 0.4em;
|
||||
}
|
||||
|
||||
code {
|
||||
padding: .2em .4em;
|
||||
margin: 0;
|
||||
font-size: 85%;
|
||||
white-space: break-spaces;
|
||||
padding: 0.2em 0.4em;
|
||||
margin: 0 0.2em;
|
||||
background-color: $color-hover-black;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
@@ -1,244 +0,0 @@
|
||||
import React, { FC, useMemo, useState } from "preact/compat";
|
||||
import useBoolean from "../../../hooks/useBoolean";
|
||||
import { RestartIcon, SettingsIcon } from "../../Main/Icons";
|
||||
import Button from "../../Main/Button/Button";
|
||||
import Modal from "../../Main/Modal/Modal";
|
||||
import Tooltip from "../../Main/Tooltip/Tooltip";
|
||||
import { Logs } from "../../../api/types";
|
||||
import Select from "../../Main/Select/Select";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import "./style.scss";
|
||||
import Switch from "../../Main/Switch/Switch";
|
||||
import TextField from "../../Main/TextField/TextField";
|
||||
import dayjs from "dayjs";
|
||||
import Hyperlink from "../../Main/Hyperlink/Hyperlink";
|
||||
import {
|
||||
LOGS_DISPLAY_FIELDS,
|
||||
LOGS_GROUP_BY,
|
||||
LOGS_DATE_FORMAT,
|
||||
LOGS_URL_PARAMS,
|
||||
WITHOUT_GROUPING
|
||||
} from "../../../constants/logs";
|
||||
|
||||
const {
|
||||
GROUP_BY,
|
||||
NO_WRAP_LINES,
|
||||
COMPACT_GROUP_HEADER,
|
||||
DISPLAY_FIELDS,
|
||||
DATE_FORMAT
|
||||
} = LOGS_URL_PARAMS;
|
||||
|
||||
const title = "Group view settings";
|
||||
|
||||
interface Props {
|
||||
logs: Logs[];
|
||||
}
|
||||
|
||||
const GroupLogsConfigurators: FC<Props> = ({ logs }) => {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
const groupBy = searchParams.get(GROUP_BY) || LOGS_GROUP_BY;
|
||||
const noWrapLines = searchParams.get(NO_WRAP_LINES) === "true";
|
||||
const compactGroupHeader = searchParams.get(COMPACT_GROUP_HEADER) === "true";
|
||||
const displayFieldsString = searchParams.get(DISPLAY_FIELDS) || "";
|
||||
const displayFields = displayFieldsString ? displayFieldsString.split(",") : [LOGS_DISPLAY_FIELDS];
|
||||
|
||||
const [dateFormat, setDateFormat] = useState(searchParams.get(DATE_FORMAT) || LOGS_DATE_FORMAT);
|
||||
const [errorFormat, setErrorFormat] = useState("");
|
||||
|
||||
const isGroupChanged = groupBy !== LOGS_GROUP_BY;
|
||||
const isDisplayFieldsChanged = displayFields.length !== 1 || displayFields[0] !== LOGS_DISPLAY_FIELDS;
|
||||
const isTimeChanged = searchParams.get(DATE_FORMAT) !== LOGS_DATE_FORMAT;
|
||||
const hasChanges = [
|
||||
isGroupChanged,
|
||||
isDisplayFieldsChanged,
|
||||
noWrapLines,
|
||||
compactGroupHeader,
|
||||
isTimeChanged
|
||||
].some(Boolean);
|
||||
|
||||
const logsKeys = useMemo(() => {
|
||||
return Array.from(new Set(logs.map(l => Object.keys(l)).flat()));
|
||||
}, [logs]);
|
||||
|
||||
const {
|
||||
value: openModal,
|
||||
toggle: toggleOpen,
|
||||
setFalse: handleClose,
|
||||
} = useBoolean(false);
|
||||
|
||||
const handleSelectGroupBy = (key: string) => {
|
||||
searchParams.set(GROUP_BY, key);
|
||||
setSearchParams(searchParams);
|
||||
};
|
||||
|
||||
const handleSelectDisplayField = (value: string) => {
|
||||
const prev = displayFields;
|
||||
const newDisplayFields = prev.includes(value) ? prev.filter(v => v !== value) : [...prev, value];
|
||||
searchParams.set(DISPLAY_FIELDS, newDisplayFields.join(","));
|
||||
setSearchParams(searchParams);
|
||||
};
|
||||
|
||||
const handleResetDisplayFields = () => {
|
||||
searchParams.delete(DISPLAY_FIELDS);
|
||||
setSearchParams(searchParams);
|
||||
};
|
||||
|
||||
const toggleWrapLines = () => {
|
||||
searchParams.set(NO_WRAP_LINES, String(!noWrapLines));
|
||||
setSearchParams(searchParams);
|
||||
};
|
||||
|
||||
const toggleCompactGroupHeader = () => {
|
||||
searchParams.set(COMPACT_GROUP_HEADER, String(!compactGroupHeader));
|
||||
setSearchParams(searchParams);
|
||||
};
|
||||
|
||||
const handleChangeDateFormat = (format: string) => {
|
||||
const date = new Date();
|
||||
if (!dayjs(date, format, true).isValid()) {
|
||||
setErrorFormat("Invalid date format");
|
||||
}
|
||||
setDateFormat(format);
|
||||
};
|
||||
|
||||
const handleSaveAndClose = () => {
|
||||
searchParams.set(DATE_FORMAT, dateFormat);
|
||||
setSearchParams(searchParams);
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const tooltipContent = () => {
|
||||
if (!hasChanges) return title;
|
||||
return (
|
||||
<div className="vm-group-logs-configurator__tooltip">
|
||||
<p>{title}</p>
|
||||
<hr/>
|
||||
<ul>
|
||||
{isGroupChanged && <li>Group by <code>{`"${groupBy}"`}</code></li>}
|
||||
{isDisplayFieldsChanged && <li>Display fields: {displayFields.length || 1}</li>}
|
||||
{noWrapLines && <li>Single-line text is enabled</li>}
|
||||
{compactGroupHeader && <li>Compact group header is enabled</li>}
|
||||
{isTimeChanged && <li>Date format: <code>{dateFormat}</code></li>}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="vm-group-logs-configurator-button">
|
||||
<Tooltip title={tooltipContent()}>
|
||||
<Button
|
||||
variant="text"
|
||||
startIcon={<SettingsIcon/>}
|
||||
onClick={toggleOpen}
|
||||
ariaLabel={title}
|
||||
/>
|
||||
</Tooltip>
|
||||
{hasChanges && <span className="vm-group-logs-configurator-button__marker"/>}
|
||||
</div>
|
||||
{openModal && (
|
||||
<Modal
|
||||
title={title}
|
||||
onClose={handleSaveAndClose}
|
||||
>
|
||||
<div className="vm-group-logs-configurator">
|
||||
<div className="vm-group-logs-configurator-item">
|
||||
<Select
|
||||
value={groupBy}
|
||||
list={[WITHOUT_GROUPING, ...logsKeys]}
|
||||
label="Group by field"
|
||||
placeholder="Group by field"
|
||||
onChange={handleSelectGroupBy}
|
||||
searchable
|
||||
/>
|
||||
<Tooltip title={"Reset grouping"}>
|
||||
<Button
|
||||
variant="text"
|
||||
color="primary"
|
||||
startIcon={<RestartIcon/>}
|
||||
onClick={() => handleSelectGroupBy(LOGS_GROUP_BY)}
|
||||
/>
|
||||
</Tooltip>
|
||||
<span className="vm-group-logs-configurator-item__info">
|
||||
Select a field to group logs by (default: <code>{LOGS_GROUP_BY}</code>).
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="vm-group-logs-configurator-item">
|
||||
<Select
|
||||
value={displayFields}
|
||||
list={logsKeys}
|
||||
label="Display fields"
|
||||
placeholder="Display fields"
|
||||
onChange={handleSelectDisplayField}
|
||||
searchable
|
||||
/>
|
||||
<Tooltip title={"Clear fields"}>
|
||||
<Button
|
||||
variant="text"
|
||||
color="primary"
|
||||
startIcon={<RestartIcon/>}
|
||||
onClick={handleResetDisplayFields}
|
||||
/>
|
||||
</Tooltip>
|
||||
<span className="vm-group-logs-configurator-item__info">
|
||||
Select fields to display instead of the message (default: <code>{LOGS_DISPLAY_FIELDS}</code>).
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="vm-group-logs-configurator-item">
|
||||
<TextField
|
||||
autofocus
|
||||
label="Date format"
|
||||
value={dateFormat}
|
||||
onChange={handleChangeDateFormat}
|
||||
error={errorFormat}
|
||||
/>
|
||||
<Tooltip title={"Reset format"}>
|
||||
<Button
|
||||
variant="text"
|
||||
color="primary"
|
||||
startIcon={<RestartIcon/>}
|
||||
onClick={() => setDateFormat(LOGS_DATE_FORMAT)}
|
||||
/>
|
||||
</Tooltip>
|
||||
<span className="vm-group-logs-configurator-item__info vm-group-logs-configurator-item__info_input">
|
||||
Set the date format (e.g., <code>YYYY-MM-DD HH:mm:ss</code>).
|
||||
Learn more in <Hyperlink
|
||||
href="https://day.js.org/docs/en/display/format"
|
||||
>this documentation</Hyperlink>. <br/>
|
||||
Your current date format: <code>{dayjs().format(dateFormat || LOGS_DATE_FORMAT)}</code>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="vm-group-logs-configurator-item">
|
||||
<Switch
|
||||
value={noWrapLines}
|
||||
onChange={toggleWrapLines}
|
||||
label="Single-line message"
|
||||
/>
|
||||
<span className="vm-group-logs-configurator-item__info">
|
||||
Displays message in a single line and truncates it with an ellipsis if it exceeds the available space
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="vm-group-logs-configurator-item">
|
||||
<Switch
|
||||
value={compactGroupHeader}
|
||||
onChange={toggleCompactGroupHeader}
|
||||
label="Compact group header"
|
||||
/>
|
||||
<span className="vm-group-logs-configurator-item__info">
|
||||
Shows group headers in one line with a "+N more" badge for extra fields.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default GroupLogsConfigurators;
|
||||
@@ -1,48 +0,0 @@
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-group-logs-configurator {
|
||||
display: grid;
|
||||
gap: calc($padding-large * 2);
|
||||
padding: $padding-global 0;
|
||||
width: 600px;
|
||||
|
||||
&-item {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 31px;
|
||||
align-items: center;
|
||||
justify-content: stretch;
|
||||
gap: 0 $padding-small;
|
||||
|
||||
&__info {
|
||||
margin-top: $padding-small;
|
||||
grid-column: 1/span 2;
|
||||
font-size: $font-size-small;
|
||||
color: $color-text-secondary;
|
||||
line-height: 130%;
|
||||
|
||||
&_input {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-button {
|
||||
position: relative;
|
||||
|
||||
&__marker {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
left: 6px;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-radius: 50%;
|
||||
background-color: $color-secondary;
|
||||
}
|
||||
}
|
||||
|
||||
&__tooltip {
|
||||
ul {
|
||||
list-style-position: inside;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,10 +30,6 @@ const Accordion: FC<AccordionProps> = ({
|
||||
onChange && onChange(isOpen);
|
||||
}, [isOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
setIsOpen(defaultExpanded);
|
||||
}, [defaultExpanded]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<header
|
||||
|
||||
@@ -14,8 +14,8 @@ interface ButtonProps {
|
||||
disabled?: boolean
|
||||
children?: ReactNode
|
||||
className?: string
|
||||
onClick?: (e: ReactMouseEvent<HTMLButtonElement>) => void
|
||||
onMouseDown?: (e: ReactMouseEvent<HTMLButtonElement>) => void
|
||||
onClick?: (e: ReactMouseEvent<HTMLButtonElement, MouseEvent>) => void
|
||||
onMouseDown?: (e: ReactMouseEvent<HTMLButtonElement, MouseEvent>) => void
|
||||
}
|
||||
|
||||
const Button: FC<ButtonProps> = ({
|
||||
|
||||
@@ -4,16 +4,13 @@ import { useState } from "react";
|
||||
import Tooltip from "../Tooltip/Tooltip";
|
||||
import Button from "../Button/Button";
|
||||
import { CopyIcon } from "../Icons";
|
||||
import useCopyToClipboard from "../../../hooks/useCopyToClipboard";
|
||||
|
||||
enum CopyState { copy = "Copy", copied = "Copied" }
|
||||
|
||||
const CodeExample: FC<{code: string}> = ({ code }) => {
|
||||
const copyToClipboard = useCopyToClipboard();
|
||||
|
||||
const [tooltip, setTooltip] = useState(CopyState.copy);
|
||||
const handlerCopy = async () => {
|
||||
await copyToClipboard(code);
|
||||
const handlerCopy = () => {
|
||||
navigator.clipboard.writeText(code);
|
||||
setTooltip(CopyState.copied);
|
||||
};
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ const DateTimeInput: FC<DateTimeInputProps> = ({
|
||||
onChange(maskedValue);
|
||||
};
|
||||
|
||||
const handleKeyUp = (e: KeyboardEvent<HTMLInputElement>) => {
|
||||
const handleKeyUp = (e: KeyboardEvent) => {
|
||||
if (e.key === "Enter") {
|
||||
onChange(maskedValue);
|
||||
setAwaitChangeForEnter(true);
|
||||
|
||||
@@ -89,8 +89,7 @@ const TimePicker: FC<CalendarTimepickerProps>= ({ selectDate, onChangeTime, onCl
|
||||
};
|
||||
|
||||
const handleFocusInput = (unit: TimeUnits, e: FocusEvent<HTMLInputElement>) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
target && target.select();
|
||||
e.target.select();
|
||||
setActiveField(unit);
|
||||
};
|
||||
|
||||
|
||||
@@ -581,45 +581,3 @@ export const CommentIcon = () => (
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const FilterIcon = () => (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
d="M4.25 5.61C6.27 8.2 10 13 10 13v6c0 .55.45 1 1 1h2c.55 0 1-.45 1-1v-6s3.72-4.8 5.74-7.39c.51-.66.04-1.61-.79-1.61H5.04c-.83 0-1.3.95-.79 1.61"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const FilterOffIcon = () => (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
d="M19.79 5.61C20.3 4.95 19.83 4 19 4H6.83l7.97 7.97zM2.81 2.81 1.39 4.22 10 13v6c0 .55.45 1 1 1h2c.55 0 1-.45 1-1v-2.17l5.78 5.78 1.41-1.41z"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const OpenNewIcon = () => (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
d="M19 19H5V5h7V3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2v-7h-2zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3z"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const ModalIcon = () => (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M19 4H5c-1.11 0-2 .9-2 2v12c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.89-2-2-2m0 14H5V8h14z"></path>
|
||||
</svg>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { FC, useCallback, useEffect, createPortal } from "preact/compat";
|
||||
import React, { FC, useCallback, useEffect } from "preact/compat";
|
||||
import ReactDOM from "react-dom";
|
||||
import { CloseIcon } from "../Icons";
|
||||
import Button from "../Button/Button";
|
||||
import { ReactNode, MouseEvent } from "react";
|
||||
@@ -57,7 +58,7 @@ const Modal: FC<ModalProps> = ({
|
||||
useEventListener("popstate", handlePopstate);
|
||||
useEventListener("keyup", handleKeyUp);
|
||||
|
||||
return createPortal((
|
||||
return ReactDOM.createPortal((
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-modal": true,
|
||||
@@ -66,11 +67,11 @@ const Modal: FC<ModalProps> = ({
|
||||
})}
|
||||
onMouseDown={onClose}
|
||||
>
|
||||
<div
|
||||
className="vm-modal-content"
|
||||
onMouseDown={handleMouseDown}
|
||||
>
|
||||
<div className="vm-modal-content-header">
|
||||
<div className="vm-modal-content">
|
||||
<div
|
||||
className="vm-modal-content-header"
|
||||
onMouseDown={handleMouseDown}
|
||||
>
|
||||
{title && (
|
||||
<div className="vm-modal-content-header__title">
|
||||
{title}
|
||||
@@ -90,6 +91,7 @@ const Modal: FC<ModalProps> = ({
|
||||
{/* tabIndex to fix Ctrl-A */}
|
||||
<div
|
||||
className="vm-modal-content-body"
|
||||
onMouseDown={handleMouseDown}
|
||||
tabIndex={0}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -1,15 +1,6 @@
|
||||
import React, {
|
||||
FC,
|
||||
MouseEvent as ReactMouseEvent,
|
||||
ReactNode,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
useCallback,
|
||||
createPortal
|
||||
} from "react";
|
||||
import React, { FC, MouseEvent as ReactMouseEvent, ReactNode, useEffect, useMemo, useRef, useState } from "react";
|
||||
import classNames from "classnames";
|
||||
import ReactDOM from "react-dom";
|
||||
import "./style.scss";
|
||||
import useClickOutside from "../../../hooks/useClickOutside";
|
||||
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||
@@ -17,16 +8,16 @@ import Button from "../Button/Button";
|
||||
import { CloseIcon } from "../Icons";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import useEventListener from "../../../hooks/useEventListener";
|
||||
import { useCallback } from "preact/compat";
|
||||
|
||||
interface PopperProps {
|
||||
children: ReactNode
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
buttonRef: React.RefObject<HTMLElement>
|
||||
placement?: "bottom-right" | "bottom-left" | "top-left" | "top-right" | "fixed"
|
||||
placementPosition?: { top: number, left: number } | null
|
||||
placement?: "bottom-right" | "bottom-left" | "top-left" | "top-right"
|
||||
animation?: string
|
||||
offset?: { top: number, left: number }
|
||||
offset?: {top: number, left: number}
|
||||
clickOutside?: boolean,
|
||||
fullWidth?: boolean
|
||||
title?: string
|
||||
@@ -38,7 +29,6 @@ const Popper: FC<PopperProps> = ({
|
||||
children,
|
||||
buttonRef,
|
||||
placement = "bottom-left",
|
||||
placementPosition,
|
||||
open = false,
|
||||
onClose,
|
||||
offset = { top: 6, left: 0 },
|
||||
@@ -102,18 +92,13 @@ const Popper: FC<PopperProps> = ({
|
||||
if (needAlignRight) position.left = buttonPos.right - popperSize.width;
|
||||
if (needAlignTop) position.top = buttonPos.top - popperSize.height - offsetTop;
|
||||
|
||||
if (placement === "fixed" && placementPosition) {
|
||||
position.top = Math.max(placementPosition.top + offset.top, 0);
|
||||
position.left = Math.max(placementPosition.left + offset.left, 0);
|
||||
return position;
|
||||
}
|
||||
|
||||
const { innerWidth, innerHeight } = window;
|
||||
const margin = 20;
|
||||
|
||||
const isOverflowBottom = (position.top + popperSize.height) > innerHeight;
|
||||
const isOverflowTop = (position.top) < 0;
|
||||
const isOverflowRight = (position.left + popperSize.width) > innerWidth;
|
||||
const isOverflowLeft = (position.left) < 0;
|
||||
const isOverflowBottom = (position.top + popperSize.height + margin) > innerHeight;
|
||||
const isOverflowTop = (position.top - margin) < 0;
|
||||
const isOverflowRight = (position.left + popperSize.width + margin) > innerWidth;
|
||||
const isOverflowLeft = (position.left - margin) < 0;
|
||||
|
||||
if (isOverflowBottom) position.top = buttonPos.top - popperSize.height - offsetTop;
|
||||
if (isOverflowTop) position.top = buttonPos.height + buttonPos.top + offsetTop;
|
||||
@@ -121,13 +106,13 @@ const Popper: FC<PopperProps> = ({
|
||||
if (isOverflowLeft) position.left = buttonPos.left + offsetLeft;
|
||||
|
||||
if (fullWidth) position.width = `${buttonPos.width}px`;
|
||||
if (position.top < 0) position.top = 0;
|
||||
if (position.left < 0) position.left = 0;
|
||||
if (position.top < 0) position.top = 20;
|
||||
if (position.left < 0) position.left = 20;
|
||||
|
||||
return position;
|
||||
}, [buttonRef, placement, isOpen, children, fullWidth]);
|
||||
},[buttonRef, placement, isOpen, children, fullWidth]);
|
||||
|
||||
const handleClickClose = (e: ReactMouseEvent<HTMLButtonElement>) => {
|
||||
const handleClickClose = (e: ReactMouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
e.stopPropagation();
|
||||
onClose();
|
||||
};
|
||||
@@ -146,10 +131,10 @@ const Popper: FC<PopperProps> = ({
|
||||
if (!popperRef.current || !isOpen || (isMobile && !disabledFullScreen)) return;
|
||||
const { right, width } = popperRef.current.getBoundingClientRect();
|
||||
if (right > window.innerWidth) {
|
||||
const left = window.innerWidth - width;
|
||||
popperRef.current.style.left = `${left}px`;
|
||||
const left = window.innerWidth - 20 - width;
|
||||
popperRef.current.style.left = left < window.innerWidth ? "0" : `${left}px`;
|
||||
}
|
||||
}, [isOpen, popperRef, placementPosition]);
|
||||
}, [isOpen, popperRef]);
|
||||
|
||||
const handlePopstate = useCallback(() => {
|
||||
if (isOpen && isMobile && !disabledFullScreen) {
|
||||
@@ -164,7 +149,7 @@ const Popper: FC<PopperProps> = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
{(isOpen || !popperSize.width) && createPortal((
|
||||
{(isOpen || !popperSize.width) && ReactDOM.createPortal((
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-popper": true,
|
||||
|
||||
@@ -11,7 +11,7 @@ interface MultipleSelectedValueProps {
|
||||
const MultipleSelectedValue: FC<MultipleSelectedValueProps> = ({ values, onRemoveItem }) => {
|
||||
const { isMobile } = useDeviceDetect();
|
||||
|
||||
const createHandleClick = (value: string) => (e: MouseEvent<HTMLDivElement>) => {
|
||||
const createHandleClick = (value: string) => (e: MouseEvent) => {
|
||||
onRemoveItem(value);
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
@@ -92,7 +92,7 @@ const Select: FC<SelectProps> = ({
|
||||
setSearch((e.target as HTMLInputElement).value);
|
||||
};
|
||||
|
||||
const createHandleClick = (value: string) => (e: MouseEvent<HTMLDivElement>) => {
|
||||
const createHandleClick = (value: string) => (e: MouseEvent) => {
|
||||
handleSelected(value);
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user