Compare commits

..

1 Commits

Author SHA1 Message Date
Yury Molodov
8fe88c2ca5 app/vmui: display vmselect version in footer
Signed-off-by: Yury Molodov <yurymolodov@gmail.com>
2025-09-05 12:03:50 +02:00
1601 changed files with 76406 additions and 91287 deletions

View File

@@ -63,7 +63,7 @@ jobs:
- name: Setup Go
id: go
uses: actions/setup-go@v6
uses: actions/setup-go@v5
with:
cache-dependency-path: |
go.sum

View File

@@ -19,7 +19,7 @@ jobs:
- name: Setup Go
id: go
uses: actions/setup-go@v6
uses: actions/setup-go@v5
with:
go-version: stable
cache: false

View File

@@ -33,7 +33,7 @@ jobs:
- name: Set up Go
id: go
uses: actions/setup-go@v6
uses: actions/setup-go@v5
with:
cache: false
go-version: stable
@@ -49,14 +49,14 @@ jobs:
restore-keys: go-artifacts-${{ runner.os }}-codeql-analyze-
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
uses: github/codeql-action/init@v3
with:
languages: go
- name: Autobuild
uses: github/codeql-action/autobuild@v4
uses: github/codeql-action/autobuild@v3
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
uses: github/codeql-action/analyze@v3
with:
category: 'language:go'

View File

@@ -36,7 +36,7 @@ jobs:
- name: Setup Go
id: go
uses: actions/setup-go@v6
uses: actions/setup-go@v5
with:
cache-dependency-path: |
go.sum
@@ -75,7 +75,7 @@ jobs:
- name: Setup Go
id: go
uses: actions/setup-go@v6
uses: actions/setup-go@v5
with:
cache-dependency-path: |
go.sum
@@ -101,7 +101,7 @@ jobs:
- name: Setup Go
id: go
uses: actions/setup-go@v6
uses: actions/setup-go@v5
with:
cache-dependency-path: |
go.sum

View File

@@ -35,7 +35,7 @@ jobs:
uses: actions/checkout@v5
- name: Setup Node
uses: actions/setup-node@v6
uses: actions/setup-node@v4
with:
node-version: '24.x'

View File

@@ -3,7 +3,7 @@
[![Latest Release](https://img.shields.io/github/v/release/VictoriaMetrics/VictoriaMetrics?sort=semver&label=&filter=!*-victorialogs&logo=github&labelColor=gray&color=gray&link=https%3A%2F%2Fgithub.com%2FVictoriaMetrics%2FVictoriaMetrics%2Freleases%2Flatest)](https://github.com/VictoriaMetrics/VictoriaMetrics/releases)
![Docker Pulls](https://img.shields.io/docker/pulls/victoriametrics/victoria-metrics?label=&logo=docker&logoColor=white&labelColor=2496ED&color=2496ED&link=https%3A%2F%2Fhub.docker.com%2Fr%2Fvictoriametrics%2Fvictoria-metrics)
[![Go Report](https://goreportcard.com/badge/github.com/VictoriaMetrics/VictoriaMetrics?link=https%3A%2F%2Fgoreportcard.com%2Freport%2Fgithub.com%2FVictoriaMetrics%2FVictoriaMetrics)](https://goreportcard.com/report/github.com/VictoriaMetrics/VictoriaMetrics)
[![Build Status](https://github.com/VictoriaMetrics/VictoriaMetrics/actions/workflows/build.yml/badge.svg?branch=master&link=https%3A%2F%2Fgithub.com%2FVictoriaMetrics%2FVictoriaMetrics%2Factions)](https://github.com/VictoriaMetrics/VictoriaMetrics/actions/workflows/build.yml)
[![Build Status](https://github.com/VictoriaMetrics/VictoriaMetrics/actions/workflows/main.yml/badge.svg?branch=master&link=https%3A%2F%2Fgithub.com%2FVictoriaMetrics%2FVictoriaMetrics%2Factions)](https://github.com/VictoriaMetrics/VictoriaMetrics/actions/workflows/main.yml)
[![codecov](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/branch/master/graph/badge.svg?link=https%3A%2F%2Fcodecov.io%2Fgh%2FVictoriaMetrics%2FVictoriaMetrics)](https://app.codecov.io/gh/VictoriaMetrics/VictoriaMetrics)
[![License](https://img.shields.io/github/license/VictoriaMetrics/VictoriaMetrics?labelColor=green&label=&link=https%3A%2F%2Fgithub.com%2FVictoriaMetrics%2FVictoriaMetrics%2Fblob%2Fmaster%2FLICENSE)](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/LICENSE)
![Slack](https://img.shields.io/badge/Join-4A154B?logo=slack&link=https%3A%2F%2Fslack.victoriametrics.com)

View File

@@ -4,11 +4,12 @@
The following versions of VictoriaMetrics receive regular security fixes:
| Version | Supported |
|--------------------------------------------------------------------------------|--------------------|
| [Latest release](https://docs.victoriametrics.com/victoriametrics/changelog/) | :white_check_mark: |
| [LTS releases](https://docs.victoriametrics.com/victoriametrics/lts-releases/) | :white_check_mark: |
| other releases | :x: |
| Version | Supported |
|---------|--------------------|
| [latest release](https://docs.victoriametrics.com/victoriametrics/changelog/) | :white_check_mark: |
| v1.102.x [LTS line](https://docs.victoriametrics.com/victoriametrics/lts-releases/) | :white_check_mark: |
| v1.110.x [LTS line](https://docs.victoriametrics.com/victoriametrics/lts-releases/) | :white_check_mark: |
| other releases | :x: |
See [this page](https://victoriametrics.com/security/) for more details.

View File

@@ -169,7 +169,7 @@ func usage() {
const s = `
victoria-metrics is a time series database and monitoring solution.
See the docs at https://docs.victoriametrics.com/victoriametrics/
See the docs at https://docs.victoriametrics.com/
`
flagutil.Usage(s)
}

View File

@@ -111,7 +111,6 @@ func main() {
flag.CommandLine.SetOutput(os.Stdout)
flag.Usage = usage
envflag.Parse()
flagutil.ApplySecretFlags()
remotewrite.InitSecretFlags()
buildinfo.Init()
logger.Init()

View File

@@ -2,7 +2,6 @@ package opentelemetry
import (
"fmt"
"io"
"net/http"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmagent/common"
@@ -25,13 +24,6 @@ var (
rowsPerInsert = metrics.NewHistogram(`vmagent_rows_per_insert{type="opentelemetry"}`)
)
// InsertHandler processes metrics from given reader.
func InsertHandlerForReader(at *auth.Token, r io.Reader, encoding string) error {
return stream.ParseStream(r, encoding, nil, func(tss []prompb.TimeSeries, mms []prompb.MetricMetadata) error {
return insertRows(at, tss, mms, nil)
})
}
// InsertHandler processes opentelemetry metrics.
func InsertHandler(at *auth.Token, req *http.Request) error {
extraLabels, err := protoparserutil.GetExtraLabels(req)

View File

@@ -31,7 +31,7 @@ type Group struct {
// EvalDelay will adjust the `time` parameter of rule evaluation requests to compensate intentional query delay from datasource.
// see https://github.com/VictoriaMetrics/VictoriaMetrics/issues/5155
EvalDelay *promutil.Duration `yaml:"eval_delay,omitempty"`
Limit *int `yaml:"limit,omitempty"`
Limit int `yaml:"limit,omitempty"`
Rules []Rule `yaml:"rules"`
Concurrency int `yaml:"concurrency"`
// Labels is a set of label value pairs, that will be added to every rule.
@@ -91,8 +91,8 @@ func (g *Group) Validate(validateTplFn ValidateTplFn, validateExpressions bool)
if g.EvalOffset != nil && g.EvalDelay != nil {
return fmt.Errorf("eval_offset cannot be used with eval_delay")
}
if g.Limit != nil && *g.Limit < 0 {
return fmt.Errorf("invalid limit %d, shouldn't be less than 0", *g.Limit)
if g.Limit < 0 {
return fmt.Errorf("invalid limit %d, shouldn't be less than 0", g.Limit)
}
if g.Concurrency < 0 {
return fmt.Errorf("invalid concurrency %d, shouldn't be less than 0", g.Concurrency)

View File

@@ -181,10 +181,9 @@ func TestGroupValidate_Failure(t *testing.T) {
EvalOffset: promutil.NewDuration(2 * time.Minute),
}, false, "eval_offset should be smaller than interval")
limit := -1
f(&Group{
Name: "wrong limit",
Limit: &limit,
Limit: -1,
}, false, "invalid limit")
f(&Group{

View File

@@ -173,10 +173,9 @@ func (c *Client) Query(ctx context.Context, query string, ts time.Time) (Result,
return Result{}, nil, fmt.Errorf("second attempt: %w", err)
}
}
defer func() { _ = resp.Body.Close() }()
// Process the received response.
var parseFn func(resp *http.Response) (Result, error)
var parseFn func(req *http.Request, resp *http.Response) (Result, error)
switch c.dataSourceType {
case datasourcePrometheus:
parseFn = parsePrometheusResponse
@@ -187,12 +186,9 @@ func (c *Client) Query(ctx context.Context, query string, ts time.Time) (Result,
default:
logger.Panicf("BUG: unsupported datasource type %q to parse query response", c.dataSourceType)
}
result, err := parseFn(resp)
if err != nil {
return Result{}, nil, fmt.Errorf("error parsing response from %q: %w", req.URL.Redacted(), err)
}
return result, req, nil
result, err := parseFn(req, resp)
_ = resp.Body.Close()
return result, req, err
}
// QueryRange executes the given query on the given time range.
@@ -233,10 +229,9 @@ func (c *Client) QueryRange(ctx context.Context, query string, start, end time.T
return res, fmt.Errorf("second attempt: %w", err)
}
}
defer func() { _ = resp.Body.Close() }()
// Process the received response.
var parseFn func(resp *http.Response) (Result, error)
var parseFn func(req *http.Request, resp *http.Response) (Result, error)
switch c.dataSourceType {
case datasourcePrometheus:
parseFn = parsePrometheusResponse
@@ -245,11 +240,8 @@ func (c *Client) QueryRange(ctx context.Context, query string, start, end time.T
default:
logger.Panicf("BUG: unsupported datasource type %q to parse query range response", c.dataSourceType)
}
res, err = parseFn(resp)
if err != nil {
return Result{}, fmt.Errorf("error parsing response from %q: %w", req.URL.Redacted(), err)
}
res, err = parseFn(req, resp)
_ = resp.Body.Close()
return res, err
}

View File

@@ -33,10 +33,10 @@ func (r graphiteResponse) metrics() []Metric {
return ms
}
func parseGraphiteResponse(resp *http.Response) (Result, error) {
func parseGraphiteResponse(req *http.Request, resp *http.Response) (Result, error) {
r := &graphiteResponse{}
if err := json.NewDecoder(resp.Body).Decode(r); err != nil {
return Result{}, fmt.Errorf("error parsing graphite metrics: %w", err)
return Result{}, fmt.Errorf("error parsing graphite metrics for %s: %w", req.URL.Redacted(), err)
}
return Result{Data: r.metrics()}, nil
}

View File

@@ -172,16 +172,16 @@ const (
rtVector, rtMatrix, rScalar = "vector", "matrix", "scalar"
)
func parsePrometheusResponse(resp *http.Response) (res Result, err error) {
func parsePrometheusResponse(req *http.Request, resp *http.Response) (res Result, err error) {
r := &promResponse{}
if err = json.NewDecoder(resp.Body).Decode(r); err != nil {
return res, fmt.Errorf("failed to decode response: %w", err)
return res, fmt.Errorf("error parsing response from %s: %w", req.URL.Redacted(), err)
}
if r.Status == statusError {
return res, fmt.Errorf("response error %q: %s", r.ErrorType, r.Error)
return res, fmt.Errorf("response error, query: %s, errorType: %s, error: %s", req.URL.Redacted(), r.ErrorType, r.Error)
}
if r.Status != statusSuccess {
return res, fmt.Errorf("unknown response status %q", r.Status)
return res, fmt.Errorf("unknown status: %s, Expected success or error", r.Status)
}
var parseFn func() ([]Metric, error)
switch r.Data.ResultType {

View File

@@ -135,7 +135,7 @@ func TestVMInstantQuery(t *testing.T) {
expErr(vmQuery, "500") // 0
expErr(vmQuery, "error parsing response") // 1
expErr(vmQuery, "response error") // 2
expErr(vmQuery, "unknown response status") // 3
expErr(vmQuery, "unknown status") // 3
expErr(vmQuery, "unexpected end of JSON input") // 4
res, _, err := pq.Query(ctx, vmQuery, ts) // 5 - vector

View File

@@ -40,8 +40,8 @@ func (c *Client) setVLogsRangeReqParams(r *http.Request, query string, start, en
c.setReqParams(r, query)
}
func parseVLogsResponse(resp *http.Response) (res Result, err error) {
res, err = parsePrometheusResponse(resp)
func parseVLogsResponse(req *http.Request, resp *http.Response) (res Result, err error) {
res, err = parsePrometheusResponse(req, resp)
if err != nil {
return Result{}, err
}

View File

@@ -90,7 +90,6 @@ func main() {
flag.CommandLine.SetOutput(os.Stdout)
flag.Usage = usage
envflag.Parse()
flagutil.ApplySecretFlags()
remoteread.InitSecretFlags()
remotewrite.InitSecretFlags()
datasource.InitSecretFlags()

View File

@@ -38,7 +38,7 @@ func (m *manager) groupAPI(gID uint64) (*rule.ApiGroup, error) {
if !ok {
return nil, fmt.Errorf("can't find group with id %d", gID)
}
return g.ToAPI(), nil
return rule.GroupToAPI(g), nil
}
// ruleAPI generates apiRule object from alert by its ID(hash)
@@ -52,7 +52,7 @@ func (m *manager) ruleAPI(gID, rID uint64) (rule.ApiRule, error) {
}
for _, r := range g.Rules {
if r.ID() == rID {
return r.ToAPI(), nil
return rule.RuleToAPI(r), nil
}
}
return rule.ApiRule{}, fmt.Errorf("can't find rule with id %d in group %q", rID, g.Name)
@@ -72,7 +72,7 @@ func (m *manager) alertAPI(gID, aID uint64) (*rule.ApiAlert, error) {
if !ok {
continue
}
if apiAlert := ar.AlertToAPI(aID); apiAlert != nil {
if apiAlert := rule.AlertToAPI(ar, aID); apiAlert != nil {
return apiAlert, nil
}
}

View File

@@ -187,54 +187,6 @@ func (ar *AlertingRule) ID() uint64 {
return ar.RuleID
}
// ToAPI returns ApiRule representation of ar
func (ar *AlertingRule) ToAPI() ApiRule {
state := ar.state
lastState := state.getLast()
r := ApiRule{
Type: TypeAlerting,
DatasourceType: ar.Type.String(),
Name: ar.Name,
Query: ar.Expr,
Duration: ar.For.Seconds(),
KeepFiringFor: ar.KeepFiringFor.Seconds(),
Labels: ar.Labels,
Annotations: ar.Annotations,
LastEvaluation: lastState.Time,
EvaluationTime: lastState.Duration.Seconds(),
Health: "ok",
State: "inactive",
Alerts: ar.AlertsToAPI(),
LastSamples: lastState.Samples,
LastSeriesFetched: lastState.SeriesFetched,
MaxUpdates: state.size(),
Updates: state.getAll(),
Debug: ar.Debug,
// encode as strings to avoid rounding in JSON
ID: fmt.Sprintf("%d", ar.ID()),
GroupID: fmt.Sprintf("%d", ar.GroupID),
GroupName: ar.GroupName,
File: ar.File,
}
if lastState.Err != nil {
r.LastError = lastState.Err.Error()
r.Health = "err"
}
// satisfy apiRule.State logic
if len(r.Alerts) > 0 {
r.State = notifier.StatePending.String()
stateFiring := notifier.StateFiring.String()
for _, a := range r.Alerts {
if a.State == stateFiring {
r.State = stateFiring
break
}
}
}
return r
}
// GetAlerts returns active alerts of rule
func (ar *AlertingRule) GetAlerts() []*notifier.Alert {
ar.alertsMu.RLock()
@@ -389,7 +341,7 @@ func (ar *AlertingRule) execRange(ctx context.Context, start, end time.Time) ([]
return []datasource.Metric{{Timestamps: []int64{0}, Values: []float64{math.NaN()}}}, nil
}
for _, s := range res.Data {
ls, err := ar.expandLabelTemplates(s, qFn)
ls, err := ar.expandLabelTemplates(s)
if err != nil {
return nil, err
}
@@ -482,7 +434,7 @@ func (ar *AlertingRule) exec(ctx context.Context, ts time.Time, limit int) ([]pr
expandedLabels := make([]*labelSet, len(res.Data))
expandedAnnotations := make([]map[string]string, len(res.Data))
for i, m := range res.Data {
ls, err := ar.expandLabelTemplates(m, qFn)
ls, err := ar.expandLabelTemplates(m)
if err != nil {
curState.Err = err
return nil, curState.Err
@@ -604,7 +556,10 @@ func (ar *AlertingRule) exec(ctx context.Context, ts time.Time, limit int) ([]pr
return append(tss, ar.toTimeSeries(ts.Unix())...), nil
}
func (ar *AlertingRule) expandLabelTemplates(m datasource.Metric, qFn templates.QueryFn) (*labelSet, error) {
func (ar *AlertingRule) expandLabelTemplates(m datasource.Metric) (*labelSet, error) {
qFn := func(_ string) ([]datasource.Metric, error) {
return nil, fmt.Errorf("`query` template isn't supported in rule label")
}
ls, err := ar.toLabels(m, qFn)
if err != nil {
return nil, fmt.Errorf("failed to expand label templates: %s", err)

View File

@@ -10,7 +10,6 @@ import (
"strings"
"sync"
"testing"
"testing/synctest"
"time"
"github.com/VictoriaMetrics/metrics"
@@ -1430,142 +1429,3 @@ func TestAlertingRuleExec_Partial(t *testing.T) {
t.Fatalf("unexpected error: %s", err)
}
}
func TestAlertingRule_QueryTemplateInLabels(t *testing.T) {
fq := &datasource.FakeQuerier{}
fakeGroup := Group{
Name: "TestQueryTemplateInLabels",
}
ar := &AlertingRule{
Name: "test_alert",
Labels: map[string]string{
"suppress_for_mass_alert": `{{ if (printf "ALERTS{alertname='SomeAlert', alertstate='firing', device='%s'} == 1" $labels.device | query) }}true{{ else }}false{{ end }}`,
},
Annotations: map[string]string{
"summary": "Test alert with query template in labels",
},
alerts: make(map[uint64]*notifier.Alert),
}
ar.GroupID = fakeGroup.GetID()
ar.q = fq
ar.state = &ruleState{
entries: make([]StateEntry, 10),
}
// Add a metric that should trigger the alert
fq.Add(metricWithValueAndLabels(t, 1, "device", "sda1"))
ts := time.Now()
_, err := ar.exec(context.TODO(), ts, 0)
if err != nil {
t.Fatalf("unexpected error with query template in labels: %s", err)
}
// Verify that the alert was created and the query template was executed
if len(ar.alerts) != 1 {
t.Fatalf("expected 1 alert, got %d", len(ar.alerts))
}
alert := ar.GetAlerts()[0]
suppressLabel, exists := alert.Labels["suppress_for_mass_alert"]
if !exists {
t.Fatalf("expected 'suppress_for_mass_alert' label to exist")
}
// The query template should have been executed (even if it returns false due to mock data)
if suppressLabel != "true" && suppressLabel != "false" {
t.Fatalf("expected 'suppress_for_mass_alert' label to be 'true' or 'false', got '%s'", suppressLabel)
}
}
// TestAlertingRule_ActiveAtPreservedInAnnotations ensures that the fix for
// https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9543 is preserved
// while allowing query templates in labels (https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9783)
func TestAlertingRule_ActiveAtPreservedInAnnotations(t *testing.T) {
// wrap into synctest because of time manipulations
synctest.Test(t, func(t *testing.T) {
fq := &datasource.FakeQuerier{}
ar := &AlertingRule{
Name: "TestActiveAtPreservation",
Labels: map[string]string{
"test_query_in_label": `{{ "static_value" }}`,
},
Annotations: map[string]string{
"description": "Alert active since {{ $activeAt }}",
},
alerts: make(map[uint64]*notifier.Alert),
q: fq,
state: &ruleState{
entries: make([]StateEntry, 10),
},
}
// Mock query result - return empty result to make suppress_for_mass_alert = false
// (no need to add anything to fq for empty result)
// Add a metric that should trigger the alert
fq.Add(metricWithValueAndLabels(t, 1, "instance", "server1"))
// First execution - creates new alert
ts1 := time.Now()
_, err := ar.exec(context.TODO(), ts1, 0)
if err != nil {
t.Fatalf("unexpected error on first exec: %s", err)
}
if len(ar.alerts) != 1 {
t.Fatalf("expected 1 alert, got %d", len(ar.alerts))
}
firstAlert := ar.GetAlerts()[0]
// Verify first execution: activeAt should be ts1 and annotation should reflect it
if !firstAlert.ActiveAt.Equal(ts1) {
t.Fatalf("expected activeAt to be %v, got %v", ts1, firstAlert.ActiveAt)
}
// Extract time from annotation (format will be like "Alert active since 2025-09-30 08:55:13.638551611 -0400 EDT m=+0.002928464")
expectedTimeStr := ts1.Format("2006-01-02 15:04:05")
if !strings.Contains(firstAlert.Annotations["description"], expectedTimeStr) {
t.Fatalf("first exec annotation should contain time %s, got: %s", expectedTimeStr, firstAlert.Annotations["description"])
}
// Second execution - should preserve activeAt in annotation
// Ensure different timestamp with different seconds
// sleep is non-blocking thanks to synctest
time.Sleep(2 * time.Second)
ts2 := time.Now()
_, err = ar.exec(context.TODO(), ts2, 0)
if err != nil {
t.Fatalf("unexpected error on second exec: %s", err)
}
// Get the alert again (should be the same alert)
if len(ar.alerts) != 1 {
t.Fatalf("expected 1 alert, got %d", len(ar.alerts))
}
secondAlert := ar.GetAlerts()[0]
// Critical test: activeAt should still be ts1, not ts2
if !secondAlert.ActiveAt.Equal(ts1) {
t.Fatalf("activeAt should be preserved as %v, but got %v", ts1, secondAlert.ActiveAt)
}
// Critical test: annotation should still contain ts1 time, not ts2
if !strings.Contains(secondAlert.Annotations["description"], expectedTimeStr) {
t.Fatalf("second exec annotation should still contain original time %s, got: %s", expectedTimeStr, secondAlert.Annotations["description"])
}
// Additional verification: annotation should NOT contain ts2 time
ts2TimeStr := ts2.Format("2006-01-02 15:04:05")
if strings.Contains(secondAlert.Annotations["description"], ts2TimeStr) {
t.Fatalf("annotation should NOT contain new eval time %s, got: %s", ts2TimeStr, secondAlert.Annotations["description"])
}
// Verify query template in labels still works (this would fail if query templates were broken)
if firstAlert.Labels["test_query_in_label"] != "static_value" {
t.Fatalf("expected test_query_in_label=static_value, got %s", firstAlert.Labels["test_query_in_label"])
}
})
}

View File

@@ -24,10 +24,6 @@ import (
)
var (
ruleResultsLimit = flag.Int("rule.resultsLimit", 0, "Limits the number of alerts or recording results a single rule can produce. "+
"Can be overridden by the limit option under group if specified. "+
"If exceeded, the rule will be marked with an error and all its results will be discarded. "+
"0 means no limit.")
ruleUpdateEntriesLimit = flag.Int("rule.updateEntriesLimit", 20, "Defines the max number of rule's state updates stored in-memory. "+
"Rule's updates are available on rule's Details page and are used for debugging purposes. The number of stored updates can be overridden per rule via update_entries_limit param.")
resendDelay = flag.Duration("rule.resendDelay", 0, "MiniMum amount of time to wait before resending an alert to notifier.")
@@ -115,6 +111,7 @@ func NewGroup(cfg config.Group, qb datasource.QuerierBuilder, defaultInterval ti
Name: cfg.Name,
File: cfg.File,
Interval: cfg.Interval.Duration(),
Limit: cfg.Limit,
Concurrency: cfg.Concurrency,
checksum: cfg.Checksum,
Params: cfg.Params,
@@ -131,11 +128,6 @@ func NewGroup(cfg config.Group, qb datasource.QuerierBuilder, defaultInterval ti
if g.Interval == 0 {
g.Interval = defaultInterval
}
if cfg.Limit != nil {
g.Limit = *cfg.Limit
} else {
g.Limit = *ruleResultsLimit
}
if g.Concurrency < 1 {
g.Concurrency = 1
}
@@ -296,7 +288,7 @@ func (g *Group) InterruptEval() {
}
}
// Close stops the group and its rules, unregisters group metrics
// Close stops the group and it's rules, unregisters group metrics
func (g *Group) Close() {
if g.doneCh == nil {
return
@@ -305,6 +297,10 @@ func (g *Group) Close() {
g.InterruptEval()
<-g.finishedCh
g.closeGroupMetrics()
}
func (g *Group) closeGroupMetrics() {
metrics.UnregisterSet(g.metrics.set, true)
}
@@ -334,7 +330,7 @@ func (g *Group) Start(ctx context.Context, nts func() []notifier.Notifier, rw re
defer func() { close(g.finishedCh) }()
evalTS := time.Now()
// sleep random duration to spread group rules evaluation
// over time to reduce the load on datasource.
// over time in order to reduce load on datasource.
if !SkipRandSleepOnGroupStart {
sleepBeforeStart := delayBeforeStart(evalTS, g.GetID(), g.Interval, g.EvalOffset)
g.infof("will start in %v", sleepBeforeStart)

View File

@@ -81,37 +81,6 @@ func (rr *RecordingRule) ID() uint64 {
return rr.RuleID
}
// ToAPI returns ApiRule representation of rr
func (rr *RecordingRule) ToAPI() ApiRule {
state := rr.state
lastState := state.getLast()
r := ApiRule{
Type: TypeRecording,
DatasourceType: rr.Type.String(),
Name: rr.Name,
Query: rr.Expr,
Labels: rr.Labels,
LastEvaluation: lastState.Time,
EvaluationTime: lastState.Duration.Seconds(),
Health: "ok",
LastSamples: lastState.Samples,
LastSeriesFetched: lastState.SeriesFetched,
MaxUpdates: state.size(),
Updates: state.getAll(),
// encode as strings to avoid rounding
ID: fmt.Sprintf("%d", rr.ID()),
GroupID: fmt.Sprintf("%d", rr.GroupID),
GroupName: rr.GroupName,
File: rr.File,
}
if lastState.Err != nil {
r.LastError = lastState.Err.Error()
r.Health = "err"
}
return r
}
// NewRecordingRule creates a new RecordingRule
func NewRecordingRule(qb datasource.QuerierBuilder, group *Group, cfg config.Rule) *RecordingRule {
debug := group.Debug

View File

@@ -21,8 +21,6 @@ type Rule interface {
// ID returns unique ID that may be used for
// identifying this Rule among others.
ID() uint64
// ToAPI returns ApiRule representation of Rule
ToAPI() ApiRule
// exec executes the rule with given context at the given timestamp and limit.
// returns an err if number of resulting time series exceeds the limit.
exec(ctx context.Context, ts time.Time, limit int) ([]prompb.TimeSeries, error)
@@ -70,6 +68,39 @@ type StateEntry struct {
Curl string `json:"curl"`
}
// GetLastEntry returns latest stateEntry of rule
func GetLastEntry(r Rule) StateEntry {
if rule, ok := r.(*AlertingRule); ok {
return rule.state.getLast()
}
if rule, ok := r.(*RecordingRule); ok {
return rule.state.getLast()
}
return StateEntry{}
}
// GetRuleStateSize returns size of rule stateEntry
func GetRuleStateSize(r Rule) int {
if rule, ok := r.(*AlertingRule); ok {
return rule.state.size()
}
if rule, ok := r.(*RecordingRule); ok {
return rule.state.size()
}
return 0
}
// GetAllRuleState returns rule entire stateEntries
func GetAllRuleState(r Rule) []StateEntry {
if rule, ok := r.(*AlertingRule); ok {
return rule.state.getAll()
}
if rule, ok := r.(*RecordingRule); ok {
return rule.state.getAll()
}
return []StateEntry{}
}
func (s *ruleState) size() int {
s.RLock()
defer s.RUnlock()

View File

@@ -18,10 +18,8 @@ const (
// ParamRuleID is rule id key in url parameter
ParamRuleID = "rule_id"
// TypeRecording is a RecordingRule type
TypeRecording = "recording"
// TypeAlerting is an AlertingRule type
TypeAlerting = "alerting"
RuleTypeRecording = "recording"
RuleTypeAlerting = "alerting"
)
// ApiGroup represents a Group for web view
@@ -197,8 +195,94 @@ func (ar ApiRule) WebLink() string {
ParamGroupID, ar.GroupID, ParamRuleID, ar.ID)
}
// AlertsToAPI returns list of ApiAlert objects from existing alerts
func (ar *AlertingRule) AlertsToAPI() []*ApiAlert {
func RuleToAPI(r any) ApiRule {
if ar, ok := r.(*AlertingRule); ok {
return alertingToAPI(ar)
}
if rr, ok := r.(*RecordingRule); ok {
return recordingToAPI(rr)
}
return ApiRule{}
}
func recordingToAPI(rr *RecordingRule) ApiRule {
lastState := GetLastEntry(rr)
r := ApiRule{
Type: RuleTypeRecording,
DatasourceType: rr.Type.String(),
Name: rr.Name,
Query: rr.Expr,
Labels: rr.Labels,
LastEvaluation: lastState.Time,
EvaluationTime: lastState.Duration.Seconds(),
Health: "ok",
LastSamples: lastState.Samples,
LastSeriesFetched: lastState.SeriesFetched,
MaxUpdates: GetRuleStateSize(rr),
Updates: GetAllRuleState(rr),
// encode as strings to avoid rounding
ID: fmt.Sprintf("%d", rr.ID()),
GroupID: fmt.Sprintf("%d", rr.GroupID),
GroupName: rr.GroupName,
File: rr.File,
}
if lastState.Err != nil {
r.LastError = lastState.Err.Error()
r.Health = "err"
}
return r
}
// alertingToAPI returns Rule representation in form of ApiRule
func alertingToAPI(ar *AlertingRule) ApiRule {
lastState := GetLastEntry(ar)
r := ApiRule{
Type: RuleTypeAlerting,
DatasourceType: ar.Type.String(),
Name: ar.Name,
Query: ar.Expr,
Duration: ar.For.Seconds(),
KeepFiringFor: ar.KeepFiringFor.Seconds(),
Labels: ar.Labels,
Annotations: ar.Annotations,
LastEvaluation: lastState.Time,
EvaluationTime: lastState.Duration.Seconds(),
Health: "ok",
State: "inactive",
Alerts: RuleToAPIAlert(ar),
LastSamples: lastState.Samples,
LastSeriesFetched: lastState.SeriesFetched,
MaxUpdates: GetRuleStateSize(ar),
Updates: GetAllRuleState(ar),
Debug: ar.Debug,
// encode as strings to avoid rounding in JSON
ID: fmt.Sprintf("%d", ar.ID()),
GroupID: fmt.Sprintf("%d", ar.GroupID),
GroupName: ar.GroupName,
File: ar.File,
}
if lastState.Err != nil {
r.LastError = lastState.Err.Error()
r.Health = "err"
}
// satisfy apiRule.State logic
if len(r.Alerts) > 0 {
r.State = notifier.StatePending.String()
stateFiring := notifier.StateFiring.String()
for _, a := range r.Alerts {
if a.State == stateFiring {
r.State = stateFiring
break
}
}
}
return r
}
// RuleToAPIAlert generates list of apiAlert objects from existing alerts
func RuleToAPIAlert(ar *AlertingRule) []*ApiAlert {
var alerts []*ApiAlert
for _, a := range ar.GetAlerts() {
if a.State == notifier.StateInactive {
@@ -210,7 +294,7 @@ func (ar *AlertingRule) AlertsToAPI() []*ApiAlert {
}
// AlertToAPI generates apiAlert object from alert by its id(hash)
func (ar *AlertingRule) AlertToAPI(id uint64) *ApiAlert {
func AlertToAPI(ar *AlertingRule, id uint64) *ApiAlert {
a := ar.GetAlert(id)
if a == nil {
return nil
@@ -244,8 +328,7 @@ func NewAlertAPI(ar *AlertingRule, a *notifier.Alert) *ApiAlert {
return aa
}
// ToAPI returns ApiGroup representation of g
func (g *Group) ToAPI() *ApiGroup {
func GroupToAPI(g *Group) *ApiGroup {
g.mu.RLock()
defer g.mu.RUnlock()
ag := ApiGroup{
@@ -270,7 +353,7 @@ func (g *Group) ToAPI() *ApiGroup {
}
ag.Rules = make([]ApiRule, 0)
for _, r := range g.Rules {
ag.Rules = append(ag.Rules, r.ToAPI())
ag.Rules = append(ag.Rules, RuleToAPI(r))
}
return &ag
}

View File

@@ -37,7 +37,7 @@ func TestRecordingToApi(t *testing.T) {
Query: "up",
Labels: map[string]string{"label": "value"},
Health: "ok",
Type: TypeRecording,
Type: RuleTypeRecording,
DatasourceType: "prometheus",
ID: "1248",
GroupID: fmt.Sprintf("%d", g.CreateID()),
@@ -47,7 +47,7 @@ func TestRecordingToApi(t *testing.T) {
Updates: make([]StateEntry, 0),
}
res := rr.ToAPI()
res := recordingToAPI(rr)
if !reflect.DeepEqual(res, expectedRes) {
t.Fatalf("expected to have: \n%v;\ngot: \n%v", expectedRes, res)

View File

@@ -47,8 +47,8 @@ var (
{Name: "Docs", URL: "https://docs.victoriametrics.com/victoriametrics/vmalert/"},
}
ruleTypeMap = map[string]string{
"alert": rule.TypeAlerting,
"record": rule.TypeRecording,
"alert": rule.RuleTypeAlerting,
"record": rule.RuleTypeRecording,
}
)
@@ -347,7 +347,7 @@ func (rh *requestHandler) groups(rf *rulesFilter) []*rule.ApiGroup {
if !rf.matchesGroup(group) {
continue
}
g := group.ToAPI()
g := rule.GroupToAPI(group)
// the returned list should always be non-nil
// https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4221
filteredRules := make([]rule.ApiRule, 0)
@@ -419,11 +419,11 @@ func (rh *requestHandler) groupAlerts() []rule.GroupAlerts {
if !ok {
continue
}
alerts = append(alerts, a.AlertsToAPI()...)
alerts = append(alerts, rule.RuleToAPIAlert(a)...)
}
if len(alerts) > 0 {
gAlerts = append(gAlerts, rule.GroupAlerts{
Group: g.ToAPI(),
Group: rule.GroupToAPI(g),
Alerts: alerts,
})
}
@@ -449,7 +449,7 @@ func (rh *requestHandler) listAlerts(rf *rulesFilter) ([]byte, error) {
if !ok {
continue
}
lr.Data.Alerts = append(lr.Data.Alerts, a.AlertsToAPI()...)
lr.Data.Alerts = append(lr.Data.Alerts, rule.RuleToAPIAlert(a)...)
}
}

View File

@@ -85,13 +85,13 @@ func TestHandler(t *testing.T) {
})
t.Run("/vmalert/rule", func(t *testing.T) {
a := ar.ToAPI()
a := rule.RuleToAPI(ar)
getResp(t, ts.URL+"/vmalert/"+a.WebLink(), nil, 200)
r := rr.ToAPI()
r := rule.RuleToAPI(rr)
getResp(t, ts.URL+"/vmalert/"+r.WebLink(), nil, 200)
})
t.Run("/vmalert/alert", func(t *testing.T) {
alerts := ar.AlertsToAPI()
alerts := rule.RuleToAPIAlert(ar)
for _, a := range alerts {
getResp(t, ts.URL+"/vmalert/"+a.WebLink(), nil, 200)
}
@@ -170,7 +170,7 @@ func TestHandler(t *testing.T) {
}
})
t.Run("/api/v1/rule?ruleID&groupID", func(t *testing.T) {
expRule := ar.ToAPI()
expRule := rule.RuleToAPI(ar)
gotRule := rule.ApiRule{}
getResp(t, ts.URL+"/"+expRule.APILink(), &gotRule, 200)
@@ -194,7 +194,7 @@ func TestHandler(t *testing.T) {
t.Run("/api/v1/group?groupID", func(t *testing.T) {
id := groupIDs[0]
g := m.groups[id]
expGroup := g.ToAPI()
expGroup := rule.GroupToAPI(g)
gotGroup := rule.ApiGroup{}
getResp(t, ts.URL+"/"+expGroup.APILink(), &gotGroup, 200)
if expGroup.ID != gotGroup.ID {

View File

@@ -372,54 +372,20 @@ func tryProcessingRequest(w http.ResponseWriter, r *http.Request, targetURL *url
updateHeadersByConfig(w.Header(), hc.ResponseHeaders)
w.WriteHeader(res.StatusCode)
err = copyStreamToClient(w, res.Body)
copyBuf := copyBufPool.Get()
copyBuf.B = bytesutil.ResizeNoCopyNoOverallocate(copyBuf.B, 16*1024)
_, err = io.CopyBuffer(w, res.Body, copyBuf.B)
copyBufPool.Put(copyBuf)
_ = res.Body.Close()
if err != nil && !netutil.IsTrivialNetworkError(err) && !errors.Is(err, context.Canceled) {
if err != nil && !netutil.IsTrivialNetworkError(err) {
remoteAddr := httpserver.GetQuotedRemoteAddr(r)
requestURI := httpserver.GetRequestURI(r)
logger.Warnf("remoteAddr: %s; requestURI: %s; error when proxying response body from %s: %s", remoteAddr, requestURI, targetURL, err)
return true, false
}
return true, false
}
func copyStreamToClient(client io.Writer, backend io.Reader) error {
copyBuf := copyBufPool.Get()
copyBuf.B = bytesutil.ResizeNoCopyNoOverallocate(copyBuf.B, 16*1024)
defer copyBufPool.Put(copyBuf)
buf := copyBuf.B
flusher, ok := client.(http.Flusher)
if !ok {
logger.Panicf("BUG: client must implement net/http.Flusher interface; got %T", client)
}
for {
n, backendErr := backend.Read(buf)
if n > 0 {
data := buf[:n]
n, clientErr := client.Write(data)
if clientErr != nil {
return fmt.Errorf("cannot write data to client: %w", clientErr)
}
if n != len(data) {
logger.Panicf("BUG: unexpected number of bytes written returned by client.Write; got %d; want %d", n, len(data))
}
// Flush the read data from the backend to the client as fast as possible
// in order to reduce delays for data propagation.
// See https://github.com/VictoriaMetrics/VictoriaLogs/issues/667
flusher.Flush()
}
if backendErr != nil {
if backendErr == io.EOF {
return nil
}
return fmt.Errorf("cannot read data from backend: %w", backendErr)
}
}
}
var copyBufPool bytesutil.ByteBufferPool
func copyHeader(dst, src http.Header) {

View File

@@ -514,11 +514,6 @@ func (w *fakeResponseWriter) getResponse() string {
return w.bb.String()
}
// Flush implements net/http.Flusher
func (w *fakeResponseWriter) Flush() {
// Nothing to do.
}
func (w *fakeResponseWriter) Header() http.Header {
if w.h == nil {
w.h = http.Header{}

View File

@@ -115,7 +115,7 @@ func main() {
if err != nil {
logger.Fatalf("cannot create backup: %s", err)
}
pushmetrics.StopAndPush()
pushmetrics.Stop()
startTime := time.Now()
logger.Infof("gracefully shutting down http server for metrics at %q", listenAddrs)

View File

@@ -68,7 +68,7 @@ func main() {
if err := a.Run(ctx); err != nil {
logger.Fatalf("cannot restore from backup: %s", err)
}
pushmetrics.StopAndPush()
pushmetrics.Stop()
srcFS.MustStop()
dstFS.MustStop()

View File

@@ -197,13 +197,13 @@ func newNextSeriesForSearchQuery(ec *evalConfig, sq *storage.SearchQuery, expr g
}
s.summarize(aggrAvg, ec.startTime, ec.endTime, ec.storageStep, 0)
t := timerpool.Get(30 * time.Second)
defer timerpool.Put(t)
select {
case seriesCh <- s:
case <-t.C:
logger.Errorf("resource leak when processing the %s (full query: %s); please report this error to VictoriaMetrics developers",
expr.AppendString(nil), ec.originalQuery)
}
timerpool.Put(t)
return nil
})
close(seriesCh)

View File

@@ -11,8 +11,6 @@ import (
"strings"
"time"
"github.com/VictoriaMetrics/metrics"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/graphite"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/netstorage"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/prometheus"
@@ -20,7 +18,6 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/searchutil"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/stats"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmstorage"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/buildinfo"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/cgroup"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fs"
@@ -30,6 +27,7 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/querytracer"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/timerpool"
"github.com/VictoriaMetrics/metrics"
)
var (
@@ -742,7 +740,6 @@ var (
func initVMUIConfig() {
var cfg struct {
Version string `json:"version"`
License struct {
Type string `json:"type"`
} `json:"license"`
@@ -758,11 +755,6 @@ func initVMUIConfig() {
if err != nil {
logger.Fatalf("cannot parse vmui default config: %s", err)
}
cfg.Version = buildinfo.ShortVersion()
if cfg.Version == "" {
// buildinfo.ShortVersion() may return empty result for builds without tags
cfg.Version = buildinfo.Version
}
cfg.VMAlert.Enabled = len(*vmalertProxyURL) != 0
data, err = json.Marshal(&cfg)
if err != nil {

View File

@@ -1150,23 +1150,15 @@ func evalInstantRollup(qt *querytracer.Tracer, ec *EvalConfig, funcName string,
}
qt.Printf("optimized calculation for instant rollup avg_over_time(m[d]) as (sum_over_time(m[d]) / count_over_time(m[d]))")
fe := expr.(*metricsql.FuncExpr)
// copy RollupExpr to drop possible offset,
// see https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9762
newArg := copyRollupExpr(fe.Args[0].(*metricsql.RollupExpr))
newArg.Offset = nil
feSum := *fe
feSum.Name = "sum_over_time"
feCount := *fe
feCount.Name = "count_over_time"
be := &metricsql.BinaryOpExpr{
Op: "/",
KeepMetricNames: fe.KeepMetricNames,
Left: &metricsql.FuncExpr{
Name: "sum_over_time",
Args: []metricsql.Expr{newArg},
KeepMetricNames: fe.KeepMetricNames,
},
Right: &metricsql.FuncExpr{
Name: "count_over_time",
Args: []metricsql.Expr{newArg},
KeepMetricNames: fe.KeepMetricNames,
},
Left: &feSum,
Right: &feCount,
}
return evalExpr(qt, ec, be)
case "rate":
@@ -1180,12 +1172,8 @@ func evalInstantRollup(qt *querytracer.Tracer, ec *EvalConfig, funcName string,
fe := afe.Args[0].(*metricsql.FuncExpr)
feIncrease := *fe
feIncrease.Name = "increase"
// copy RollupExpr to drop possible offset,
// see https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9762
newArg := copyRollupExpr(fe.Args[0].(*metricsql.RollupExpr))
newArg.Offset = nil
feIncrease.Args = []metricsql.Expr{newArg}
d := newArg.Window.Duration(ec.Step)
re := fe.Args[0].(*metricsql.RollupExpr)
d := re.Window.Duration(ec.Step)
if d == 0 {
d = ec.Step
}
@@ -1205,12 +1193,8 @@ func evalInstantRollup(qt *querytracer.Tracer, ec *EvalConfig, funcName string,
fe := expr.(*metricsql.FuncExpr)
feIncrease := *fe
feIncrease.Name = "increase"
// copy RollupExpr to drop possible offset,
// see https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9762
newArg := copyRollupExpr(fe.Args[0].(*metricsql.RollupExpr))
newArg.Offset = nil
feIncrease.Args = []metricsql.Expr{newArg}
d := newArg.Window.Duration(ec.Step)
re := fe.Args[0].(*metricsql.RollupExpr)
d := re.Window.Duration(ec.Step)
if d == 0 {
d = ec.Step
}
@@ -2015,23 +1999,3 @@ func dropStaleNaNs(funcName string, values []float64, timestamps []int64) ([]flo
}
return dstValues, dstTimestamps
}
func copyRollupExpr(re *metricsql.RollupExpr) *metricsql.RollupExpr {
var newRe metricsql.RollupExpr
newRe.Expr = re.Expr
newRe.InheritStep = re.InheritStep
newRe.At = re.At
if re.Window != nil {
newRe.Window = &metricsql.DurationExpr{}
*newRe.Window = *re.Window
}
if re.Offset != nil {
newRe.Offset = &metricsql.DurationExpr{}
*newRe.Offset = *re.Offset
}
if re.Step != nil {
newRe.Step = &metricsql.DurationExpr{}
*newRe.Step = *re.Step
}
return &newRe
}

View File

@@ -123,7 +123,7 @@ The list of MetricsQL features on top of PromQL:
* `if` binary operator. `q1 if q2` removes values from `q1` for missing values from `q2`.
* `ifnot` binary operator. `q1 ifnot q2` removes values from `q1` for existing values from `q2`.
* `WITH` templates. This feature simplifies writing and managing complex queries.
Go to [WITH templates playground](https://play.victoriametrics.com/select/0/prometheus/graph/#/expand-with-exprs) and try it.
Go to [WITH templates playground](https://play.victoriametrics.com/select/accounting/1/6a716b0f-38bc-4856-90ce-448fd713e3fe/expand-with-exprs) and try it.
* String literals may be concatenated. This is useful with `WITH` templates:
`WITH (commonPrefix="long_metric_prefix_") {__name__=commonPrefix+"suffix1"} / {__name__=commonPrefix+"suffix2"}`.
* `keep_metric_names` modifier can be applied to all the [rollup functions](#rollup-functions), [transform functions](#transform-functions)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -6,7 +6,6 @@
<link rel="apple-touch-icon" href="./favicon.svg"/>
<link rel="mask-icon" href="./favicon.svg" color="#000000">
<meta name="robots" content="noindex">
<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"/>
@@ -37,10 +36,10 @@
<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 type="module" crossorigin src="./assets/index-D13qGB62.js"></script>
<link rel="modulepreload" crossorigin href="./assets/vendor-DY9kCvzk.js">
<script type="module" crossorigin src="./assets/index-DK22yiEQ.js"></script>
<link rel="modulepreload" crossorigin href="./assets/vendor-DBOs1yKE.js">
<link rel="stylesheet" crossorigin href="./assets/vendor-D1GxaB_c.css">
<link rel="stylesheet" crossorigin href="./assets/index-I8MVeF75.css">
<link rel="stylesheet" crossorigin href="./assets/index-Ccv_zSYG.css">
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>

View File

@@ -1,4 +1,4 @@
FROM golang:1.25.3 AS build-web-stage
FROM golang:1.25.0 AS build-web-stage
COPY build /build
WORKDIR /build
@@ -6,7 +6,7 @@ COPY web/ /build/
RUN GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o web-amd64 github.com/VictoriMetrics/vmui/ && \
GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -o web-windows github.com/VictoriMetrics/vmui/
FROM alpine:3.22.2
FROM alpine:3.22.1
USER root
COPY --from=build-web-stage /build/web-amd64 /app/web

View File

@@ -79,13 +79,15 @@ export default [...compat.extends(
}],
"react/jsx-first-prop-new-line": [1, "multiline"],
"object-curly-spacing": [2, "always"],
// Disable core indent rule due to recursion issues in ESLint 9; use JSX-specific rules instead
indent: "off",
"react/jsx-indent": ["error", 2],
"react/jsx-indent-props": ["error", 2],
indent: ["error", 2, {
SwitchCase: 1,
}],
// Formatting rules moved out of ESLint core; omit here to avoid deprecation noise
"linebreak-style": ["error", "unix"],
quotes: ["error", "double"],
semi: ["error", "always"],
"react/prop-types": 0,
"react/react-in-jsx-scope": "off",

View File

@@ -6,7 +6,6 @@
<link rel="apple-touch-icon" href="/favicon.svg"/>
<link rel="mask-icon" href="/favicon.svg" color="#000000">
<meta name="robots" content="noindex">
<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"/>

View File

@@ -17,7 +17,7 @@
"react-input-mask": "^2.0.4",
"react-router-dom": "^7.6.3",
"uplot": "^1.6.32",
"vite": "^7.1.5",
"vite": "^7.0.4",
"web-vitals": "^5.0.3"
},
"devDependencies": {
@@ -7321,13 +7321,13 @@
"license": "MIT"
},
"node_modules/tinyglobby": {
"version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
"version": "0.2.14",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
"integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==",
"license": "MIT",
"dependencies": {
"fdir": "^6.5.0",
"picomatch": "^4.0.3"
"fdir": "^6.4.4",
"picomatch": "^4.0.2"
},
"engines": {
"node": ">=12.0.0"
@@ -7337,13 +7337,10 @@
}
},
"node_modules/tinyglobby/node_modules/fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"version": "6.4.6",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz",
"integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==",
"license": "MIT",
"engines": {
"node": ">=12.0.0"
},
"peerDependencies": {
"picomatch": "^3 || ^4"
},
@@ -7354,9 +7351,9 @@
}
},
"node_modules/tinyglobby/node_modules/picomatch": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"license": "MIT",
"engines": {
"node": ">=12"
@@ -7660,17 +7657,17 @@
"license": "MIT"
},
"node_modules/vite": {
"version": "7.1.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.5.tgz",
"integrity": "sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ==",
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.0.4.tgz",
"integrity": "sha512-SkaSguuS7nnmV7mfJ8l81JGBFV7Gvzp8IzgE8A8t23+AxuNX61Q5H1Tpz5efduSN7NHC8nQXD3sKQKZAu5mNEA==",
"license": "MIT",
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
"picomatch": "^4.0.3",
"fdir": "^6.4.6",
"picomatch": "^4.0.2",
"postcss": "^8.5.6",
"rollup": "^4.43.0",
"tinyglobby": "^0.2.15"
"rollup": "^4.40.0",
"tinyglobby": "^0.2.14"
},
"bin": {
"vite": "bin/vite.js"
@@ -7775,13 +7772,10 @@
}
},
"node_modules/vite/node_modules/fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"version": "6.4.6",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz",
"integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==",
"license": "MIT",
"engines": {
"node": ">=12.0.0"
},
"peerDependencies": {
"picomatch": "^3 || ^4"
},
@@ -7792,9 +7786,9 @@
}
},
"node_modules/vite/node_modules/picomatch": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"license": "MIT",
"engines": {
"node": ">=12"

View File

@@ -29,7 +29,7 @@
"react-input-mask": "^2.0.4",
"react-router-dom": "^7.6.3",
"uplot": "^1.6.32",
"vite": "^7.1.5",
"vite": "^7.0.4",
"web-vitals": "^5.0.3"
},
"devDependencies": {

View File

@@ -123,7 +123,7 @@ The list of MetricsQL features on top of PromQL:
* `if` binary operator. `q1 if q2` removes values from `q1` for missing values from `q2`.
* `ifnot` binary operator. `q1 ifnot q2` removes values from `q1` for existing values from `q2`.
* `WITH` templates. This feature simplifies writing and managing complex queries.
Go to [WITH templates playground](https://play.victoriametrics.com/select/0/prometheus/graph/#/expand-with-exprs) and try it.
Go to [WITH templates playground](https://play.victoriametrics.com/select/accounting/1/6a716b0f-38bc-4856-90ce-448fd713e3fe/expand-with-exprs) and try it.
* String literals may be concatenated. This is useful with `WITH` templates:
`WITH (commonPrefix="long_metric_prefix_") {__name__=commonPrefix+"suffix1"} / {__name__=commonPrefix+"suffix2"}`.
* `keep_metric_names` modifier can be applied to all the [rollup functions](#rollup-functions), [transform functions](#transform-functions)

View File

@@ -116,7 +116,7 @@ const LegendConfigs: FC<Props> = ({ data, isCompact }) => {
onEnter={onApplyFormat}
/>
<span className="vm-legend-configs-item__info vm-legend-configs-item__info_input">
Customize legend labels with text and &#123;&#123;label_name&#125;&#125; placeholders.
Customize legend labels with text and &#123;&#123;label_name&#125;&#125; placeholders.
</span>
</div>
@@ -130,7 +130,7 @@ const LegendConfigs: FC<Props> = ({ data, isCompact }) => {
searchable
/>
<span className="vm-legend-configs-item__info">
Choose a label to group the legend. By default, legends are grouped by query.
Choose a label to group the legend. By default, legends are grouped by query.
</span>
</div>
</>

View File

@@ -142,7 +142,7 @@ const StepConfigurator: FC = () => {
startIcon={<TimelineIcon/>}
onClick={toggleOpenOptions}
>
Step: {isAutoStep ? `auto (${customStep})` : customStep}
Step: {isAutoStep ? `auto (${customStep})` : customStep}
</Button>
)}
<Popper

View File

@@ -1,9 +1,8 @@
import { useMemo } from "preact/compat";
import "./style.scss";
import { Alert as APIAlert } from "../../../types";
import { createSearchParams } from "react-router-dom";
import Button from "../../Main/Button/Button";
import Badges, { BadgeColor } from "../Badges";
import Badges from "../Badges";
import {
SearchIcon,
} from "../../Main/Icons";
@@ -16,13 +15,6 @@ interface BaseAlertProps {
const BaseAlert = ({ item }: BaseAlertProps) => {
const query = item?.expression;
const alertLabels = item?.labels || {};
const alertLabelsItems = useMemo(() => {
return Object.fromEntries(Object.entries(alertLabels).map(([name, value]) => [name, {
color: "passive" as BadgeColor,
value: value,
}]));
}, [alertLabels]);
const openQueryLink = () => {
const params = {
@@ -35,10 +27,6 @@ const BaseAlert = ({ item }: BaseAlertProps) => {
return (
<div className="vm-explore-alerts-alert-item">
<table>
<colgroup>
<col className="vm-col-md"/>
<col/>
</colgroup>
<tbody>
<tr>
<td
@@ -57,7 +45,7 @@ const BaseAlert = ({ item }: BaseAlertProps) => {
</td>
</tr>
<tr>
<td>Query</td>
<td className="vm-col-md">Query</td>
<td>
<CodeExample
code={query}
@@ -65,15 +53,18 @@ const BaseAlert = ({ item }: BaseAlertProps) => {
</td>
</tr>
<tr>
<td>Active at</td>
<td className="vm-col-md">Active at</td>
<td>{dayjs(item.activeAt).format("DD MMM YYYY HH:mm:ss")}</td>
</tr>
{!!Object.keys(alertLabels).length && (
{!!Object.keys(item?.labels || {}).length && (
<tr>
<td>Labels</td>
<td className="vm-col-md">Labels</td>
<td>
<Badges
items={alertLabelsItems}
items={Object.fromEntries(Object.entries(item.labels).map(([name, value]) => [name, {
color: "passive",
value: value,
}]))}
/>
</td>
</tr>
@@ -84,14 +75,10 @@ const BaseAlert = ({ item }: BaseAlertProps) => {
<>
<span className="title">Annotations</span>
<table>
<colgroup>
<col className="vm-col-md"/>
<col/>
</colgroup>
<tbody>
{Object.entries(item.annotations || {}).map(([name, value]) => (
<tr key={name}>
<td>{name}</td>
<td className="vm-col-md">{name}</td>
<td>{value}</td>
</tr>
))}

View File

@@ -1,14 +1,16 @@
@use "src/styles/variables" as *;
.vm-modal.vm-explore-alerts {
.vm-modal-content {
overflow-y: scroll;
.vm-modal {
.vm-explore-alerts-alert-item {
table {
width: auto;
}
}
}
.vm-explore-alerts-alert-item {
row-gap: $padding-global;
margin: $padding-global;
margin-right: $padding-global;
display: flex;
flex-direction: column;
@@ -17,35 +19,36 @@
text-align: center;
}
.vm-col-sm {
width: 10%;
white-space: nowrap;
text-align: left;
overflow: hidden;
text-overflow: ellipsis;
}
.vm-col-md {
width: 20%;
width: 15%;
white-space: nowrap;
text-align: left;
overflow: hidden;
text-overflow: ellipsis;
}
.vm-button {
color: $color-passive;
border: 1px solid $color-passive;
}
.vm-code-example {
.vm-button {
background-color: $color-code;
}
border: 1px solid var(--color-passive);
}
table {
word-break: break-word;
table-layout: fixed;
width: 100%;
td, th {
line-height: 30px;
padding: 4px $padding-small;
white-space: nowrap;
text-align: left;
overflow: hidden;
text-overflow: ellipsis;
}
th {
font-weight: bold;
text-align: center;
padding: 0 $padding-small;
}
}

View File

@@ -1,45 +1,17 @@
import { useMemo } from "preact/compat";
import "./style.scss";
import { Group as APIGroup } from "../../../types";
import dayjs from "dayjs";
import { formatDuration } from "../helpers";
import Badges, { BadgeColor } from "../Badges";
import Badges from "../Badges";
interface BaseGroupProps {
group: APIGroup;
}
const BaseGroup = ({ group }: BaseGroupProps) => {
const groupLabels = group?.labels || {};
const groupLabelsItems = useMemo(() => {
return Object.fromEntries(Object.entries(groupLabels).map(([name, value]) => [name, {
color: "passive" as BadgeColor,
value: value,
}]));
}, [groupLabels]);
const groupParams = group?.params || [];
const groupParamsItems = useMemo(() => {
return Object.fromEntries(groupParams.map(value => [value, {
color: "passive" as BadgeColor,
}]));
}, [groupParams]);
const groupHeaders = group?.headers || [];
const groupHeadersItems = useMemo(() => {
return Object.fromEntries(groupHeaders.map(value => [value, {
color: "passive" as BadgeColor,
}]));
}, [groupHeaders]);
const groupNotifierHeaders = group?.notifier_headers || [];
const groupNotifierHeadersItems = useMemo(() => {
return Object.fromEntries(groupNotifierHeaders.map(value => [value, {
color: "passive" as BadgeColor,
}]));
}, [groupNotifierHeaders]);
return (
<div className="vm-explore-alerts-group">
<div></div>
<table>
<tbody>
{!!group.interval && (
@@ -78,42 +50,51 @@ const BaseGroup = ({ group }: BaseGroupProps) => {
<td>{group.concurrency}</td>
</tr>
)}
{!!Object.keys(groupLabels).length && (
{!!group?.labels?.length && (
<tr>
<td className="vm-col-md">Labels</td>
<td>
<Badges
items={groupLabelsItems}
items={Object.fromEntries(Object.entries(group.labels).map(([name, value]) => [name, {
color: "passive",
value: value,
}]))}
/>
</td>
</tr>
)}
{!!groupParams.length && (
{!!group?.params?.length && (
<tr>
<td className="vm-col-md">Params</td>
<td>
<Badges
items={groupParamsItems}
items={Object.fromEntries(group.params.map(value => [value, {
color: "passive",
}]))}
/>
</td>
</tr>
)}
{!!groupHeaders.length && (
{!!group?.headers?.length && (
<tr>
<td className="vm-col-md">Headers</td>
<td>
<Badges
items={groupHeadersItems}
items={Object.fromEntries(group.headers.map(value => [value, {
color: "passive",
}]))}
/>
</td>
</tr>
)}
{!!groupNotifierHeaders.length && (
{!!group?.notifier_headers?.length && (
<tr>
<td className="vm-col-md">Notifier headers</td>
<td>
<Badges
items={groupNotifierHeadersItems}
items={Object.fromEntries(group.notifier_headers.map(value => [value, {
color: "passive",
}]))}
/>
</td>
</tr>

View File

@@ -1,14 +1,16 @@
@use "src/styles/variables" as *;
.vm-modal.vm-explore-alerts {
.vm-modal-content {
overflow-y: scroll;
.vm-modal {
.vm-explore-alerts-group {
table {
width: auto;
}
}
}
.vm-explore-alerts-group {
row-gap: $padding-global;
margin: $padding-global;
margin-right: $padding-global;
display: flex;
flex-direction: column;
@@ -39,13 +41,24 @@
}
}
.vm-col-sm {
width: 10%;
white-space: nowrap;
text-align: left;
overflow: hidden;
text-overflow: ellipsis;
}
.vm-col-md {
width: 20%;
width: 15%;
white-space: nowrap;
text-align: left;
overflow: hidden;
text-overflow: ellipsis;
}
table {
width: 100%;
table-layout: fixed;
tr.hoverable {
cursor: pointer;
&:hover {
@@ -55,10 +68,6 @@
td, th {
line-height: 30px;
padding: 4px $padding-small;
white-space: nowrap;
text-align: left;
overflow: hidden;
text-overflow: ellipsis;
}
th {
font-weight: bold;

View File

@@ -1,4 +1,3 @@
import { useMemo } from "preact/compat";
import "./style.scss";
import { Rule as APIRule } from "../../../types";
import { useNavigate, createSearchParams } from "react-router-dom";
@@ -26,14 +25,6 @@ const BaseRule = ({ item }: BaseRuleProps) => {
};
};
const ruleLabels = item?.labels || {};
const ruleLabelsItems = useMemo(() => {
return Object.fromEntries(Object.entries(ruleLabels).map(([name, value]) => [name, {
color: "passive" as BadgeColor,
value: value,
}]));
}, [ruleLabels]);
const openQueryLink = () => {
const params = {
"g0.expr": query,
@@ -44,11 +35,8 @@ const BaseRule = ({ item }: BaseRuleProps) => {
return (
<div className="vm-explore-alerts-rule-item">
<div></div>
<table>
<colgroup>
<col className="vm-col-md"/>
<col/>
</colgroup>
<tbody>
<tr>
<td
@@ -67,7 +55,7 @@ const BaseRule = ({ item }: BaseRuleProps) => {
</td>
</tr>
<tr>
<td>Query</td>
<td className="vm-col-md">Query</td>
<td>
<CodeExample
code={query}
@@ -76,30 +64,33 @@ const BaseRule = ({ item }: BaseRuleProps) => {
</tr>
{!!item.duration && (
<tr>
<td>For</td>
<td className="vm-col-md">For</td>
<td>{formatDuration(item.duration)}</td>
</tr>
)}
{!!item.lastEvaluation && (
<tr>
<td>Last evaluation</td>
<td className="vm-col-md">Last evaluation</td>
<td>{dayjs(item.lastEvaluation).format("DD MMM YYYY HH:mm:ss")}</td>
</tr>
)}
{!!item.lastError && item.health !== "ok" && (
<tr>
<td>Last error</td>
<td className="vm-col-md">Last error</td>
<td>
<Alert variant="error">{item.lastError}</Alert>
</td>
</tr>
)}
{!!Object.keys(ruleLabelsItems).length && (
{!!Object.keys(item?.labels || {}).length && (
<tr>
<td>Labels</td>
<td className="vm-col-md">Labels</td>
<td>
<Badges
items={ruleLabelsItems}
items={Object.fromEntries(Object.entries(item.labels).map(([name, value]) => [name, {
color: "passive",
value: value,
}]))}
/>
</td>
</tr>
@@ -109,15 +100,11 @@ const BaseRule = ({ item }: BaseRuleProps) => {
{!!Object.keys(item?.annotations || {}).length && (
<>
<span className="title">Annotations</span>
<table>
<colgroup>
<col className="vm-col-md"/>
<col/>
</colgroup>
<table className="fixed">
<tbody>
{Object.entries(item.annotations || {}).map(([name, value]) => (
<tr key={name}>
<td>{name}</td>
<td className="vm-col-md">{name}</td>
<td>{value}</td>
</tr>
))}
@@ -128,14 +115,14 @@ const BaseRule = ({ item }: BaseRuleProps) => {
{!!item?.updates?.length && (
<>
<span className="title">{`Last updates ${item.updates.length}/${item.max_updates_entries}`}</span>
<table>
<table className="fixed">
<thead>
<tr>
<th>Updated at</th>
<th>Series returned</th>
<th>Series fetched</th>
<th>Duration</th>
<th>Executed at</th>
<th className="vm-col-md">Updated at</th>
<th className="vm-col-md">Series returned</th>
<th className="vm-col-md">Series fetched</th>
<th className="vm-col-md">Duration</th>
<th className="vm-col-md">Executed at</th>
</tr>
</thead>
<tbody>
@@ -143,11 +130,11 @@ const BaseRule = ({ item }: BaseRuleProps) => {
<tr
key={update.at}
>
<td>{dayjs(update.time).format("DD MMM YYYY HH:mm:ss")}</td>
<td>{update.samples}</td>
<td>{update.series_fetched}</td>
<td>{formatDuration(update.duration / 1e9)}</td>
<td>{dayjs(update.at).format("DD MMM YYYY HH:mm:ss")}</td>
<td className="vm-col-md">{dayjs(update.time).format("DD MMM YYYY HH:mm:ss")}</td>
<td className="vm-col-md">{update.samples}</td>
<td className="vm-col-md">{update.series_fetched}</td>
<td className="vm-col-md">{formatDuration(update.duration / 1e9)}</td>
<td className="vm-col-md">{dayjs(update.at).format("DD MMM YYYY HH:mm:ss")}</td>
</tr>
))}
</tbody>
@@ -157,21 +144,14 @@ const BaseRule = ({ item }: BaseRuleProps) => {
{!!item?.alerts?.length && (
<>
<span className="title">Alerts</span>
<table>
<colgroup>
<col className="vm-col-sm"/>
<col className="vm-col-sm"/>
<col className="vm-col-sm"/>
<col/>
<col className="vm-col-hidden"/>
</colgroup>
<table className="fixed">
<thead>
<tr>
<th>Active since</th>
<th>State</th>
<th>Value</th>
<th className="title">Labels</th>
<th></th>
<th className="vm-col-sm">Active since</th>
<th className="vm-col-sm">State</th>
<th className="vm-col-sm">Value</th>
<th>Labels</th>
<th className="vm-col-hidden"></th>
</tr>
</thead>
<tbody>
@@ -180,15 +160,15 @@ const BaseRule = ({ item }: BaseRuleProps) => {
id={`alert-${alert.id}`}
key={alert.id}
>
<td>
<td className="vm-col-sm">
{dayjs(alert.activeAt).format("DD MMM YYYY HH:mm:ss")}
</td>
<td>
<td className="vm-col-sm">
<Badges
items={{ [alert.state]: { color: alert.state as BadgeColor } }}
/>
</td>
<td>
<td className="vm-col-sm">
<Badges
items={{ [alert.value]: { color: "passive" } }}
/>
@@ -202,7 +182,7 @@ const BaseRule = ({ item }: BaseRuleProps) => {
}]))}
/>
</td>
<td>
<td className="vm-col-hidden">
<Button
className="vm-button-borderless"
size="small"

View File

@@ -1,14 +1,16 @@
@use "src/styles/variables" as *;
.vm-modal.vm-explore-alerts {
.vm-modal-content {
overflow-y: scroll;
.vm-modal {
.vm-explore-alerts-rule-item {
table {
width: auto;
}
}
}
.vm-explore-alerts-rule-item {
row-gap: $padding-global;
margin: $padding-global;
margin-right: $padding-global;
display: flex;
flex-direction: column;
@@ -18,46 +20,46 @@
}
.vm-col-hidden {
width: 40px;
width: 30px;
}
.vm-button {
color: $color-passive;
border: 1px solid $color-passive;
}
.vm-code-example {
.vm-button {
background-color: $color-code;
}
border: 1px solid var(--color-passive);
}
.vm-col-sm {
width: 15%;
width: 10%;
white-space: nowrap;
text-align: left;
overflow: hidden;
text-overflow: ellipsis;
}
.vm-col-md {
width: 20%;
width: 15%;
white-space: nowrap;
text-align: left;
overflow: hidden;
text-overflow: ellipsis;
}
table {
word-break: break-word;
table-layout: fixed;
&.fixed {
table-layout: fixed;
}
width: 100%;
td, th {
line-height: 30px;
padding: 4px $padding-small;
vertical-align: middle;
white-space: nowrap;
text-align: left;
overflow: hidden;
text-overflow: ellipsis;
}
td.align-center {
text-align: center
}
th {
font-weight: bold;
text-align: center;
padding: 0 $padding-small;
}
}

View File

@@ -35,14 +35,12 @@ const GroupHeaderHeader: FC<GroupHeaderControlsProps> = ({ group }) => {
<div className="vm-explore-alerts-group-header__file">{group.file}</div>
)}
</div>
<div className="vm-explore-alerts-controls">
<Badges
align="end"
items={Object.fromEntries(Object.entries(group.states || {}).map(([name, value]) => [name.toLowerCase(), {
color: name.toLowerCase().replace(" ", "-") as BadgeColor,
value: value,
}]))}
/>
<Badges
items={Object.fromEntries(Object.entries(group.states || {}).map(([name, value]) => [name.toLowerCase(), {
color: name.toLowerCase().replace(" ", "-") as BadgeColor,
value: value,
}]))}
>
<Button
className="vm-button-borderless"
size="small"
@@ -51,7 +49,7 @@ const GroupHeaderHeader: FC<GroupHeaderControlsProps> = ({ group }) => {
startIcon={<DetailsIcon />}
onClick={openGroupModal}
/>
</div>
</Badges>
</div>
);
};

View File

@@ -58,8 +58,3 @@
border-radius: 6px;
}
}
.vm-explore-alerts-controls {
display: flex;
column-gap: $padding-global;
}

View File

@@ -1,4 +1,4 @@
import { FC, useMemo } from "preact/compat";
import { FC } from "preact/compat";
import "./style.scss";
import useDeviceDetect from "../../../hooks/useDeviceDetect";
import useCopyToClipboard from "../../../hooks/useCopyToClipboard";
@@ -83,13 +83,6 @@ const ItemHeader: FC<ItemHeaderControlsProps> = ({ name, id, groupId, entity, ty
}
};
const badgesItems = useMemo(() => {
return Object.fromEntries(Object.entries(states || {}).map(([name, value]) => [name, {
color: name.toLowerCase().replace(" ", "-") as BadgeColor,
value: value == 1 ? 0 : value,
}]));
}, [states]);
return (
<div
className={headerClasses}
@@ -99,11 +92,12 @@ const ItemHeader: FC<ItemHeaderControlsProps> = ({ name, id, groupId, entity, ty
{renderIcon()}
<div className="vm-explore-alerts-item-header__name">{name}</div>
</div>
<div className="vm-explore-alerts-controls">
<Badges
align="end"
items={badgesItems}
/>
<Badges
items={Object.fromEntries(Object.entries(states || {}).map(([name, value]) => [name, {
color: name.toLowerCase().replace(" ", "-") as BadgeColor,
value: value == 1 ? 0 : value,
}]))}
>
{onClose ? (
<Button
className="vm-back-button"
@@ -125,7 +119,7 @@ const ItemHeader: FC<ItemHeaderControlsProps> = ({ name, id, groupId, entity, ty
onClick={openItemLink}
/>
)}
</div>
</Badges>
</div>
);
};

View File

@@ -2,6 +2,7 @@
.vm-explore-alerts-item-header {
display: flex;
grid-template-columns: auto 1fr auto auto;
align-items: center;
justify-content: space-between;
gap: $padding-global;
@@ -27,6 +28,7 @@
}
&_mobile {
grid-template-columns: 1fr auto;
.vm-button-text {
display: none;
}
@@ -49,11 +51,9 @@
&__title {
display: flex;
column-gap: $padding-global;
overflow: hidden;
svg {
fill: $color-text-disabled;
width: 14px;
min-width: 14px;
}
}
@@ -68,8 +68,3 @@
border-radius: 6px;
}
}
.vm-explore-alerts-controls {
display: flex;
column-gap: $padding-global;
}

View File

@@ -2,12 +2,14 @@
.vm-explore-alerts-notifier-header {
display: flex;
grid-template-columns: auto 1fr auto auto;
align-items: center;
padding: $padding-global;
justify-content: space-between;
gap: $padding-global;
&_mobile {
grid-template-columns: 1fr auto;
padding: $padding-small $padding-global;
}

View File

@@ -9,7 +9,6 @@ import useDeviceDetect from "../../../hooks/useDeviceDetect";
interface NotifiersHeaderProps {
kinds: string[];
allKinds: string[];
search: string;
onChangeKinds: (input: string) => void;
onChangeSearch: (input: string) => void;
}
@@ -17,7 +16,6 @@ interface NotifiersHeaderProps {
const NotifiersHeader: FC<NotifiersHeaderProps> = ({
kinds,
allKinds,
search,
onChangeKinds,
onChangeSearch,
}) => {
@@ -48,7 +46,6 @@ const NotifiersHeader: FC<NotifiersHeaderProps> = ({
<div className="vm-explore-alerts-header-search">
<TextField
label="Search"
value={search}
placeholder="Filter by kind, address or labels"
startIcon={<SearchIcon />}
onChange={onChangeSearch}

View File

@@ -11,7 +11,6 @@ interface RulesHeaderProps {
allTypes: string[];
allStates: string[];
states: string[];
search: string;
onChangeTypes: (input: string) => void;
onChangeStates: (input: string) => void;
onChangeSearch: (input: string) => void;
@@ -22,7 +21,6 @@ const RulesHeader: FC<RulesHeaderProps> = ({
allTypes,
allStates,
states,
search,
onChangeTypes,
onChangeStates,
onChangeSearch,
@@ -71,7 +69,6 @@ const RulesHeader: FC<RulesHeaderProps> = ({
<div className="vm-explore-alerts-header-search">
<TextField
label="Search"
value={search}
placeholder="Filter by rule, name or labels"
startIcon={<SearchIcon />}
onChange={onChangeSearch}

View File

@@ -21,6 +21,26 @@
min-width: 150px;
}
&-description {
display: grid;
grid-template-columns: 1fr auto;
align-items: flex-start;
gap: $padding-small;
ul {
list-style-position: inside;
}
button {
color: inherit;
min-height: 29px;
}
code {
margin: 0 3px;
}
}
&-search {
flex-grow: 1;
.vm-text-field__input {

View File

@@ -1,9 +1,9 @@
import { FC, useMemo } from "preact/compat";
import { FC } from "preact/compat";
import "./style.scss";
import { Target as APITarget } from "../../../types";
import Alert from "../../Main/Alert/Alert";
import Accordion from "../../Main/Accordion/Accordion";
import Badges, { BadgeColor } from "../Badges";
import Badges from "../Badges";
interface TargetProps {
target: APITarget;
@@ -11,13 +11,6 @@ interface TargetProps {
const Target: FC<TargetProps> = ({ target }) => {
const state = target?.lastError ? "unhealthy" : "ok";
const targetLabels = target?.labels || {};
const badgesItems = useMemo(() => {
return Object.fromEntries(Object.entries(targetLabels).map(([name, value]) => [name, {
value: value,
color: "passive" as BadgeColor,
}]));
}, [targetLabels]);
return (
<div className={`vm-explore-alerts-target vm-badge-item ${state.replace(" ", "-")}`}>
{(!!target?.labels?.length || !!target?.lastError) ? (
@@ -30,12 +23,15 @@ const Target: FC<TargetProps> = ({ target }) => {
<div className="vm-explore-alerts-target-item">
<table>
<tbody>
{!!Object.keys(targetLabels).length && (
{!!target?.labels?.length && (
<tr>
<td className="vm-col-md">Labels</td>
<td>
<Badges
items={badgesItems}
items={Object.fromEntries(Object.entries(target.labels).map(([name, value]) => [name, {
value: value,
color: "passive",
}]))}
/>
</td>
</tr>

View File

@@ -106,7 +106,7 @@ const ExploreMetricItemHeader: FC<ExploreMetricItemControlsProps> = ({
onClick={handleClickRemove}
fullWidth
>
Remove graph
Remove graph
</Button>
</div>
</Modal>

View File

@@ -6,9 +6,8 @@
padding: $padding-global;
white-space: pre-wrap;
border-radius: $border-radius-small;
background-color: $color-code;
background-color: rgba($color-black, 0.05);
overflow: auto;
text-overflow: ellipsis;
&__copy {
position: absolute;

View File

@@ -13,8 +13,6 @@ import Button from "../../Button/Button";
interface DatePickerProps {
date: Date | Dayjs
format?: string
minDate?: Date | Dayjs
maxDate?: Date | Dayjs
onChange: (date: string) => void
}
@@ -26,8 +24,6 @@ enum CalendarTypeView {
const Calendar: FC<DatePickerProps> = ({
date,
minDate,
maxDate,
format = DATE_TIME_FORMAT,
onChange,
}) => {
@@ -38,8 +34,6 @@ const Calendar: FC<DatePickerProps> = ({
const today = dayjs.tz();
const viewDateIsToday = today.format(DATE_FORMAT) === viewDate.format(DATE_FORMAT);
const { isMobile } = useDeviceDetect();
const min = minDate ? dayjs(minDate) : undefined;
const max = maxDate ? dayjs(maxDate) : undefined;
const toggleDisplayYears = () => {
setViewType(prev => prev === CalendarTypeView.years ? CalendarTypeView.days : CalendarTypeView.years);
@@ -81,13 +75,9 @@ const Calendar: FC<DatePickerProps> = ({
onChangeViewDate={handleChangeViewDate}
toggleDisplayYears={toggleDisplayYears}
showArrowNav={viewType === CalendarTypeView.days}
hasPrev={viewType === CalendarTypeView.days && (!min || viewDate.startOf("month").isAfter(min))}
hasNext={viewType === CalendarTypeView.days && (!max || viewDate.endOf("month").isBefore(max))}
/>
{viewType === CalendarTypeView.days && (
<CalendarBody
minDate={min}
maxDate={max}
viewDate={viewDate}
selectDate={selectDate}
onChangeSelectDate={handleChangeSelectDate}
@@ -95,16 +85,12 @@ const Calendar: FC<DatePickerProps> = ({
)}
{viewType === CalendarTypeView.years && (
<YearsList
minDate={min}
maxDate={max}
viewDate={viewDate}
onChangeViewDate={handleChangeViewDate}
/>
)}
{viewType === CalendarTypeView.months && (
<MonthsList
minDate={min}
maxDate={max}
selectDate={selectDate}
viewDate={viewDate}
onChangeViewDate={handleChangeViewDate}
@@ -117,7 +103,7 @@ const Calendar: FC<DatePickerProps> = ({
size="small"
onClick={handleToday}
>
show today
show today
</Button>
</div>
)}

View File

@@ -4,8 +4,6 @@ import classNames from "classnames";
import Tooltip from "../../../Tooltip/Tooltip";
interface CalendarBodyProps {
minDate?: Dayjs
maxDate?: Dayjs
viewDate: Dayjs
selectDate: Dayjs
onChangeSelectDate: (date: Dayjs) => void
@@ -13,7 +11,7 @@ interface CalendarBodyProps {
const weekday = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
const CalendarBody: FC<CalendarBodyProps> = ({ minDate, maxDate, viewDate: date, selectDate, onChangeSelectDate }) => {
const CalendarBody: FC<CalendarBodyProps> = ({ viewDate: date, selectDate, onChangeSelectDate }) => {
const format = "YYYY-MM-DD";
const today = dayjs.tz();
const viewDate = dayjs(date.format(format));
@@ -46,25 +44,21 @@ const CalendarBody: FC<CalendarBodyProps> = ({ minDate, maxDate, viewDate: date,
</Tooltip>
))}
{days.map((d, i) => {
const isDisabled = d && ((minDate && d.isBefore(minDate)) || (maxDate && d.isAfter(maxDate)));
return (
<div
className={classNames({
"vm-calendar-body-cell": true,
"vm-calendar-body-cell_day": true,
"vm-calendar-body-cell_day_empty": !d,
"vm-calendar-body-cell_day_active": (d && d.format(format)) === selectDate.format(format),
"vm-calendar-body-cell_day_today": (d && d.format(format)) === today.format(format),
"vm-calendar-body-cell_day_disabled": isDisabled,
})}
key={d ? d.format(format) : i}
onClick={isDisabled ? undefined : createHandlerSelectDate(d)}
>
{d && d.format("D")}
</div>
);
})}
{days.map((d, i) => (
<div
className={classNames({
"vm-calendar-body-cell": true,
"vm-calendar-body-cell_day": true,
"vm-calendar-body-cell_day_empty": !d,
"vm-calendar-body-cell_day_active": (d && d.format(format)) === selectDate.format(format),
"vm-calendar-body-cell_day_today": (d && d.format(format)) === today.format(format)
})}
key={d ? d.format(format) : i}
onClick={createHandlerSelectDate(d)}
>
{d && d.format("D")}
</div>
))}
</div>
);
};

View File

@@ -1,18 +1,15 @@
import { FC } from "preact/compat";
import { Dayjs } from "dayjs";
import { ArrowDownIcon, ArrowDropDownIcon } from "../../../Icons";
import classNames from "classnames";
interface CalendarHeaderProps {
viewDate: Dayjs
onChangeViewDate: (date: Dayjs) => void
showArrowNav: boolean
toggleDisplayYears: () => void
hasNext: boolean
hasPrev: boolean
}
const CalendarHeader: FC<CalendarHeaderProps> = ({ hasPrev, hasNext, viewDate, showArrowNav, onChangeViewDate, toggleDisplayYears }) => {
const CalendarHeader: FC<CalendarHeaderProps> = ({ viewDate, showArrowNav, onChangeViewDate, toggleDisplayYears }) => {
const setPrevMonth = () => {
onChangeViewDate(viewDate.subtract(1, "month"));
@@ -38,20 +35,14 @@ const CalendarHeader: FC<CalendarHeaderProps> = ({ hasPrev, hasNext, viewDate, s
{showArrowNav && (
<div className="vm-calendar-header-right">
<div
className={classNames({
"vm-calendar-header-right__prev": true,
"vm-calendar-header-right_disabled": !hasPrev,
})}
onClick={hasPrev ? setPrevMonth : undefined}
className="vm-calendar-header-right__prev"
onClick={setPrevMonth}
>
<ArrowDownIcon/>
</div>
<div
className={classNames({
"vm-calendar-header-right__next": true,
"vm-calendar-header-right_disabled": !hasNext,
})}
onClick={hasNext ? setNextMonth : undefined}
className="vm-calendar-header-right__next"
onClick={setNextMonth}
>
<ArrowDownIcon/>
</div>

View File

@@ -3,14 +3,13 @@ import dayjs, { Dayjs } from "dayjs";
import classNames from "classnames";
interface CalendarMonthsProps {
minDate?: Dayjs
maxDate?: Dayjs
viewDate: Dayjs,
selectDate: Dayjs
onChangeViewDate: (date: Dayjs) => void
}
const MonthsList: FC<CalendarMonthsProps> = ({ minDate, maxDate, viewDate, selectDate, onChangeViewDate }) => {
const MonthsList: FC<CalendarMonthsProps> = ({ viewDate, selectDate, onChangeViewDate }) => {
const today = dayjs().format("MM");
const currentMonths = useMemo(() => selectDate.format("MM"), [selectDate]);
@@ -30,24 +29,20 @@ const MonthsList: FC<CalendarMonthsProps> = ({ minDate, maxDate, viewDate, selec
return (
<div className="vm-calendar-years">
{months.map(m => {
const isDisabled = m && ((minDate && m.isBefore(minDate)) || (maxDate && m.isAfter(maxDate)));
return (
<div
className={classNames({
"vm-calendar-years__year": true,
"vm-calendar-years__year_selected": m.format("MM") === currentMonths,
"vm-calendar-years__year_today": m.format("MM") === today,
"vm-calendar-years__year_disabled": isDisabled,
})}
id={`vm-calendar-year-${m.format("MM")}`}
key={m.format("MM")}
onClick={isDisabled ? undefined : createHandlerClick(m)}
>
{m.format("MMMM")}
</div>
);
})}
{months.map(m => (
<div
className={classNames({
"vm-calendar-years__year": true,
"vm-calendar-years__year_selected": m.format("MM") === currentMonths,
"vm-calendar-years__year_today": m.format("MM") === today
})}
id={`vm-calendar-year-${m.format("MM")}`}
key={m.format("MM")}
onClick={createHandlerClick(m)}
>
{m.format("MMMM")}
</div>
))}
</div>
);
};

View File

@@ -3,13 +3,11 @@ import dayjs, { Dayjs } from "dayjs";
import classNames from "classnames";
interface CalendarYearsProps {
minDate?: Dayjs
maxDate?: Dayjs
viewDate: Dayjs
onChangeViewDate: (date: Dayjs) => void
}
const YearsList: FC<CalendarYearsProps> = ({ minDate, maxDate, viewDate, onChangeViewDate }) => {
const YearsList: FC<CalendarYearsProps> = ({ viewDate, onChangeViewDate }) => {
const today = dayjs().format("YYYY");
const currentYear = useMemo(() => viewDate.format("YYYY"), [viewDate]);
@@ -32,24 +30,20 @@ const YearsList: FC<CalendarYearsProps> = ({ minDate, maxDate, viewDate, onChang
return (
<div className="vm-calendar-years">
{years.map(y => {
const isDisabled = y && (minDate && y.isBefore(minDate)) || (maxDate && y.isAfter(maxDate));
return (
<div
className={classNames({
"vm-calendar-years__year": true,
"vm-calendar-years__year_selected": y.format("YYYY") === currentYear,
"vm-calendar-years__year_today": y.format("YYYY") === today,
"vm-calendar-years__year_disabled": isDisabled,
})}
id={`vm-calendar-year-${y.format("YYYY")}`}
key={y.format("YYYY")}
onClick={isDisabled ? undefined : createHandlerClick(y)}
>
{y.format("YYYY")}
</div>
);
})}
{years.map(y => (
<div
className={classNames({
"vm-calendar-years__year": true,
"vm-calendar-years__year_selected": y.format("YYYY") === currentYear,
"vm-calendar-years__year_today": y.format("YYYY") === today
})}
id={`vm-calendar-year-${y.format("YYYY")}`}
key={y.format("YYYY")}
onClick={createHandlerClick(y)}
>
{y.format("YYYY")}
</div>
))}
</div>
);
};

View File

@@ -69,10 +69,6 @@
}
}
&_disabled {
color: $color-text-disabled;
}
&__prev {
transform: rotate(90deg);
}
@@ -112,12 +108,7 @@
cursor: pointer;
transition: color 200ms ease, background-color 300ms ease-in-out;
&_disabled {
cursor: unset;
color: $color-text-disabled;
}
&:not(&_disabled):hover {
&:hover {
background-color: $color-hover-black;
}
@@ -157,12 +148,7 @@
cursor: pointer;
transition: color 200ms ease, background-color 300ms ease-in-out;
&_disabled {
cursor: unset;
color: $color-text-disabled;
}
&:not(&_disabled):hover {
&:hover {
background-color: $color-hover-black;
}

View File

@@ -8,13 +8,11 @@ import useBoolean from "../../../hooks/useBoolean";
import useEventListener from "../../../hooks/useEventListener";
interface DatePickerProps {
date: string | Date | Dayjs;
date: string | Date | Dayjs,
targetRef: React.RefObject<HTMLElement>;
format?: string;
label?: string;
minDate?: Date | Dayjs;
maxDate?: Date | Dayjs;
onChange: (val: string) => void;
format?: string
label?: string
onChange: (val: string) => void
}
const DatePicker = forwardRef<HTMLDivElement, DatePickerProps>(({
@@ -22,9 +20,7 @@ const DatePicker = forwardRef<HTMLDivElement, DatePickerProps>(({
targetRef,
format = DATE_TIME_FORMAT,
onChange,
label,
minDate,
maxDate
label
}, ref) => {
const dateDayjs = useMemo(() => dayjs(date).isValid() ? dayjs.tz(date) : dayjs().tz(), [date]);
const { isMobile } = useDeviceDetect();
@@ -60,8 +56,6 @@ const DatePicker = forwardRef<HTMLDivElement, DatePickerProps>(({
date={dateDayjs}
format={format}
onChange={handleChangeDate}
minDate={minDate}
maxDate={maxDate}
/>
</div>
</Popper>

View File

@@ -3,52 +3,37 @@ import { ChangeEvent, KeyboardEvent } from "react";
import { CalendarIcon } from "../../Icons";
import DatePicker from "../DatePicker";
import Button from "../../Button/Button";
import { DATE_ISO_FORMAT, DATE_FORMAT, DATE_TIME_FORMAT } from "../../../../constants/date";
import { DATE_TIME_FORMAT } from "../../../../constants/date";
import InputMask from "react-input-mask";
import dayjs, { Dayjs } from "dayjs";
import dayjs from "dayjs";
import classNames from "classnames";
import "./style.scss";
const formatStringDate = (val: string, format: string) => {
return dayjs(val).isValid() ? dayjs.tz(val).format(format) : val;
const formatStringDate = (val: string) => {
return dayjs(val).isValid() ? dayjs.tz(val).format(DATE_TIME_FORMAT) : val;
};
interface DateTimeInputProps {
value?: string;
label: string;
pickerLabel: string;
format?: string;
pickerRef: React.RefObject<HTMLDivElement>;
onChange: (date: string) => void;
onEnter: () => void;
disabled?: boolean;
minDate?: Date | Dayjs;
maxDate?: Date | Dayjs;
}
const masks: Record<string, string> = {
[DATE_ISO_FORMAT]: "9999-99-99T99:99:99",
[DATE_FORMAT]: "9999-99-99",
[DATE_TIME_FORMAT]: "9999-99-99 99:99:99"
};
const DateTimeInput: FC<DateTimeInputProps> = ({
value = "",
format = DATE_TIME_FORMAT,
minDate,
maxDate,
label,
pickerLabel,
pickerRef,
onChange,
onEnter,
disabled
onEnter
}) => {
const wrapperRef = useRef<HTMLDivElement>(null);
const [inputRef, setInputRef] = useState<HTMLInputElement | null>(null);
const mask = masks[format];
const [maskedValue, setMaskedValue] = useState(formatStringDate(value, format));
const [maskedValue, setMaskedValue] = useState(formatStringDate(value));
const [focusToTime, setFocusToTime] = useState(false);
const [awaitChangeForEnter, setAwaitChangeForEnter] = useState(false);
const error = dayjs(maskedValue).isValid() ? "" : "Invalid date format";
@@ -74,7 +59,7 @@ const DateTimeInput: FC<DateTimeInputProps> = ({
};
useEffect(() => {
const newValue = formatStringDate(value, format);
const newValue = formatStringDate(value);
if (newValue !== maskedValue) {
setMaskedValue(newValue);
}
@@ -97,16 +82,15 @@ const DateTimeInput: FC<DateTimeInputProps> = ({
<div
className={classNames({
"vm-date-time-input": true,
"vm-date-time-input_error": error,
"vm-date-time-input_disabled": disabled,
"vm-date-time-input_error": error
})}
>
<label>{label}</label>
<InputMask
tabIndex={1}
inputRef={setInputRef}
mask={mask}
placeholder={format}
mask="9999-99-99 99:99:99"
placeholder="YYYY-MM-DD HH:mm:ss"
value={maskedValue}
autoCapitalize={"none"}
inputMode={"numeric"}
@@ -114,7 +98,6 @@ const DateTimeInput: FC<DateTimeInputProps> = ({
onChange={handleMaskedChange}
onBlur={handleBlur}
onKeyUp={handleKeyUp}
disabled={disabled}
/>
{error && (
<span className="vm-date-time-input__error-text">{error}</span>
@@ -129,7 +112,6 @@ const DateTimeInput: FC<DateTimeInputProps> = ({
size="small"
startIcon={<CalendarIcon/>}
ariaLabel="calendar"
disabled={disabled}
/>
</div>
<DatePicker
@@ -138,9 +120,6 @@ const DateTimeInput: FC<DateTimeInputProps> = ({
date={maskedValue}
onChange={handleChangeDate}
targetRef={wrapperRef}
minDate={minDate}
maxDate={maxDate}
format={format}
/>
</div>
);

View File

@@ -23,14 +23,6 @@
user-select: none;
}
&_disabled {
cursor: default;
pointer-events: none;
* {
color: $color-text-disabled !important;
}
}
&__icon {
position: absolute;
bottom: 2px;

View File

@@ -22,8 +22,8 @@ interface PopperProps {
children: ReactNode
open: boolean
onClose: () => void
buttonRef?: React.RefObject<HTMLElement>
placement?: "bottom-right" | "bottom-left" | "top-left" | "top-right" | "center-left" | "center-right" | "fixed"
buttonRef: React.RefObject<HTMLElement>
placement?: "bottom-right" | "bottom-left" | "top-left" | "top-right" | "fixed"
placementPosition?: { top: number, left: number } | null
animation?: string
offset?: { top: number, left: number }
@@ -32,7 +32,6 @@ interface PopperProps {
title?: string
disabledFullScreen?: boolean
variant?: "default" | "dark"
classes?: string[]
}
const Popper: FC<PopperProps> = ({
@@ -47,7 +46,6 @@ const Popper: FC<PopperProps> = ({
fullWidth,
title,
disabledFullScreen,
classes,
variant
}) => {
const { isMobile } = useDeviceDetect();
@@ -80,7 +78,6 @@ const Popper: FC<PopperProps> = ({
}, [popperRef]);
const popperStyle = useMemo(() => {
if (!buttonRef) return {};
const buttonEl = buttonRef.current;
if (!buttonEl || !isOpen) return {};
@@ -93,9 +90,8 @@ const Popper: FC<PopperProps> = ({
width: "auto"
};
const needAlignRight = placement?.includes("right");
const needAlignRight = placement === "bottom-right" || placement === "top-right";
const needAlignTop = placement?.includes("top");
const needAlignCenter = placement?.includes("center");
const offsetTop = offset?.top || 0;
const offsetLeft = offset?.left || 0;
@@ -105,7 +101,6 @@ const Popper: FC<PopperProps> = ({
if (needAlignRight) position.left = buttonPos.right - popperSize.width;
if (needAlignTop) position.top = buttonPos.top - popperSize.height - offsetTop;
if (needAlignCenter) position.top = buttonPos.top + (buttonPos.height - popperSize.height) / 2 - offsetTop;
if (placement === "fixed" && placementPosition) {
position.top = Math.max(placementPosition.top + offset.top, 0);
@@ -166,10 +161,6 @@ const Popper: FC<PopperProps> = ({
useEventListener("scroll", handleClose);
useEventListener("popstate", handlePopstate);
useClickOutside(popperRef, handleClickOutside, buttonRef);
const classMap: Record<string, boolean> = {};
(classes || []).forEach((cls) => {
classMap[cls] = true;
});
return (
<>
@@ -180,7 +171,6 @@ const Popper: FC<PopperProps> = ({
[`vm-popper_${variant}`]: variant,
"vm-popper_mobile": isMobile && !disabledFullScreen,
"vm-popper_open": (isMobile || Object.keys(popperStyle).length) && isOpen,
...classMap,
})}
ref={popperRef}
style={(isMobile && !disabledFullScreen) ? {} : popperStyle}

View File

@@ -46,12 +46,11 @@ const Select: FC<SelectProps> = ({
const autocompleteAnchorEl = useRef<HTMLDivElement>(null);
const [wrapperRef, setWrapperRef] = useState<React.RefObject<HTMLElement> | null>(null);
const [openList, setOpenList] = useState(false);
const resultList = [...list];
const inputRef = useRef<HTMLInputElement>(null);
const isMultiple = Array.isArray(value);
let selectedValues = Array.isArray(value) ? value.slice() : [];
const selectedValues = Array.isArray(value) ? value.slice() : [];
const hideInput = isMobile && isMultiple && !!selectedValues?.length;
const textFieldValue = useMemo(() => {
@@ -78,7 +77,7 @@ const Select: FC<SelectProps> = ({
};
const handleBlur = () => {
resultList.includes(search) && onChange(search);
list.includes(search) && onChange(search);
};
const handleToggleList = (e: MouseEvent<HTMLDivElement>) => {
@@ -124,10 +123,8 @@ const Select: FC<SelectProps> = ({
useEventListener("keyup", handleKeyUp);
useClickOutside(autocompleteAnchorEl, handleCloseList, wrapperRef);
if (includeAll && !resultList.includes("All")) resultList.push("All");
if (includeAll && (!selectedValues?.length || selectedValues?.length === resultList?.length)) {
selectedValues = ["All"];
}
includeAll && !list.includes("All") && list.push("All");
includeAll && !selectedValues?.length && selectedValues.push("All");
return (
<div
@@ -158,7 +155,6 @@ const Select: FC<SelectProps> = ({
onInput={handleChange}
onFocus={handleFocus}
onBlur={handleBlur}
disabled={disabled}
ref={inputRef}
readOnly={isMobile || !searchable}
/>
@@ -186,7 +182,7 @@ const Select: FC<SelectProps> = ({
itemClassName={itemClassName}
label={label}
value={autocompleteValue}
options={resultList.map(l => ({ value: l }))}
options={list.map(l => ({ value: l }))}
anchor={autocompleteAnchorEl}
selected={selectedValues}
minLength={1}

View File

@@ -128,14 +128,8 @@
}
&_disabled {
pointer-events: none;
* {
color: var(--color-text-disabled);
cursor: default;
}
input::placeholder {
color: var(--color-text-disabled);
cursor: not-allowed;
}
.vm-select-input {

View File

@@ -9,7 +9,6 @@ $switch-border-radius: $switch-handle-size + ($switch-padding * 2);
.vm-switch {
font-size: $font-size-small;
display: flex;
column-gap: $padding-small;
align-items: center;
justify-content: flex-start;
cursor: pointer;
@@ -20,6 +19,10 @@ $switch-border-radius: $switch-handle-size + ($switch-padding * 2);
flex-direction: row-reverse;
}
&_full-width &__label {
margin-left: 0;
}
&_disabled {
opacity: 0.6;
cursor: default;
@@ -71,6 +74,7 @@ $switch-border-radius: $switch-handle-size + ($switch-padding * 2);
&__label {
white-space: nowrap;
font-size: inherit;
margin-left: $padding-small;
transition: color 200ms ease;
color: $color-text-secondary;
}

View File

@@ -72,6 +72,7 @@ const TextField: FC<TextFieldProps> = ({
"vm-text-field__input_error": error,
"vm-text-field__input_warning": !error && warning,
"vm-text-field__input_icon-start": startIcon,
"vm-text-field__input_disabled": disabled,
"vm-text-field__input_textarea": type === "textarea",
});
@@ -135,14 +136,12 @@ const TextField: FC<TextFieldProps> = ({
className={classNames({
"vm-text-field": true,
"vm-text-field_textarea": type === "textarea",
"vm-text-field_dark": isDarkTheme,
"vm-text-field_disabled": disabled
"vm-text-field_dark": isDarkTheme
})}
data-replicated-value={value}
>
{startIcon && <div className="vm-text-field__icon-start">{startIcon}</div>}
{endIcon && <div className="vm-text-field__icon-end">{endIcon}</div>}
{label && <span className="vm-text-field__label">{label}</span>}
{type === "textarea"
? (
<textarea
@@ -181,6 +180,7 @@ const TextField: FC<TextFieldProps> = ({
/>
)
}
{label && <span className="vm-text-field__label">{label}</span>}
<TextFieldMessage
error={error}
warning={warning}

View File

@@ -1,19 +1,11 @@
@use "src/styles/variables" as *;
.vm-text-field {
position: relative;
display: grid;
margin: 6px 0;
width: 100%;
&_disabled {
color: $color-text-disabled;
pointer-events: none;
}
&:is(&_disabled) > &__label {
color: $color-text-disabled;
}
&_textarea:after {
content: attr(data-replicated-value) " ";
white-space: pre-wrap;
@@ -30,7 +22,7 @@
background-color: transparent;
font-size: $font-size;
line-height: 18px;
grid-area: 2 / 1 / 2 / 2;
grid-area: 1 / 1 / 2 / 2;
overflow: hidden;
box-sizing: border-box;
}
@@ -39,10 +31,8 @@
&__error,
&__warning,
&__helper-text, {
width: fit-content;
margin-top: calc(($font-size-small/-2) - 1px);
margin-bottom: calc(($font-size-small/-2) - 1px);
margin-left: calc($padding-global/2);
position: absolute;
left: calc($padding-global/2);
max-width: calc(100% - $padding-global);
padding: 0 3px;
font-size: $font-size-small;
@@ -60,12 +50,16 @@
}
&__label {
top: calc(($font-size-small/-2) - 2px);
color: $color-text-secondary;
}
&__helper-text,
&__warning,
&__error {
position: relative;
top: calc($font-size-small/-2);
width: fit-content;
overflow-wrap: anywhere;
pointer-events: auto;
user-select: text;
@@ -149,17 +143,28 @@
&__icon-start,
&__icon-end {
align-self: center;
display: flex;
align-items: center;
justify-content: center;
max-width: 15px;
top: 0;
left: $padding-small;
height: 36px;
position: absolute;
color: $color-text-secondary;
}
&__icon-start {
left: $padding-small;
}
&__icon-end {
left: auto;
right: $padding-small;
}
&__controls-info {
position: absolute;
bottom: $padding-small;
right: $padding-global;
color: $color-text-secondary;
font-size: $font-size-small;
opacity: 0.8;
}
}

View File

@@ -1,28 +1,20 @@
import { createRef, useEffect, useState, useMemo } from "react";
import { useState, useMemo } from "react";
import classNames from "classnames";
import { ArrowDropDownIcon, CopyIcon, DoneIcon } from "../Main/Icons";
import { getComparator, stableSort } from "./helpers";
import Tooltip from "../Main/Tooltip/Tooltip";
import Button from "../Main/Button/Button";
import { useEffect } from "preact/compat";
import useCopyToClipboard from "../../hooks/useCopyToClipboard";
type OrderDir = "asc" | "desc"
export interface TableColumn<T> {
key: keyof T;
title?: string;
format?: (obj: T) => string;
className?: string;
}
interface TableProps<T> {
rows: T[];
columns: TableColumn<T>[];
columns: { title?: string, key: keyof Partial<T>, className?: string }[];
defaultOrderBy: keyof T;
copyToClipboard?: keyof T;
defaultOrderDir?: OrderDir;
rowClasses?: (obj: T) => Record<string, boolean>;
rowAction?: (ref: React.RefObject<HTMLElement>) => () => void;
// TODO: Remove when pagination is implemented on the backend.
paginationOffset: {
startIndex: number;
@@ -30,34 +22,12 @@ interface TableProps<T> {
}
}
interface TableRow {
id: string;
}
const Table = <T extends TableRow>({
rows,
columns,
defaultOrderBy,
defaultOrderDir,
copyToClipboard,
paginationOffset,
rowClasses,
rowAction = (_ref: React.RefObject<HTMLElement>) => () => {},
}: TableProps<T>) => {
const Table = <T extends object>({ rows, columns, defaultOrderBy, defaultOrderDir, copyToClipboard, paginationOffset }: TableProps<T>) => {
const handleCopyToClipboard = useCopyToClipboard();
const [orderBy, setOrderBy] = useState<keyof T>(defaultOrderBy);
const [orderDir, setOrderDir] = useState<OrderDir>(defaultOrderDir || "desc");
const [copied, setCopied] = useState<number | null>(null);
const [rowRefs, setRowRefs] = useState(new Map());
useEffect(() => {
const newRowRefs = new Map();
rows.forEach(row => {
newRowRefs.set(row.id, createRef());
});
setRowRefs(newRowRefs);
}, [rows]);
// const sortedList = useMemo(() => stableSort(rows as [], getComparator(orderDir, orderBy)),
// [rows, orderBy, orderDir]);
@@ -88,8 +58,6 @@ const Table = <T extends TableRow>({
return () => clearTimeout(timeout);
}, [copied]);
const copyCol = copyToClipboard && columns.find((col) => col.key == copyToClipboard);
return (
<table className="vm-table">
<thead className="vm-table-header">
@@ -122,14 +90,8 @@ const Table = <T extends TableRow>({
<tbody className="vm-table-body">
{sortedList.map((row, rowIndex) => (
<tr
className={classNames({
"vm-table__row": true,
...(rowClasses ? rowClasses(row) : {}),
})}
id={row.id}
className="vm-table__row"
key={rowIndex}
ref={rowRefs.get(row.id)}
onClick={rowAction(rowRefs.get(row.id))}
>
{columns.map((col) => (
<td
@@ -139,10 +101,10 @@ const Table = <T extends TableRow>({
})}
key={String(col.key)}
>
{(col.format ? col.format(row) : String(row[col.key])) || "-"}
{row[col.key] || "-"}
</td>
))}
{copyToClipboard && copyCol && (
{copyToClipboard && (
<td className="vm-table-cell vm-table-cell_right">
{row[copyToClipboard] && (
<div className="vm-table-cell__content">
@@ -152,7 +114,7 @@ const Table = <T extends TableRow>({
color={copied === rowIndex ? "success" : "gray"}
size="small"
startIcon={copied === rowIndex ? <DoneIcon/> : <CopyIcon/>}
onClick={createCopyHandler(copyCol.format ? copyCol.format(row) : String(row[copyToClipboard]), rowIndex)}
onClick={createCopyHandler(row[copyToClipboard], rowIndex)}
ariaLabel="copy row"
/>
</Tooltip>

View File

@@ -198,7 +198,7 @@ const TableSettings: FC<TableSettingsProps> = ({
{toggleTableCompact && tableCompact !== undefined && (
<div className="vm-table-settings-modal-section">
<div className="vm-table-settings-modal-section__title">
Table view
Table view
</div>
<div className="vm-table-settings-modal-columns-list__item">
<Switch

View File

@@ -41,12 +41,12 @@ export function descendingComparator<T>(a: T, b: T, orderBy: keyof T) {
return 0;
}
export function getComparator<T extends object>(
export function getComparator<Key extends (string | number | symbol)>(
order: Order,
orderBy: keyof T,
orderBy: Key,
): (
a: T,
b: T,
a: { [key in Key]: number | string },
b: { [key in Key]: number | string },
) => number {
return order === "desc"
? (a, b) => descendingComparator(a, b, orderBy)
@@ -55,7 +55,7 @@ export function getComparator<T extends object>(
// This method is created for cross-browser compatibility, if you don't
// need to support IE11, you can use Array.prototype.sort() directly
export function stableSort<T>(array: readonly T[], comparator: (a: T, b: T) => number): T[] {
export function stableSort<T>(array: readonly T[], comparator: (a: T, b: T) => number) {
const stabilizedThis = array.map((el, index) => [el, index] as [T, number]);
stabilizedThis.sort((a, b) => {
const order = comparator(a[0], b[0]);

View File

@@ -75,7 +75,7 @@ const TracingsView: FC<TraceViewProps> = ({ traces, jsonEditor = false, onDelete
>
<div className="vm-tracings-view-trace-header">
<h3 className="vm-tracings-view-trace-header-title">
Trace for <b className="vm-tracings-view-trace-header-title__query">{trace.queryValue}</b>
Trace for <b className="vm-tracings-view-trace-header-title__query">{trace.queryValue}</b>
</h3>
<Tooltip title={expandedTraces.includes(trace.idValue) ? "Collapse All" : "Expand All"}>
<Button

View File

@@ -128,7 +128,7 @@ const TableView: FC<GraphViewProps> = ({ data, displayColumns }) => {
>
<ArrowDropDownIcon/>
</div>
Value
Value
</div>
</td>
{hasCopyValue && <td className="vm-table-cell vm-table-cell_header"/>}

View File

@@ -10,7 +10,6 @@ export const darkPalette = {
"color-success": "#57ab5a",
"color-background-success": "#0e1b0e",
"color-passive": "#a7acb3",
"color-code": "#4e5a6a",
"color-background-body": "#22272e",
"color-background-block": "#2d333b",
"color-background-tooltip": "rgba(22, 22, 22, 0.8)",
@@ -45,7 +44,6 @@ export const lightPalette = {
"color-success": "#4caf50",
"color-background-success": "#d4ecd5",
"color-passive": "#5d6267",
"color-code": "#ecedee",
"color-background-body": "#FEFEFF",
"color-background-block": "#FFFFFF",
"color-background-tooltip": "rgba(80,80,80,0.9)",

View File

@@ -61,7 +61,7 @@ const DebugInfoClipboardApi = () => (
target="_blank"
rel="noopener noreferrer"
>
Clipboard API documentation
Clipboard API documentation
</a>
</p>
</p>

View File

@@ -1,39 +1,42 @@
import { useSearchParams } from "react-router-dom";
import { useNavigate, useSearchParams } from "react-router-dom";
import { useCallback } from "preact/compat";
type ParamValue = string | number | boolean | null | undefined;
const useSearchParamsFromObject = () => {
const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams();
const setSearchParamsFromKeys = useCallback((objectParams: Record<string, ParamValue>) => {
const hadParams = !!searchParams.size;
const setSearchParamsFromKeys = useCallback((objectParams: Record<string, string | number>) => {
const hasSearchParams = !!searchParams.size;
let hasChanged = false;
const newSearchParams = new URLSearchParams(searchParams);
const beforeParams = searchParams.toString();
for (const [key, newValue] of Object.entries(objectParams)) {
const isEmpty = newValue === null || newValue === undefined || newValue === "";
if (isEmpty) {
searchParams.keys().forEach(key => {
if (!(key in objectParams)) {
newSearchParams.delete(key);
continue;
hasChanged = true;
}
});
const next = String(newValue);
if (newSearchParams.get(key) !== next) {
newSearchParams.set(key, next);
Object.entries(objectParams).forEach(([key, value]) => {
if (newSearchParams.get(key) !== `${value}`) {
newSearchParams.set(key, `${value}`);
hasChanged = true;
}
});
if (!hasChanged) return;
if (hasSearchParams) {
setSearchParams(newSearchParams);
} else {
navigate(`?${newSearchParams.toString()}`, { replace: true });
}
}, [searchParams, navigate]);
if (beforeParams === newSearchParams.toString()) return;
setSearchParams(newSearchParams, { replace: !hadParams });
},
[searchParams, setSearchParams]
);
return { setSearchParamsFromKeys };
return {
setSearchParamsFromKeys
};
};
export default useSearchParamsFromObject;

View File

@@ -41,7 +41,7 @@ const Footer: FC<Props> = memo(({ links = footerLinksByDefault }) => {
))}
<div className="vm-footer__copyright">
&copy; {copyrightYears} VictoriaMetrics.
{version && <span className="vm-footer__version">&nbsp;Version: {version}</span>}
{version && <span className="vm-footer__version">&nbsp;{version}</span>}
</div>
</footer>;
});

View File

@@ -67,27 +67,39 @@ const Header: FC<HeaderProps> = ({ controlsComponent }) => {
})}
style={{ background, color }}
>
<div
className={classNames({
"vm-header-logo": true,
"vm-header-logo_mobile": displaySidebar,
})}
onClick={onClickLogo}
style={{ color }}
>
{<Logo/>}
</div>
{displaySidebar ? (
<SidebarHeader
background={background}
color={color}
/>
) : (
<HeaderNav
color={color}
background={background}
/>
<>
{!appModeEnable && (
<div
className="vm-header-logo"
onClick={onClickLogo}
style={{ color }}
>
{<Logo/>}
</div>
)}
<HeaderNav
color={color}
background={background}
/>
</>
)}
{displaySidebar && (
<div
className={classNames({
"vm-header-logo": true,
"vm-header-logo_mobile": true,
})}
onClick={onClickLogo}
style={{ color }}
>
{<Logo/>}
</div>
)}
<HeaderControls
controlsComponent={controlsComponent}

View File

@@ -53,7 +53,7 @@ const HeaderControls: FC<ControlsProps & HeaderProps> = ({
if (isMobile) {
return (
<>
<div className="vm-header-controls vm-header-controls_mobile">
<div className="vm-header-controls">
<Button
className={classNames({
"vm-header-button": !appModeEnable

View File

@@ -1,7 +1,6 @@
@use "src/styles/variables" as *;
.vm-header-controls {
order: 3;
display: flex;
align-items: center;
justify-content: flex-end;
@@ -12,7 +11,6 @@
display: grid;
grid-template-columns: 1fr;
padding: 0;
flex-grow: initial;
.vm-header-button {
border: none;

View File

@@ -3,7 +3,6 @@
$sidebar-transition: cubic-bezier(0.280, 0.840, 0.420, 1);
.vm-header-sidebar {
order: 1;
width: 24px;
height: 24px;
color: inherit;
@@ -17,7 +16,7 @@ $sidebar-transition: cubic-bezier(0.280, 0.840, 0.420, 1);
left: 0;
top: 0;
height: 51px;
width: 48px;
width: 51px;
transition: left 350ms $sidebar-transition;
&_open {
@@ -54,6 +53,15 @@ $sidebar-transition: cubic-bezier(0.280, 0.840, 0.420, 1);
transform: translateX(0);
}
&__logo {
position: relative;
display: flex;
align-items: center;
justify-content: flex-start;
cursor: pointer;
width: 65px;
}
&-settings {
display: grid;
align-items: center;

View File

@@ -17,6 +17,7 @@
@media (max-width: 1000px) {
position: sticky;
top: 0;
gap: $padding-small;
padding: $padding-small;
}
@@ -52,7 +53,6 @@
}
&_mobile {
order: 2;
max-width: 75px;
min-width: 75px;
margin: 0 auto;

View File

@@ -13,8 +13,6 @@ import useSearchParamsFromObject from "../../../hooks/useSearchParamsFromObject"
import useStateSearchParams from "../../../hooks/useStateSearchParams";
import Hyperlink from "../../../components/Main/Hyperlink/Hyperlink";
const DEFAULT_TOP_N = 10;
const CardinalityConfigurator: FC<CardinalityTotalsProps> = ({ isPrometheus, isCluster, ...props }) => {
const { isMobile } = useDeviceDetect();
const [searchParams] = useSearchParams();
@@ -23,8 +21,7 @@ const CardinalityConfigurator: FC<CardinalityTotalsProps> = ({ isPrometheus, isC
const showTips = searchParams.get("tips") || "";
const [match, setMatch] = useStateSearchParams("", "match");
const [focusLabel, setFocusLabel] = useStateSearchParams("", "focusLabel");
const [topN, setTopN] = useStateSearchParams(DEFAULT_TOP_N, "topN");
const hasChanges = !!(match || focusLabel || (topN !== DEFAULT_TOP_N && !isPrometheus));
const [topN, setTopN] = useStateSearchParams(10, "topN");
const errorTopN = useMemo(() => topN < 0 ? "Number must be bigger than zero" : "", [topN]);
@@ -38,10 +35,7 @@ const CardinalityConfigurator: FC<CardinalityTotalsProps> = ({ isPrometheus, isC
};
const handleResetQuery = () => {
setSearchParamsFromKeys({ match: "", focusLabel: "", topN: "" });
setMatch("");
setFocusLabel("");
setTopN(DEFAULT_TOP_N);
setSearchParamsFromKeys({ match: "", focusLabel: "" });
};
const handleToggleTips = () => {
@@ -51,7 +45,7 @@ const CardinalityConfigurator: FC<CardinalityTotalsProps> = ({ isPrometheus, isC
useEffect(() => {
const matchQuery = searchParams.get("match");
const topNQuery = +(searchParams.get("topN") || DEFAULT_TOP_N);
const topNQuery = +(searchParams.get("topN") || 10);
const focusLabelQuery = searchParams.get("focusLabel");
if (matchQuery !== match) setMatch(matchQuery || "");
if (topNQuery !== topN) setTopN(topNQuery);
@@ -100,7 +94,7 @@ const CardinalityConfigurator: FC<CardinalityTotalsProps> = ({ isPrometheus, isC
<TextField
label="Limit entries"
type="number"
value={isPrometheus ? DEFAULT_TOP_N : topN}
value={isPrometheus ? 10 : topN}
error={errorTopN}
disabled={isPrometheus}
helperText={isPrometheus ? "not available for Prometheus" : ""}
@@ -122,7 +116,7 @@ const CardinalityConfigurator: FC<CardinalityTotalsProps> = ({ isPrometheus, isC
withIcon={true}
>
<WikiIcon/>
Statistic inaccuracy explanation
Statistic inaccuracy explanation
</Hyperlink>
</div>
}
@@ -151,9 +145,8 @@ const CardinalityConfigurator: FC<CardinalityTotalsProps> = ({ isPrometheus, isC
variant="text"
startIcon={<RestartIcon/>}
onClick={handleResetQuery}
disabled={!hasChanges}
>
Reset filters
Reset
</Button>
<Button
startIcon={<PlayIcon/>}

View File

@@ -5,7 +5,6 @@ import usePrevious from "../../../hooks/usePrevious";
import { MAX_QUERY_FIELDS } from "../../../constants/graph";
import { useQueryDispatch, useQueryState } from "../../../state/query/QueryStateContext";
import { useTimeDispatch } from "../../../state/time/TimeStateContext";
import { getQueryStringValue } from "../../../utils/query-string";
import {
DeleteIcon,
PlayIcon,
@@ -22,7 +21,6 @@ import classNames from "classnames";
import { MouseEvent as ReactMouseEvent } from "react";
import { arrayEquals } from "../../../utils/array";
import useDeviceDetect from "../../../hooks/useDeviceDetect";
import useSearchParamsFromObject from "../../../hooks/useSearchParamsFromObject";
import { QueryStats } from "../../../api/types";
import { usePrettifyQuery } from "./hooks/usePrettifyQuery";
import QueryHistory from "../../../components/QueryHistory/QueryHistory";
@@ -52,9 +50,6 @@ export interface QueryConfiguratorProps {
}
}
const defaultHideQueryStr = getQueryStringValue("expr.hide", "") as string;
const defaultHideQuery: number[] = defaultHideQueryStr.split(",").filter(v => v).map(Number);
const QueryConfigurator: FC<QueryConfiguratorProps> = ({
queryErrors,
setQueryErrors,
@@ -74,10 +69,9 @@ const QueryConfigurator: FC<QueryConfiguratorProps> = ({
const { query, queryHistory, autocomplete, autocompleteQuick } = useQueryState();
const queryDispatch = useQueryDispatch();
const timeDispatch = useTimeDispatch();
const { setSearchParamsFromKeys } = useSearchParamsFromObject();
const [stateQuery, setStateQuery] = useState(query || []);
const [hideQuery, setHideQuery] = useState<number[]>(defaultHideQuery);
const [hideQuery, setHideQuery] = useState<number[]>([]);
const [awaitStateQuery, setAwaitStateQuery] = useState(false);
const prevStateQuery = usePrevious(stateQuery) as (undefined | string[]);
@@ -182,7 +176,6 @@ const QueryConfigurator: FC<QueryConfiguratorProps> = ({
useEffect(() => {
onHideQuery && onHideQuery(hideQuery);
setSearchParamsFromKeys({ "expr.hide": hideQuery.join(",") });
}, [hideQuery]);
useEffect(() => {

View File

@@ -17,8 +17,8 @@ const WarningHeatmapToLine:FC = () => {
<Alert variant="warning">
<div className="vm-warning-heatmap-to-line">
<p className="vm-warning-heatmap-to-line__text">
The expression cannot be displayed as a heatmap.
To make the graph work, disable the heatmap in the &quot;Graph settings&quot; or modify the expression.
The expression cannot be displayed as a heatmap.
To make the graph work, disable the heatmap in the &quot;Graph settings&quot; or modify the expression.
</p>
<Button

View File

@@ -40,7 +40,7 @@ const WarningLimitSeries: FC<Props> = ({ warning, query, onChange }) => {
variant="outlined"
onClick={handleShowAll}
>
Show all
Show all
</Button>
</div>
</Alert>

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