mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2026-06-18 08:13:57 +03:00
Compare commits
54 Commits
flaky-vmau
...
configwatc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0625bb77d | ||
|
|
4163f18250 | ||
|
|
686289c02b | ||
|
|
9ae10247bb | ||
|
|
06ce3f1496 | ||
|
|
d0690ba15f | ||
|
|
483e00ffb9 | ||
|
|
06f969a4a7 | ||
|
|
9517f5cf1a | ||
|
|
e62e0685dc | ||
|
|
df92e617db | ||
|
|
7c0c8cc702 | ||
|
|
07291c1d62 | ||
|
|
7c0015b836 | ||
|
|
06e52a99fd | ||
|
|
f5840951a4 | ||
|
|
9ca5a8d0f4 | ||
|
|
894b22590d | ||
|
|
f85fd161e4 | ||
|
|
7d552dbd9a | ||
|
|
795c3deaee | ||
|
|
cb44353a36 | ||
|
|
7e05200c60 | ||
|
|
a2f033ce6c | ||
|
|
78b217d70c | ||
|
|
c9b23de9ce | ||
|
|
16a75129be | ||
|
|
68bdb5e4d3 | ||
|
|
4360d10962 | ||
|
|
ce9c868f59 | ||
|
|
212ce1baf0 | ||
|
|
1a091e5831 | ||
|
|
bac186fc65 | ||
|
|
15ce9e5e49 | ||
|
|
2c1596ea84 | ||
|
|
21d4f844ab | ||
|
|
da0002ce66 | ||
|
|
f35b9ed36d | ||
|
|
b4dc67cba6 | ||
|
|
70afdd0285 | ||
|
|
51efd2c32b | ||
|
|
1e208a8c79 | ||
|
|
e49027df8f | ||
|
|
a518a4a904 | ||
|
|
ad46fce7d4 | ||
|
|
7cc13ee1cc | ||
|
|
74fcd10d2e | ||
|
|
59007cda51 | ||
|
|
5869a39e7b | ||
|
|
c3c802a61c | ||
|
|
8b92af9d45 | ||
|
|
e313874d01 | ||
|
|
58a4e48901 | ||
|
|
16d75ab0bd |
23
.github/copilot-instructions.md
vendored
Normal file
23
.github/copilot-instructions.md
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
# Project Overview
|
||||
|
||||
VictoriaMetrics is a fast, cost-saving, and scalable solution for monitoring and managing time series data. It delivers high performance and reliability, making it an ideal choice for businesses of all sizes.
|
||||
|
||||
## Folder Structure
|
||||
|
||||
- `/app`: Contains the compilable binaries.
|
||||
- `/lib`: Contains the golang reusable libraries
|
||||
- `/docs/victoriametrics`: Contains documentation for the project.
|
||||
- `/apptest/tests`: Contains integration tests.
|
||||
|
||||
## Libraries and Frameworks
|
||||
|
||||
- Backend: Golang, no framework. Use third-party libraries sparingly.
|
||||
- Frontend: React.
|
||||
|
||||
## Code review guidelines
|
||||
|
||||
Ensure the feature or bugfix includes a changelog entry in /docs/victoriametrics/changelog/CHANGELOG.md.
|
||||
Verify the entry is under the ## tip section and matches the structure and style of existing entries.
|
||||
Chore-only changes may be omitted from the changelog.
|
||||
|
||||
|
||||
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -59,7 +59,7 @@ jobs:
|
||||
arch: amd64
|
||||
steps:
|
||||
- name: Code checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup Go
|
||||
id: go
|
||||
|
||||
2
.github/workflows/codeql-analysis-go.yml
vendored
2
.github/workflows/codeql-analysis-go.yml
vendored
@@ -29,7 +29,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Set up Go
|
||||
id: go
|
||||
|
||||
4
.github/workflows/docs.yaml
vendored
4
.github/workflows/docs.yaml
vendored
@@ -16,12 +16,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Code checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
path: __vm
|
||||
|
||||
- name: Checkout private code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
repository: VictoriaMetrics/vmdocs
|
||||
token: ${{ secrets.VM_BOT_GH_TOKEN }}
|
||||
|
||||
6
.github/workflows/test.yml
vendored
6
.github/workflows/test.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Code checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup Go
|
||||
id: go
|
||||
@@ -71,7 +71,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Code checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup Go
|
||||
id: go
|
||||
@@ -97,7 +97,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Code checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup Go
|
||||
id: go
|
||||
|
||||
2
.github/workflows/vmui.yml
vendored
2
.github/workflows/vmui.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Code checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
|
||||
11
Makefile
11
Makefile
@@ -12,11 +12,12 @@ PKG_TAG := $(BUILDINFO_TAG)
|
||||
endif
|
||||
|
||||
EXTRA_DOCKER_TAG_SUFFIX ?=
|
||||
EXTRA_GO_BUILD_TAGS ?=
|
||||
|
||||
GO_BUILDINFO = -X '$(PKG_PREFIX)/lib/buildinfo.Version=$(APP_NAME)-$(DATEINFO_TAG)-$(BUILDINFO_TAG)'
|
||||
TAR_OWNERSHIP ?= --owner=1000 --group=1000
|
||||
|
||||
GOLANGCI_LINT_VERSION := 2.2.1
|
||||
GOLANGCI_LINT_VERSION := 2.4.0
|
||||
|
||||
.PHONY: $(MAKECMDGOALS)
|
||||
|
||||
@@ -470,16 +471,16 @@ vendor-update:
|
||||
go mod vendor
|
||||
|
||||
app-local:
|
||||
CGO_ENABLED=1 go build $(RACE) -ldflags "$(GO_BUILDINFO)" -o bin/$(APP_NAME)$(RACE) $(PKG_PREFIX)/app/$(APP_NAME)
|
||||
CGO_ENABLED=1 go build $(RACE) -ldflags "$(GO_BUILDINFO)" -tags "$(EXTRA_GO_BUILD_TAGS)" -o bin/$(APP_NAME)$(RACE) $(PKG_PREFIX)/app/$(APP_NAME)
|
||||
|
||||
app-local-pure:
|
||||
CGO_ENABLED=0 go build $(RACE) -ldflags "$(GO_BUILDINFO)" -o bin/$(APP_NAME)-pure$(RACE) $(PKG_PREFIX)/app/$(APP_NAME)
|
||||
CGO_ENABLED=0 go build $(RACE) -ldflags "$(GO_BUILDINFO)" -tags "$(EXTRA_GO_BUILD_TAGS)" -o bin/$(APP_NAME)-pure$(RACE) $(PKG_PREFIX)/app/$(APP_NAME)
|
||||
|
||||
app-local-goos-goarch:
|
||||
CGO_ENABLED=$(CGO_ENABLED) GOOS=$(GOOS) GOARCH=$(GOARCH) go build $(RACE) -ldflags "$(GO_BUILDINFO)" -o bin/$(APP_NAME)-$(GOOS)-$(GOARCH)$(RACE) $(PKG_PREFIX)/app/$(APP_NAME)
|
||||
CGO_ENABLED=$(CGO_ENABLED) GOOS=$(GOOS) GOARCH=$(GOARCH) go build $(RACE) -ldflags "$(GO_BUILDINFO)" -tags "$(EXTRA_GO_BUILD_TAGS)" -o bin/$(APP_NAME)-$(GOOS)-$(GOARCH)$(RACE) $(PKG_PREFIX)/app/$(APP_NAME)
|
||||
|
||||
app-local-windows-goarch:
|
||||
CGO_ENABLED=0 GOOS=windows GOARCH=$(GOARCH) go build $(RACE) -ldflags "$(GO_BUILDINFO)" -o bin/$(APP_NAME)-windows-$(GOARCH)$(RACE).exe $(PKG_PREFIX)/app/$(APP_NAME)
|
||||
CGO_ENABLED=0 GOOS=windows GOARCH=$(GOARCH) go build $(RACE) -ldflags "$(GO_BUILDINFO)" -tags "$(EXTRA_GO_BUILD_TAGS)" -o bin/$(APP_NAME)-windows-$(GOARCH)$(RACE).exe $(PKG_PREFIX)/app/$(APP_NAME)
|
||||
|
||||
quicktemplate-gen: install-qtc
|
||||
qtc
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/configwatcher"
|
||||
"github.com/VictoriaMetrics/metrics"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmagent/csvimport"
|
||||
@@ -112,6 +113,7 @@ func main() {
|
||||
flag.Usage = usage
|
||||
envflag.Parse()
|
||||
remotewrite.InitSecretFlags()
|
||||
configwatcher.Init()
|
||||
buildinfo.Init()
|
||||
logger.Init()
|
||||
timeserieslimits.Init(*maxLabelsPerTimeseries, *maxLabelNameLen, *maxLabelValueLen)
|
||||
@@ -199,6 +201,7 @@ func main() {
|
||||
}
|
||||
protoparserutil.StopUnmarshalWorkers()
|
||||
remotewrite.Stop()
|
||||
configwatcher.Stop()
|
||||
|
||||
logger.Infof("successfully stopped vmagent in %.3f seconds", time.Since(startTime).Seconds())
|
||||
}
|
||||
|
||||
@@ -463,12 +463,6 @@ again:
|
||||
// - Real-world implementations of v1 use both 400 and 415 status codes.
|
||||
// See more in research: https://github.com/VictoriaMetrics/VictoriaMetrics/pull/8462#issuecomment-2786918054
|
||||
case 415, 400:
|
||||
if c.canDowngradeVMProto.Swap(false) {
|
||||
logger.Infof("received unsupported media type or bad request from remote storage at %q. Downgrading protocol from VictoriaMetrics to Prometheus remote write for all future requests. "+
|
||||
"See https://docs.victoriametrics.com/victoriametrics/vmagent/#victoriametrics-remote-write-protocol", c.sanitizedURL)
|
||||
c.useVMProto.Store(false)
|
||||
}
|
||||
|
||||
if encoding.IsZstd(block) {
|
||||
logger.Infof("received unsupported media type or bad request from remote storage at %q. Re-packing the block to Prometheus remote write and retrying."+
|
||||
"See https://docs.victoriametrics.com/victoriametrics/vmagent/#victoriametrics-remote-write-protocol", c.sanitizedURL)
|
||||
|
||||
@@ -209,7 +209,7 @@ func Init() {
|
||||
// In this case it is impossible to prevent from sending many duplicates of samples passed to TryPush() to all the configured -remoteWrite.url
|
||||
// if these samples couldn't be sent to the -remoteWrite.url with the disabled persistent queue. So it is better sending samples
|
||||
// to the remaining -remoteWrite.url and dropping them on the blocked queue.
|
||||
dropSamplesOnFailureGlobal = *dropSamplesOnOverload || disableOnDiskQueueAny && len(disableOnDiskQueues) > 1
|
||||
dropSamplesOnFailureGlobal = *dropSamplesOnOverload || disableOnDiskQueueAny && len(*remoteWriteURLs) > 1
|
||||
|
||||
dropDanglingQueues()
|
||||
|
||||
|
||||
@@ -29,6 +29,18 @@ type manager struct {
|
||||
groups map[uint64]*rule.Group
|
||||
}
|
||||
|
||||
// groupAPI generates apiGroup object from group by its ID(hash)
|
||||
func (m *manager) groupAPI(gID uint64) (*apiGroup, error) {
|
||||
m.groupsMu.RLock()
|
||||
defer m.groupsMu.RUnlock()
|
||||
|
||||
g, ok := m.groups[gID]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("can't find group with id %d", gID)
|
||||
}
|
||||
return groupToAPI(g), nil
|
||||
}
|
||||
|
||||
// ruleAPI generates apiRule object from alert by its ID(hash)
|
||||
func (m *manager) ruleAPI(gID, rID uint64) (apiRule, error) {
|
||||
m.groupsMu.RLock()
|
||||
|
||||
@@ -22,10 +22,11 @@ import (
|
||||
// AlertManager represents integration provider with Prometheus alert manager
|
||||
// https://github.com/prometheus/alertmanager
|
||||
type AlertManager struct {
|
||||
addr *url.URL
|
||||
argFunc AlertURLGenerator
|
||||
client *http.Client
|
||||
timeout time.Duration
|
||||
addr *url.URL
|
||||
argFunc AlertURLGenerator
|
||||
client *http.Client
|
||||
timeout time.Duration
|
||||
lastError string
|
||||
|
||||
authCfg *promauth.Config
|
||||
// stores already parsed RelabelConfigs object
|
||||
@@ -71,6 +72,10 @@ func (am AlertManager) Addr() string {
|
||||
return am.addr.Redacted()
|
||||
}
|
||||
|
||||
func (am *AlertManager) LastError() string {
|
||||
return am.lastError
|
||||
}
|
||||
|
||||
// Send an alert or resolve message
|
||||
func (am *AlertManager) Send(ctx context.Context, alerts []Alert, headers map[string]string) error {
|
||||
am.metrics.alertsSent.Add(len(alerts))
|
||||
@@ -79,6 +84,9 @@ func (am *AlertManager) Send(ctx context.Context, alerts []Alert, headers map[st
|
||||
am.metrics.alertsSendDuration.UpdateDuration(startTime)
|
||||
if err != nil {
|
||||
am.metrics.alertsSendErrors.Add(len(alerts))
|
||||
am.lastError = err.Error()
|
||||
} else {
|
||||
am.lastError = ""
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -18,6 +18,11 @@ type FakeNotifier struct {
|
||||
// Close does nothing
|
||||
func (*FakeNotifier) Close() {}
|
||||
|
||||
// LastError returns last error message
|
||||
func (*FakeNotifier) LastError() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Addr returns ""
|
||||
func (*FakeNotifier) Addr() string { return "" }
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@ type Notifier interface {
|
||||
Send(ctx context.Context, alerts []Alert, notifierHeaders map[string]string) error
|
||||
// Addr returns address where alerts are sent.
|
||||
Addr() string
|
||||
// LastError returns error, that occured during last attempt to send data
|
||||
LastError() string
|
||||
// Close is a destructor for the Notifier
|
||||
Close()
|
||||
}
|
||||
|
||||
@@ -25,6 +25,11 @@ func (bh *blackHoleNotifier) Close() {
|
||||
bh.metrics.close()
|
||||
}
|
||||
|
||||
// LastError return last notifier's error
|
||||
func (bh *blackHoleNotifier) LastError() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// newBlackHoleNotifier creates a new blackHoleNotifier
|
||||
func newBlackHoleNotifier() *blackHoleNotifier {
|
||||
address := "blackhole"
|
||||
|
||||
@@ -28,8 +28,8 @@ var (
|
||||
"Defines how many retries to make before giving up on rule if request for it returns an error.")
|
||||
disableProgressBar = flag.Bool("replay.disableProgressBar", false, "Whether to disable rendering progress bars during the replay. "+
|
||||
"Progress bar rendering might be verbose or break the logs parsing, so it is recommended to be disabled when not used in interactive mode.")
|
||||
ruleEvaluationConcurrency = flag.Int("replay.ruleEvaluationConcurrency", 1, "The maximum number of concurrent `/query_range` requests for a single rule. "+
|
||||
"Increasing this value when replaying for a long time and a single request range is limited by `-replay.maxDatapointsPerQuery`.")
|
||||
ruleEvaluationConcurrency = flag.Int("replay.ruleEvaluationConcurrency", 1, "The maximum number of concurrent '/query_range' requests when replay recording rule or alerting rule with for=0. "+
|
||||
"Increasing this value when replaying for a long time, since each request is limited by -replay.maxDatapointsPerQuery.")
|
||||
)
|
||||
|
||||
func replay(groupsCfg []config.Group, qb datasource.QuerierBuilder, rw remotewrite.RWClient) (totalRows, droppedRows int, err error) {
|
||||
|
||||
@@ -246,24 +246,33 @@ func TestReplay(t *testing.T) {
|
||||
|
||||
// multiple rules + rule concurrency + group concurrency
|
||||
f("2021-01-01T12:00:00.000Z", "2021-01-01T12:02:30.000Z", 1, 3, 0, []config.Group{
|
||||
{Rules: []config.Rule{{Alert: "foo-group-single-concurrent", Expr: "sum(up) > 1"}, {Alert: "bar-group-single-concurrent", Expr: "max(up) < 1"}}, Concurrency: 2}}, &fakeReplayQuerier{
|
||||
{Rules: []config.Rule{{Alert: "foo-group-single-concurrent", For: promutil.NewDuration(30 * time.Second), Expr: "sum(up) > 1"}, {Alert: "bar-group-single-concurrent", Expr: "max(up) < 1"}}, Concurrency: 2}}, &fakeReplayQuerier{
|
||||
registry: map[string]map[string][]datasource.Metric{
|
||||
"sum(up) > 1": {
|
||||
"12:00:00+12:01:00": {},
|
||||
"12:01:00+12:02:00": {{
|
||||
Timestamps: []int64{1},
|
||||
"12:00:00+12:01:00": {{
|
||||
Timestamps: []int64{1609502460},
|
||||
Values: []float64{1},
|
||||
}},
|
||||
"12:01:00+12:02:00": {{
|
||||
Timestamps: []int64{1609502520},
|
||||
Values: []float64{1},
|
||||
}},
|
||||
"12:02:00+12:02:30": {{
|
||||
Timestamps: []int64{1609502580},
|
||||
Values: []float64{1},
|
||||
}},
|
||||
"12:02:00+12:02:30": {},
|
||||
},
|
||||
"max(up) < 1": {
|
||||
"12:00:00+12:01:00": {},
|
||||
"12:00:00+12:01:00": {{
|
||||
Timestamps: []int64{1609502460},
|
||||
Values: []float64{1},
|
||||
}},
|
||||
"12:01:00+12:02:00": {{
|
||||
Timestamps: []int64{1},
|
||||
Timestamps: []int64{1609502520},
|
||||
Values: []float64{1},
|
||||
}},
|
||||
"12:02:00+12:02:30": {},
|
||||
},
|
||||
},
|
||||
}, 4)
|
||||
}, 10)
|
||||
}
|
||||
|
||||
@@ -341,11 +341,15 @@ 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, as, err := ar.expandTemplates(s, qFn, time.Time{})
|
||||
ls, err := ar.expandLabelTemplates(s)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to expand templates: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
alertID := hash(ls.processed)
|
||||
as, err := ar.expandAnnotationTemplates(s, qFn, time.Time{}, ls)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
a := ar.newAlert(s, time.Time{}, ls.processed, as) // initial alert
|
||||
|
||||
prevT := time.Time{}
|
||||
@@ -363,7 +367,7 @@ func (ar *AlertingRule) execRange(ctx context.Context, start, end time.Time) ([]
|
||||
a.State = notifier.StatePending
|
||||
a.ActiveAt = at
|
||||
// re-template the annotations as active timestamp is changed
|
||||
_, a.Annotations, _ = ar.expandTemplates(s, qFn, at)
|
||||
a.Annotations, _ = ar.expandAnnotationTemplates(s, qFn, at, ls)
|
||||
a.Start = time.Time{}
|
||||
} else if at.Sub(a.ActiveAt) >= ar.For && a.State != notifier.StateFiring {
|
||||
a.State = notifier.StateFiring
|
||||
@@ -376,13 +380,15 @@ func (ar *AlertingRule) execRange(ctx context.Context, start, end time.Time) ([]
|
||||
}
|
||||
result = append(result, ar.alertToTimeSeries(a, s.Timestamps[i])...)
|
||||
|
||||
// save alert's state on last iteration, so it can be used on the next execRange call
|
||||
if at.Equal(end) {
|
||||
// if for>0, save alert's state on last iteration, so it can be used on the next execRange call
|
||||
if ar.For > 0 && at.Equal(end) {
|
||||
holdAlertState[alertID] = a
|
||||
}
|
||||
}
|
||||
}
|
||||
ar.alerts = holdAlertState
|
||||
if len(holdAlertState) > 0 {
|
||||
ar.alerts = holdAlertState
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -428,9 +434,22 @@ 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, as, err := ar.expandTemplates(m, qFn, ts)
|
||||
ls, err := ar.expandLabelTemplates(m)
|
||||
if err != nil {
|
||||
curState.Err = fmt.Errorf("failed to expand templates: %w", err)
|
||||
curState.Err = err
|
||||
return nil, curState.Err
|
||||
}
|
||||
at := ts
|
||||
alertID := hash(ls.processed)
|
||||
if a, ok := ar.alerts[alertID]; ok {
|
||||
// modify activeAt for annotation templating if the alert has already triggered(in state Pending or Firing)
|
||||
if a.State != notifier.StateInactive {
|
||||
at = a.ActiveAt
|
||||
}
|
||||
}
|
||||
as, err := ar.expandAnnotationTemplates(m, qFn, at, ls)
|
||||
if err != nil {
|
||||
curState.Err = err
|
||||
return nil, curState.Err
|
||||
}
|
||||
expandedLabels[i] = ls
|
||||
@@ -473,6 +492,7 @@ func (ar *AlertingRule) exec(ctx context.Context, ts time.Time, limit int) ([]pr
|
||||
a.KeepFiringSince = time.Time{}
|
||||
continue
|
||||
}
|
||||
|
||||
a := ar.newAlert(m, ts, labels.processed, annotations)
|
||||
a.ID = alertID
|
||||
a.State = notifier.StatePending
|
||||
@@ -536,12 +556,18 @@ func (ar *AlertingRule) exec(ctx context.Context, ts time.Time, limit int) ([]pr
|
||||
return append(tss, ar.toTimeSeries(ts.Unix())...), nil
|
||||
}
|
||||
|
||||
func (ar *AlertingRule) expandTemplates(m datasource.Metric, qFn templates.QueryFn, ts time.Time) (*labelSet, map[string]string, 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, nil, fmt.Errorf("failed to expand labels: %w", err)
|
||||
return nil, fmt.Errorf("failed to expand label templates: %s", err)
|
||||
}
|
||||
return ls, nil
|
||||
}
|
||||
|
||||
func (ar *AlertingRule) expandAnnotationTemplates(m datasource.Metric, qFn templates.QueryFn, activeAt time.Time, ls *labelSet) (map[string]string, error) {
|
||||
tplData := notifier.AlertTplData{
|
||||
Value: m.Values[0],
|
||||
Type: ar.Type.String(),
|
||||
@@ -549,14 +575,14 @@ func (ar *AlertingRule) expandTemplates(m datasource.Metric, qFn templates.Query
|
||||
Expr: ar.Expr,
|
||||
AlertID: hash(ls.processed),
|
||||
GroupID: ar.GroupID,
|
||||
ActiveAt: ts,
|
||||
ActiveAt: activeAt,
|
||||
For: ar.For,
|
||||
}
|
||||
as, err := notifier.ExecTemplate(qFn, ar.Annotations, tplData)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to template annotations: %w", err)
|
||||
return nil, fmt.Errorf("failed to expand annotation templates: %s", err)
|
||||
}
|
||||
return ls, as, nil
|
||||
return as, nil
|
||||
}
|
||||
|
||||
// toTimeSeries creates `ALERTS` and `ALERTS_FOR_STATE` for active alerts
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
@@ -267,8 +268,15 @@ func TestAlertingRule_Exec(t *testing.T) {
|
||||
if got.State != exp.State {
|
||||
t.Fatalf("evalIndex %d: expected state %d; got %d", i, exp.State, got.State)
|
||||
}
|
||||
if rule.Annotations != nil && exp.Annotations != nil {
|
||||
if !reflect.DeepEqual(got.Annotations, exp.Annotations) {
|
||||
t.Fatalf("evalIndex %d: expected annotations %v; got %v", i, exp.Annotations, got.Annotations)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// reset ts for next test
|
||||
ts, _ = time.Parse(time.RFC3339, "2024-10-29T00:00:00Z")
|
||||
}
|
||||
|
||||
f(newTestAlertingRule("empty", 0), [][]datasource.Metric{}, nil, nil)
|
||||
@@ -522,7 +530,7 @@ func TestAlertingRule_Exec(t *testing.T) {
|
||||
},
|
||||
})
|
||||
|
||||
f(newTestAlertingRule("for-pending=>firing=>inactive=>pending=>firing", defaultStep), [][]datasource.Metric{
|
||||
f(newTestAlertingRuleWithCustomFields("for-pending=>firing=>inactive=>pending=>firing", defaultStep, 0, 0, map[string]string{"activeAt": "{{ $activeAt.UnixMilli }}"}), [][]datasource.Metric{
|
||||
{metricWithLabels(t, "name", "foo")},
|
||||
{metricWithLabels(t, "name", "foo")},
|
||||
// empty step to set alert inactive
|
||||
@@ -530,11 +538,11 @@ func TestAlertingRule_Exec(t *testing.T) {
|
||||
{metricWithLabels(t, "name", "foo")},
|
||||
{metricWithLabels(t, "name", "foo")},
|
||||
}, map[int][]testAlert{
|
||||
0: {{labels: []string{"name", "foo"}, alert: ¬ifier.Alert{State: notifier.StatePending}}},
|
||||
1: {{labels: []string{"name", "foo"}, alert: ¬ifier.Alert{State: notifier.StateFiring}}},
|
||||
2: {{labels: []string{"name", "foo"}, alert: ¬ifier.Alert{State: notifier.StateInactive}}},
|
||||
3: {{labels: []string{"name", "foo"}, alert: ¬ifier.Alert{State: notifier.StatePending}}},
|
||||
4: {{labels: []string{"name", "foo"}, alert: ¬ifier.Alert{State: notifier.StateFiring}}},
|
||||
0: {{labels: []string{"name", "foo"}, alert: ¬ifier.Alert{State: notifier.StatePending, Annotations: map[string]string{"activeAt": strconv.FormatInt(ts.UnixMilli(), 10)}}}},
|
||||
1: {{labels: []string{"name", "foo"}, alert: ¬ifier.Alert{State: notifier.StateFiring, Annotations: map[string]string{"activeAt": strconv.FormatInt(ts.UnixMilli(), 10)}}}},
|
||||
2: {{labels: []string{"name", "foo"}, alert: ¬ifier.Alert{State: notifier.StateInactive, Annotations: map[string]string{"activeAt": strconv.FormatInt(ts.UnixMilli(), 10)}}}},
|
||||
3: {{labels: []string{"name", "foo"}, alert: ¬ifier.Alert{State: notifier.StatePending, Annotations: map[string]string{"activeAt": strconv.FormatInt(ts.Add(defaultStep*3).UnixMilli(), 10)}}}},
|
||||
4: {{labels: []string{"name", "foo"}, alert: ¬ifier.Alert{State: notifier.StateFiring, Annotations: map[string]string{"activeAt": strconv.FormatInt(ts.Add(defaultStep*3).UnixMilli(), 10)}}}},
|
||||
}, nil)
|
||||
|
||||
f(newTestAlertingRuleWithCustomFields("for-pending=>firing=>keepfiring=>firing", defaultStep, 0, defaultStep, nil), [][]datasource.Metric{
|
||||
|
||||
@@ -587,6 +587,11 @@ func (g *Group) Replay(start, end time.Time, rw remotewrite.RWClient, maxDataPoi
|
||||
|
||||
func replayRuleRange(r Rule, ri rangeIterator, bar *pb.ProgressBar, rw remotewrite.RWClient, replayRuleRetryAttempts, ruleEvaluationConcurrency int) int {
|
||||
fmt.Printf("> Rule %q (ID: %d)\n", r, r.ID())
|
||||
// alerting rule with for>0 can't be replayed concurrently, since the status change might depend on the previous evaluation
|
||||
// see https://github.com/VictoriaMetrics/VictoriaMetrics/commit/abcb21aa5ee918ba9a4e9cde495dba06e1e9564c
|
||||
if r, ok := r.(*AlertingRule); ok && r.For > 0 {
|
||||
ruleEvaluationConcurrency = 1
|
||||
}
|
||||
sem := make(chan struct{}, ruleEvaluationConcurrency)
|
||||
wg := sync.WaitGroup{}
|
||||
res := make(chan int, int(ri.end.Sub(ri.start)/ri.step)+1)
|
||||
|
||||
@@ -437,7 +437,7 @@ func TestRecordingRuleExec_Negative(t *testing.T) {
|
||||
|
||||
_, err = rr.exec(context.TODO(), time.Now(), 0)
|
||||
if err != nil {
|
||||
t.Fatalf("cannot execute recroding rule: %s", err)
|
||||
t.Fatalf("cannot execute recording rule: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,8 @@ var (
|
||||
{"api/v1/alerts", "list all active alerts"},
|
||||
{"api/v1/notifiers", "list all notifiers"},
|
||||
{fmt.Sprintf("api/v1/alert?%s=<int>&%s=<int>", paramGroupID, paramAlertID), "get alert status by group and alert ID"},
|
||||
{fmt.Sprintf("api/v1/rule?%s=<int>&%s=<int>", paramGroupID, paramRuleID), "get rule status by group and rule ID"},
|
||||
{fmt.Sprintf("api/v1/group?%s=<int>", paramGroupID), "get group status by group ID"},
|
||||
}
|
||||
systemLinks = [][2]string{
|
||||
{"vmalert/groups", "UI"},
|
||||
@@ -195,6 +197,20 @@ func (rh *requestHandler) handler(w http.ResponseWriter, r *http.Request) bool {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(data)
|
||||
return true
|
||||
case "/vmalert/api/v1/group", "/api/v1/group":
|
||||
group, err := rh.getGroup(r)
|
||||
if err != nil {
|
||||
httpserver.Errorf(w, r, "%s", err)
|
||||
return true
|
||||
}
|
||||
data, err := json.Marshal(group)
|
||||
if err != nil {
|
||||
httpserver.Errorf(w, r, "failed to marshal group: %s", err)
|
||||
return true
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(data)
|
||||
return true
|
||||
case "/-/reload":
|
||||
if !httpserver.CheckAuthFlag(w, r, reloadAuthKey) {
|
||||
return true
|
||||
@@ -209,6 +225,18 @@ func (rh *requestHandler) handler(w http.ResponseWriter, r *http.Request) bool {
|
||||
}
|
||||
}
|
||||
|
||||
func (rh *requestHandler) getGroup(r *http.Request) (*apiGroup, error) {
|
||||
groupID, err := strconv.ParseUint(r.FormValue(paramGroupID), 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read %q param: %w", paramGroupID, err)
|
||||
}
|
||||
obj, err := rh.m.groupAPI(groupID)
|
||||
if err != nil {
|
||||
return nil, errResponse(err, http.StatusNotFound)
|
||||
}
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
func (rh *requestHandler) getRule(r *http.Request) (apiRule, error) {
|
||||
groupID, err := strconv.ParseUint(r.FormValue(paramGroupID), 10, 64)
|
||||
if err != nil {
|
||||
@@ -337,12 +365,12 @@ func (rh *requestHandler) groups(rf *rulesFilter) []*apiGroup {
|
||||
rule.Alerts = nil
|
||||
}
|
||||
if rule.LastError != "" {
|
||||
g.Unhealthy++
|
||||
g.unhealthy++
|
||||
} else {
|
||||
g.Healthy++
|
||||
g.healthy++
|
||||
}
|
||||
if isNoMatch(rule) {
|
||||
g.NoMatch++
|
||||
g.noMatch++
|
||||
}
|
||||
filteredRules = append(filteredRules, rule)
|
||||
}
|
||||
@@ -459,8 +487,9 @@ func (rh *requestHandler) listNotifiers() ([]byte, error) {
|
||||
}
|
||||
for _, target := range protoTargets {
|
||||
notifier.Targets = append(notifier.Targets, &apiTarget{
|
||||
Address: target.Addr(),
|
||||
Labels: target.Labels.ToMap(),
|
||||
Address: target.Addr(),
|
||||
Labels: target.Labels.ToMap(),
|
||||
LastError: target.LastError(),
|
||||
})
|
||||
}
|
||||
lr.Data.Notifiers = append(lr.Data.Notifiers, notifier)
|
||||
|
||||
@@ -113,7 +113,7 @@
|
||||
{%= Controls(prefix, currentIcon, currentText, icons, filters, true) %}
|
||||
{% if len(groups) > 0 %}
|
||||
{% for _, g := range groups %}
|
||||
<div id="group-{%s g.ID %}" class="d-flex w-100 border-0 flex-column group-items{% if g.Unhealthy > 0 %} alert-danger{% endif %}">
|
||||
<div id="group-{%s g.ID %}" class="d-flex w-100 border-0 flex-column group-items{% if g.unhealthy > 0 %} alert-danger{% endif %}">
|
||||
<span class="d-flex justify-content-between">
|
||||
<a href="#group-{%s g.ID %}">{%s g.Name %}{% if g.Type != "prometheus" %} ({%s g.Type %}){% endif %} (every {%f.0 g.Interval %}s) #</a>
|
||||
<span
|
||||
@@ -123,9 +123,9 @@
|
||||
data-bs-target="#sub-{%s g.ID %}"
|
||||
>
|
||||
<span class="d-flex gap-2">
|
||||
{% if g.Unhealthy > 0 %}<span class="badge bg-danger" title="Number of rules with status Error">{%d g.Unhealthy %}</span> {% endif %}
|
||||
{% if g.NoMatch > 0 %}<span class="badge bg-warning" title="Number of rules with status NoMatch">{%d g.NoMatch %}</span> {% endif %}
|
||||
<span class="badge bg-success" title="Number of rules with status Ok">{%d g.Healthy %}</span>
|
||||
{% if g.unhealthy > 0 %}<span class="badge bg-danger" title="Number of rules with status Error">{%d g.unhealthy %}</span> {% endif %}
|
||||
{% if g.noMatch > 0 %}<span class="badge bg-warning" title="Number of rules with status NoMatch">{%d g.noMatch %}</span> {% endif %}
|
||||
<span class="badge bg-success" title="Number of rules with status Ok">{%d g.healthy %}</span>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
@@ -363,7 +363,7 @@ func StreamListGroups(qw422016 *qt422016.Writer, r *http.Request, groups []*apiG
|
||||
//line app/vmalert/web.qtpl:116
|
||||
qw422016.N().S(`" class="d-flex w-100 border-0 flex-column group-items`)
|
||||
//line app/vmalert/web.qtpl:116
|
||||
if g.Unhealthy > 0 {
|
||||
if g.unhealthy > 0 {
|
||||
//line app/vmalert/web.qtpl:116
|
||||
qw422016.N().S(` alert-danger`)
|
||||
//line app/vmalert/web.qtpl:116
|
||||
@@ -407,11 +407,11 @@ func StreamListGroups(qw422016 *qt422016.Writer, r *http.Request, groups []*apiG
|
||||
<span class="d-flex gap-2">
|
||||
`)
|
||||
//line app/vmalert/web.qtpl:126
|
||||
if g.Unhealthy > 0 {
|
||||
if g.unhealthy > 0 {
|
||||
//line app/vmalert/web.qtpl:126
|
||||
qw422016.N().S(`<span class="badge bg-danger" title="Number of rules with status Error">`)
|
||||
//line app/vmalert/web.qtpl:126
|
||||
qw422016.N().D(g.Unhealthy)
|
||||
qw422016.N().D(g.unhealthy)
|
||||
//line app/vmalert/web.qtpl:126
|
||||
qw422016.N().S(`</span> `)
|
||||
//line app/vmalert/web.qtpl:126
|
||||
@@ -420,11 +420,11 @@ func StreamListGroups(qw422016 *qt422016.Writer, r *http.Request, groups []*apiG
|
||||
qw422016.N().S(`
|
||||
`)
|
||||
//line app/vmalert/web.qtpl:127
|
||||
if g.NoMatch > 0 {
|
||||
if g.noMatch > 0 {
|
||||
//line app/vmalert/web.qtpl:127
|
||||
qw422016.N().S(`<span class="badge bg-warning" title="Number of rules with status NoMatch">`)
|
||||
//line app/vmalert/web.qtpl:127
|
||||
qw422016.N().D(g.NoMatch)
|
||||
qw422016.N().D(g.noMatch)
|
||||
//line app/vmalert/web.qtpl:127
|
||||
qw422016.N().S(`</span> `)
|
||||
//line app/vmalert/web.qtpl:127
|
||||
@@ -433,7 +433,7 @@ func StreamListGroups(qw422016 *qt422016.Writer, r *http.Request, groups []*apiG
|
||||
qw422016.N().S(`
|
||||
<span class="badge bg-success" title="Number of rules with status Ok">`)
|
||||
//line app/vmalert/web.qtpl:128
|
||||
qw422016.N().D(g.Healthy)
|
||||
qw422016.N().D(g.healthy)
|
||||
//line app/vmalert/web.qtpl:128
|
||||
qw422016.N().S(`</span>
|
||||
</span>
|
||||
|
||||
@@ -25,6 +25,7 @@ func TestHandler(t *testing.T) {
|
||||
m := &manager{groups: map[uint64]*rule.Group{}}
|
||||
var ar *rule.AlertingRule
|
||||
var rr *rule.RecordingRule
|
||||
var groupIDs []uint64
|
||||
for _, dsType := range []string{"prometheus", "", "graphite"} {
|
||||
g := rule.NewGroup(config.Group{
|
||||
Name: "group",
|
||||
@@ -45,7 +46,9 @@ func TestHandler(t *testing.T) {
|
||||
ar = g.Rules[0].(*rule.AlertingRule)
|
||||
rr = g.Rules[1].(*rule.RecordingRule)
|
||||
g.ExecOnce(context.Background(), func() []notifier.Notifier { return nil }, nil, time.Time{})
|
||||
m.groups[g.CreateID()] = g
|
||||
id := g.CreateID()
|
||||
m.groups[id] = g
|
||||
groupIDs = append(groupIDs, id)
|
||||
}
|
||||
rh := &requestHandler{m: m}
|
||||
|
||||
@@ -188,6 +191,21 @@ func TestHandler(t *testing.T) {
|
||||
t.Fatalf("expected %+v to have state updates field not empty", gotRuleWithUpdates.StateUpdates)
|
||||
}
|
||||
})
|
||||
t.Run("/api/v1/group?groupID", func(t *testing.T) {
|
||||
id := groupIDs[0]
|
||||
g := m.groups[id]
|
||||
expGroup := groupToAPI(g)
|
||||
gotGroup := apiGroup{}
|
||||
getResp(t, ts.URL+"/"+expGroup.APILink(), &gotGroup, 200)
|
||||
if expGroup.ID != gotGroup.ID {
|
||||
t.Fatalf("expected to get Group %q; got %q instead", expGroup.ID, gotGroup.ID)
|
||||
}
|
||||
gotGroup = apiGroup{}
|
||||
getResp(t, ts.URL+"/vmalert/"+expGroup.APILink(), &gotGroup, 200)
|
||||
if expGroup.ID != gotGroup.ID {
|
||||
t.Fatalf("expected to get Group %q; got %q instead", expGroup.ID, gotGroup.ID)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("/api/v1/rules&filters", func(t *testing.T) {
|
||||
check := func(url string, statusCode, expGroups, expRules int) {
|
||||
|
||||
@@ -28,6 +28,8 @@ type apiNotifier struct {
|
||||
type apiTarget struct {
|
||||
Address string `json:"address"`
|
||||
Labels map[string]string `json:"labels"`
|
||||
// LastError contains the error faced while sending to notifier.
|
||||
LastError string `json:"lastError"`
|
||||
}
|
||||
|
||||
// apiAlert represents a notifier.AlertingRule state
|
||||
@@ -109,11 +111,16 @@ type apiGroup struct {
|
||||
// EvalDelay will adjust the `time` parameter of rule evaluation requests to compensate intentional query delay from datasource.
|
||||
EvalDelay float64 `json:"eval_delay,omitempty"`
|
||||
// Unhealthy unhealthy rules count
|
||||
Unhealthy int
|
||||
unhealthy int
|
||||
// Healthy passing rules count
|
||||
Healthy int
|
||||
healthy int
|
||||
// NoMatch not matching rules count
|
||||
NoMatch int
|
||||
noMatch int
|
||||
}
|
||||
|
||||
// APILink returns a link to the group's JSON representation.
|
||||
func (ag *apiGroup) APILink() string {
|
||||
return fmt.Sprintf("api/v1/group?%s=%s", paramGroupID, ag.ID)
|
||||
}
|
||||
|
||||
// groupAlerts represents a group of alerts for WEB view
|
||||
|
||||
@@ -1,106 +1,110 @@
|
||||
# All these commands must run from repository root.
|
||||
|
||||
# special tag to reduce resulting binary size
|
||||
# See this issue https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8008
|
||||
VMBACKUP_GO_BUILD_TAGS=disable_grpc_modules
|
||||
|
||||
vmbackup:
|
||||
APP_NAME=vmbackup $(MAKE) app-local
|
||||
APP_NAME=vmbackup EXTRA_GO_BUILD_TAGS=$(VMBACKUP_GO_BUILD_TAGS) $(MAKE) app-local
|
||||
|
||||
vmbackup-race:
|
||||
APP_NAME=vmbackup RACE=-race $(MAKE) app-local
|
||||
APP_NAME=vmbackup EXTRA_GO_BUILD_TAGS=$(VMBACKUP_GO_BUILD_TAGS) RACE=-race $(MAKE) app-local
|
||||
|
||||
vmbackup-prod:
|
||||
APP_NAME=vmbackup $(MAKE) app-via-docker
|
||||
APP_NAME=vmbackup EXTRA_GO_BUILD_TAGS=$(VMBACKUP_GO_BUILD_TAGS) $(MAKE) app-via-docker
|
||||
|
||||
vmbackup-pure-prod:
|
||||
APP_NAME=vmbackup $(MAKE) app-via-docker-pure
|
||||
APP_NAME=vmbackup EXTRA_GO_BUILD_TAGS=$(VMBACKUP_GO_BUILD_TAGS) $(MAKE) app-via-docker-pure
|
||||
|
||||
vmbackup-linux-amd64-prod:
|
||||
APP_NAME=vmbackup $(MAKE) app-via-docker-linux-amd64
|
||||
APP_NAME=vmbackup EXTRA_GO_BUILD_TAGS=$(VMBACKUP_GO_BUILD_TAGS) $(MAKE) app-via-docker-linux-amd64
|
||||
|
||||
vmbackup-linux-arm-prod:
|
||||
APP_NAME=vmbackup $(MAKE) app-via-docker-linux-arm
|
||||
APP_NAME=vmbackup EXTRA_GO_BUILD_TAGS=$(VMBACKUP_GO_BUILD_TAGS) $(MAKE) app-via-docker-linux-arm
|
||||
|
||||
vmbackup-linux-arm64-prod:
|
||||
APP_NAME=vmbackup $(MAKE) app-via-docker-linux-arm64
|
||||
APP_NAME=vmbackup EXTRA_GO_BUILD_TAGS=$(VMBACKUP_GO_BUILD_TAGS) $(MAKE) app-via-docker-linux-arm64
|
||||
|
||||
vmbackup-linux-ppc64le-prod:
|
||||
APP_NAME=vmbackup $(MAKE) app-via-docker-linux-ppc64le
|
||||
APP_NAME=vmbackup EXTRA_GO_BUILD_TAGS=$(VMBACKUP_GO_BUILD_TAGS) $(MAKE) app-via-docker-linux-ppc64le
|
||||
|
||||
vmbackup-linux-386-prod:
|
||||
APP_NAME=vmbackup $(MAKE) app-via-docker-linux-386
|
||||
APP_NAME=vmbackup EXTRA_GO_BUILD_TAGS=$(VMBACKUP_GO_BUILD_TAGS) $(MAKE) app-via-docker-linux-386
|
||||
|
||||
vmbackup-darwin-amd64-prod:
|
||||
APP_NAME=vmbackup $(MAKE) app-via-docker-darwin-amd64
|
||||
APP_NAME=vmbackup EXTRA_GO_BUILD_TAGS=$(VMBACKUP_GO_BUILD_TAGS) $(MAKE) app-via-docker-darwin-amd64
|
||||
|
||||
vmbackup-darwin-arm64-prod:
|
||||
APP_NAME=vmbackup $(MAKE) app-via-docker-darwin-arm64
|
||||
APP_NAME=vmbackup EXTRA_GO_BUILD_TAGS=$(VMBACKUP_GO_BUILD_TAGS) $(MAKE) app-via-docker-darwin-arm64
|
||||
|
||||
vmbackup-freebsd-amd64-prod:
|
||||
APP_NAME=vmbackup $(MAKE) app-via-docker-freebsd-amd64
|
||||
APP_NAME=vmbackup EXTRA_GO_BUILD_TAGS=$(VMBACKUP_GO_BUILD_TAGS) $(MAKE) app-via-docker-freebsd-amd64
|
||||
|
||||
vmbackup-openbsd-amd64-prod:
|
||||
APP_NAME=vmbackup $(MAKE) app-via-docker-openbsd-amd64
|
||||
APP_NAME=vmbackup EXTRA_GO_BUILD_TAGS=$(VMBACKUP_GO_BUILD_TAGS) $(MAKE) app-via-docker-openbsd-amd64
|
||||
|
||||
vmbackup-windows-amd64-prod:
|
||||
APP_NAME=vmbackup $(MAKE) app-via-docker-windows-amd64
|
||||
APP_NAME=vmbackup EXTRA_GO_BUILD_TAGS=$(VMBACKUP_GO_BUILD_TAGS) $(MAKE) app-via-docker-windows-amd64
|
||||
|
||||
package-vmbackup:
|
||||
APP_NAME=vmbackup $(MAKE) package-via-docker
|
||||
APP_NAME=vmbackup EXTRA_GO_BUILD_TAGS=$(VMBACKUP_GO_BUILD_TAGS) $(MAKE) package-via-docker
|
||||
|
||||
package-vmbackup-pure:
|
||||
APP_NAME=vmbackup $(MAKE) package-via-docker-pure
|
||||
APP_NAME=vmbackup EXTRA_GO_BUILD_TAGS=$(VMBACKUP_GO_BUILD_TAGS) $(MAKE) package-via-docker-pure
|
||||
|
||||
package-vmbackup-amd64:
|
||||
APP_NAME=vmbackup $(MAKE) package-via-docker-amd64
|
||||
APP_NAME=vmbackup EXTRA_GO_BUILD_TAGS=$(VMBACKUP_GO_BUILD_TAGS) $(MAKE) package-via-docker-amd64
|
||||
|
||||
package-vmbackup-arm:
|
||||
APP_NAME=vmbackup $(MAKE) package-via-docker-arm
|
||||
APP_NAME=vmbackup EXTRA_GO_BUILD_TAGS=$(VMBACKUP_GO_BUILD_TAGS) $(MAKE) package-via-docker-arm
|
||||
|
||||
package-vmbackup-arm64:
|
||||
APP_NAME=vmbackup $(MAKE) package-via-docker-arm64
|
||||
APP_NAME=vmbackup EXTRA_GO_BUILD_TAGS=$(VMBACKUP_GO_BUILD_TAGS) $(MAKE) package-via-docker-arm64
|
||||
|
||||
package-vmbackup-ppc64le:
|
||||
APP_NAME=vmbackup $(MAKE) package-via-docker-ppc64le
|
||||
APP_NAME=vmbackup EXTRA_GO_BUILD_TAGS=$(VMBACKUP_GO_BUILD_TAGS) $(MAKE) package-via-docker-ppc64le
|
||||
|
||||
package-vmbackup-386:
|
||||
APP_NAME=vmbackup $(MAKE) package-via-docker-386
|
||||
APP_NAME=vmbackup EXTRA_GO_BUILD_TAGS=$(VMBACKUP_GO_BUILD_TAGS) $(MAKE) package-via-docker-386
|
||||
|
||||
publish-vmbackup:
|
||||
APP_NAME=vmbackup $(MAKE) publish-via-docker
|
||||
APP_NAME=vmbackup EXTRA_GO_BUILD_TAGS=$(VMBACKUP_GO_BUILD_TAGS) $(MAKE) publish-via-docker
|
||||
|
||||
vmbackup-linux-amd64:
|
||||
APP_NAME=vmbackup CGO_ENABLED=1 GOOS=linux GOARCH=amd64 $(MAKE) app-local-goos-goarch
|
||||
APP_NAME=vmbackup EXTRA_GO_BUILD_TAGS=$(VMBACKUP_GO_BUILD_TAGS) CGO_ENABLED=1 GOOS=linux GOARCH=amd64 $(MAKE) app-local-goos-goarch
|
||||
|
||||
vmbackup-linux-arm:
|
||||
APP_NAME=vmbackup CGO_ENABLED=0 GOOS=linux GOARCH=arm $(MAKE) app-local-goos-goarch
|
||||
APP_NAME=vmbackup EXTRA_GO_BUILD_TAGS=$(VMBACKUP_GO_BUILD_TAGS) CGO_ENABLED=0 GOOS=linux GOARCH=arm $(MAKE) app-local-goos-goarch
|
||||
|
||||
vmbackup-linux-arm64:
|
||||
APP_NAME=vmbackup CGO_ENABLED=0 GOOS=linux GOARCH=arm64 $(MAKE) app-local-goos-goarch
|
||||
APP_NAME=vmbackup EXTRA_GO_BUILD_TAGS=$(VMBACKUP_GO_BUILD_TAGS) CGO_ENABLED=0 GOOS=linux GOARCH=arm64 $(MAKE) app-local-goos-goarch
|
||||
|
||||
vmbackup-linux-ppc64le:
|
||||
APP_NAME=vmbackup CGO_ENABLED=0 GOOS=linux GOARCH=ppc64le $(MAKE) app-local-goos-goarch
|
||||
APP_NAME=vmbackup EXTRA_GO_BUILD_TAGS=$(VMBACKUP_GO_BUILD_TAGS) CGO_ENABLED=0 GOOS=linux GOARCH=ppc64le $(MAKE) app-local-goos-goarch
|
||||
|
||||
vmbackup-linux-s390x:
|
||||
APP_NAME=vmbackup CGO_ENABLED=0 GOOS=linux GOARCH=s390x $(MAKE) app-local-goos-goarch
|
||||
APP_NAME=vmbackup EXTRA_GO_BUILD_TAGS=$(VMBACKUP_GO_BUILD_TAGS) CGO_ENABLED=0 GOOS=linux GOARCH=s390x $(MAKE) app-local-goos-goarch
|
||||
|
||||
vmbackup-linux-loong64:
|
||||
APP_NAME=vmbackup CGO_ENABLED=0 GOOS=linux GOARCH=loong64 $(MAKE) app-local-goos-goarch
|
||||
APP_NAME=vmbackup EXTRA_GO_BUILD_TAGS=$(VMBACKUP_GO_BUILD_TAGS) CGO_ENABLED=0 GOOS=linux GOARCH=loong64 $(MAKE) app-local-goos-goarch
|
||||
|
||||
vmbackup-linux-386:
|
||||
APP_NAME=vmbackup CGO_ENABLED=0 GOOS=linux GOARCH=386 $(MAKE) app-local-goos-goarch
|
||||
APP_NAME=vmbackup EXTRA_GO_BUILD_TAGS=$(VMBACKUP_GO_BUILD_TAGS) CGO_ENABLED=0 GOOS=linux GOARCH=386 $(MAKE) app-local-goos-goarch
|
||||
|
||||
vmbackup-darwin-amd64:
|
||||
APP_NAME=vmbackup CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 $(MAKE) app-local-goos-goarch
|
||||
APP_NAME=vmbackup EXTRA_GO_BUILD_TAGS=$(VMBACKUP_GO_BUILD_TAGS) CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 $(MAKE) app-local-goos-goarch
|
||||
|
||||
vmbackup-darwin-arm64:
|
||||
APP_NAME=vmbackup CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 $(MAKE) app-local-goos-goarch
|
||||
APP_NAME=vmbackup EXTRA_GO_BUILD_TAGS=$(VMBACKUP_GO_BUILD_TAGS) CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 $(MAKE) app-local-goos-goarch
|
||||
|
||||
vmbackup-freebsd-amd64:
|
||||
APP_NAME=vmbackup CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 $(MAKE) app-local-goos-goarch
|
||||
APP_NAME=vmbackup EXTRA_GO_BUILD_TAGS=$(VMBACKUP_GO_BUILD_TAGS) CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 $(MAKE) app-local-goos-goarch
|
||||
|
||||
vmbackup-openbsd-amd64:
|
||||
APP_NAME=vmbackup CGO_ENABLED=0 GOOS=openbsd GOARCH=amd64 $(MAKE) app-local-goos-goarch
|
||||
APP_NAME=vmbackup EXTRA_GO_BUILD_TAGS=$(VMBACKUP_GO_BUILD_TAGS) CGO_ENABLED=0 GOOS=openbsd GOARCH=amd64 $(MAKE) app-local-goos-goarch
|
||||
|
||||
vmbackup-windows-amd64:
|
||||
GOARCH=amd64 APP_NAME=vmbackup $(MAKE) app-local-windows-goarch
|
||||
GOARCH=amd64 APP_NAME=vmbackup EXTRA_GO_BUILD_TAGS=$(VMBACKUP_GO_BUILD_TAGS) $(MAKE) app-local-windows-goarch
|
||||
|
||||
vmbackup-pure:
|
||||
APP_NAME=vmbackup $(MAKE) app-local-pure
|
||||
APP_NAME=vmbackup EXTRA_GO_BUILD_TAGS=$(VMBACKUP_GO_BUILD_TAGS) $(MAKE) app-local-pure
|
||||
|
||||
@@ -121,7 +121,7 @@ func (p *vmNativeProcessor) runSingle(ctx context.Context, f native.Filter, srcU
|
||||
pr := bar.NewProxyReader(reader)
|
||||
if pr != nil {
|
||||
reader = pr
|
||||
fmt.Printf("Continue import process with filter %s:\n", f.String())
|
||||
fmt.Fprintf(log.Writer(), "Continue import process with filter %s:\n", f.String())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,7 +191,7 @@ func (p *vmNativeProcessor) runBackfilling(ctx context.Context, tenantID string,
|
||||
initParams = []any{srcURL, dstURL, p.filter.String(), tenantID}
|
||||
}
|
||||
|
||||
fmt.Println("") // extra line for better output formatting
|
||||
fmt.Fprintln(log.Writer(), "") // extra line for better output formatting
|
||||
log.Printf(initMessage, initParams...)
|
||||
if len(ranges) > 1 {
|
||||
log.Printf("Selected time range will be split into %d ranges according to %q step", len(ranges), p.filter.Chunk)
|
||||
|
||||
@@ -1,106 +1,110 @@
|
||||
# All these commands must run from repository root.
|
||||
|
||||
# special tag to reduce resulting binary size
|
||||
# See this issue https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8008
|
||||
VMRESTORE_GO_BUILD_TAGS=disable_grpc_modules
|
||||
|
||||
vmrestore:
|
||||
APP_NAME=vmrestore $(MAKE) app-local
|
||||
APP_NAME=vmrestore EXTRA_GO_BUILD_TAGS=$(VMRESTORE_GO_BUILD_TAGS) $(MAKE) app-local
|
||||
|
||||
vmrestore-race:
|
||||
APP_NAME=vmrestore RACE=-race $(MAKE) app-local
|
||||
APP_NAME=vmrestore EXTRA_GO_BUILD_TAGS=$(VMRESTORE_GO_BUILD_TAGS) RACE=-race $(MAKE) app-local
|
||||
|
||||
vmrestore-prod:
|
||||
APP_NAME=vmrestore $(MAKE) app-via-docker
|
||||
APP_NAME=vmrestore EXTRA_GO_BUILD_TAGS=$(VMRESTORE_GO_BUILD_TAGS) $(MAKE) app-via-docker
|
||||
|
||||
vmrestore-pure-prod:
|
||||
APP_NAME=vmrestore $(MAKE) app-via-docker-pure
|
||||
APP_NAME=vmrestore EXTRA_GO_BUILD_TAGS=$(VMRESTORE_GO_BUILD_TAGS) $(MAKE) app-via-docker-pure
|
||||
|
||||
vmrestore-linux-amd64-prod:
|
||||
APP_NAME=vmrestore $(MAKE) app-via-docker-linux-amd64
|
||||
APP_NAME=vmrestore EXTRA_GO_BUILD_TAGS=$(VMRESTORE_GO_BUILD_TAGS) $(MAKE) app-via-docker-linux-amd64
|
||||
|
||||
vmrestore-linux-arm-prod:
|
||||
APP_NAME=vmrestore $(MAKE) app-via-docker-linux-arm
|
||||
APP_NAME=vmrestore EXTRA_GO_BUILD_TAGS=$(VMRESTORE_GO_BUILD_TAGS) $(MAKE) app-via-docker-linux-arm
|
||||
|
||||
vmrestore-linux-arm64-prod:
|
||||
APP_NAME=vmrestore $(MAKE) app-via-docker-linux-arm64
|
||||
APP_NAME=vmrestore EXTRA_GO_BUILD_TAGS=$(VMRESTORE_GO_BUILD_TAGS) $(MAKE) app-via-docker-linux-arm64
|
||||
|
||||
vmrestore-linux-ppc64le-prod:
|
||||
APP_NAME=vmrestore $(MAKE) app-via-docker-linux-ppc64le
|
||||
APP_NAME=vmrestore EXTRA_GO_BUILD_TAGS=$(VMRESTORE_GO_BUILD_TAGS) $(MAKE) app-via-docker-linux-ppc64le
|
||||
|
||||
vmrestore-linux-386-prod:
|
||||
APP_NAME=vmrestore $(MAKE) app-via-docker-linux-386
|
||||
APP_NAME=vmrestore EXTRA_GO_BUILD_TAGS=$(VMRESTORE_GO_BUILD_TAGS) $(MAKE) app-via-docker-linux-386
|
||||
|
||||
vmrestore-darwin-amd64-prod:
|
||||
APP_NAME=vmrestore $(MAKE) app-via-docker-darwin-amd64
|
||||
APP_NAME=vmrestore EXTRA_GO_BUILD_TAGS=$(VMRESTORE_GO_BUILD_TAGS) $(MAKE) app-via-docker-darwin-amd64
|
||||
|
||||
vmrestore-darwin-arm64-prod:
|
||||
APP_NAME=vmrestore $(MAKE) app-via-docker-darwin-arm64
|
||||
APP_NAME=vmrestore EXTRA_GO_BUILD_TAGS=$(VMRESTORE_GO_BUILD_TAGS) $(MAKE) app-via-docker-darwin-arm64
|
||||
|
||||
vmrestore-freebsd-amd64-prod:
|
||||
APP_NAME=vmrestore $(MAKE) app-via-docker-freebsd-amd64
|
||||
APP_NAME=vmrestore EXTRA_GO_BUILD_TAGS=$(VMRESTORE_GO_BUILD_TAGS) $(MAKE) app-via-docker-freebsd-amd64
|
||||
|
||||
vmrestore-openbsd-amd64-prod:
|
||||
APP_NAME=vmrestore $(MAKE) app-via-docker-openbsd-amd64
|
||||
APP_NAME=vmrestore EXTRA_GO_BUILD_TAGS=$(VMRESTORE_GO_BUILD_TAGS) $(MAKE) app-via-docker-openbsd-amd64
|
||||
|
||||
vmrestore-windows-amd64-prod:
|
||||
APP_NAME=vmrestore $(MAKE) app-via-docker-windows-amd64
|
||||
APP_NAME=vmrestore EXTRA_GO_BUILD_TAGS=$(VMRESTORE_GO_BUILD_TAGS) $(MAKE) app-via-docker-windows-amd64
|
||||
|
||||
package-vmrestore:
|
||||
APP_NAME=vmrestore $(MAKE) package-via-docker
|
||||
APP_NAME=vmrestore EXTRA_GO_BUILD_TAGS=$(VMRESTORE_GO_BUILD_TAGS) $(MAKE) package-via-docker
|
||||
|
||||
package-vmrestore-pure:
|
||||
APP_NAME=vmrestore $(MAKE) package-via-docker-pure
|
||||
APP_NAME=vmrestore EXTRA_GO_BUILD_TAGS=$(VMRESTORE_GO_BUILD_TAGS) $(MAKE) package-via-docker-pure
|
||||
|
||||
package-vmrestore-amd64:
|
||||
APP_NAME=vmrestore $(MAKE) package-via-docker-amd64
|
||||
APP_NAME=vmrestore EXTRA_GO_BUILD_TAGS=$(VMRESTORE_GO_BUILD_TAGS) $(MAKE) package-via-docker-amd64
|
||||
|
||||
package-vmrestore-arm:
|
||||
APP_NAME=vmrestore $(MAKE) package-via-docker-arm
|
||||
APP_NAME=vmrestore EXTRA_GO_BUILD_TAGS=$(VMRESTORE_GO_BUILD_TAGS) $(MAKE) package-via-docker-arm
|
||||
|
||||
package-vmrestore-arm64:
|
||||
APP_NAME=vmrestore $(MAKE) package-via-docker-arm64
|
||||
APP_NAME=vmrestore EXTRA_GO_BUILD_TAGS=$(VMRESTORE_GO_BUILD_TAGS) $(MAKE) package-via-docker-arm64
|
||||
|
||||
package-vmrestore-ppc64le:
|
||||
APP_NAME=vmrestore $(MAKE) package-via-docker-ppc64le
|
||||
APP_NAME=vmrestore EXTRA_GO_BUILD_TAGS=$(VMRESTORE_GO_BUILD_TAGS) $(MAKE) package-via-docker-ppc64le
|
||||
|
||||
package-vmrestore-386:
|
||||
APP_NAME=vmrestore $(MAKE) package-via-docker-386
|
||||
APP_NAME=vmrestore EXTRA_GO_BUILD_TAGS=$(VMRESTORE_GO_BUILD_TAGS) $(MAKE) package-via-docker-386
|
||||
|
||||
publish-vmrestore:
|
||||
APP_NAME=vmrestore $(MAKE) publish-via-docker
|
||||
APP_NAME=vmrestore EXTRA_GO_BUILD_TAGS=$(VMRESTORE_GO_BUILD_TAGS) $(MAKE) publish-via-docker
|
||||
|
||||
vmrestore-linux-amd64:
|
||||
APP_NAME=vmrestore CGO_ENABLED=1 GOOS=linux GOARCH=amd64 $(MAKE) app-local-goos-goarch
|
||||
APP_NAME=vmrestore EXTRA_GO_BUILD_TAGS=$(VMRESTORE_GO_BUILD_TAGS) CGO_ENABLED=1 GOOS=linux GOARCH=amd64 $(MAKE) app-local-goos-goarch
|
||||
|
||||
vmrestore-linux-arm:
|
||||
APP_NAME=vmrestore CGO_ENABLED=0 GOOS=linux GOARCH=arm $(MAKE) app-local-goos-goarch
|
||||
APP_NAME=vmrestore EXTRA_GO_BUILD_TAGS=$(VMRESTORE_GO_BUILD_TAGS) CGO_ENABLED=0 GOOS=linux GOARCH=arm $(MAKE) app-local-goos-goarch
|
||||
|
||||
vmrestore-linux-arm64:
|
||||
APP_NAME=vmrestore CGO_ENABLED=0 GOOS=linux GOARCH=arm64 $(MAKE) app-local-goos-goarch
|
||||
APP_NAME=vmrestore EXTRA_GO_BUILD_TAGS=$(VMRESTORE_GO_BUILD_TAGS) CGO_ENABLED=0 GOOS=linux GOARCH=arm64 $(MAKE) app-local-goos-goarch
|
||||
|
||||
vmrestore-linux-ppc64le:
|
||||
APP_NAME=vmrestore CGO_ENABLED=0 GOOS=linux GOARCH=ppc64le $(MAKE) app-local-goos-goarch
|
||||
APP_NAME=vmrestore EXTRA_GO_BUILD_TAGS=$(VMRESTORE_GO_BUILD_TAGS) CGO_ENABLED=0 GOOS=linux GOARCH=ppc64le $(MAKE) app-local-goos-goarch
|
||||
|
||||
vmrestore-linux-s390x:
|
||||
APP_NAME=vmrestore CGO_ENABLED=0 GOOS=linux GOARCH=s390x $(MAKE) app-local-goos-goarch
|
||||
APP_NAME=vmrestore EXTRA_GO_BUILD_TAGS=$(VMRESTORE_GO_BUILD_TAGS) CGO_ENABLED=0 GOOS=linux GOARCH=s390x $(MAKE) app-local-goos-goarch
|
||||
|
||||
vmrestore-linux-loong64:
|
||||
APP_NAME=vmrestore CGO_ENABLED=0 GOOS=linux GOARCH=loong64 $(MAKE) app-local-goos-goarch
|
||||
APP_NAME=vmrestore EXTRA_GO_BUILD_TAGS=$(VMRESTORE_GO_BUILD_TAGS) CGO_ENABLED=0 GOOS=linux GOARCH=loong64 $(MAKE) app-local-goos-goarch
|
||||
|
||||
vmrestore-linux-386:
|
||||
APP_NAME=vmrestore CGO_ENABLED=0 GOOS=linux GOARCH=386 $(MAKE) app-local-goos-goarch
|
||||
APP_NAME=vmrestore EXTRA_GO_BUILD_TAGS=$(VMRESTORE_GO_BUILD_TAGS) CGO_ENABLED=0 GOOS=linux GOARCH=386 $(MAKE) app-local-goos-goarch
|
||||
|
||||
vmrestore-darwin-amd64:
|
||||
APP_NAME=vmrestore CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 $(MAKE) app-local-goos-goarch
|
||||
APP_NAME=vmrestore EXTRA_GO_BUILD_TAGS=$(VMRESTORE_GO_BUILD_TAGS) CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 $(MAKE) app-local-goos-goarch
|
||||
|
||||
vmrestore-darwin-arm64:
|
||||
APP_NAME=vmrestore CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 $(MAKE) app-local-goos-goarch
|
||||
APP_NAME=vmrestore EXTRA_GO_BUILD_TAGS=$(VMRESTORE_GO_BUILD_TAGS) CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 $(MAKE) app-local-goos-goarch
|
||||
|
||||
vmrestore-freebsd-amd64:
|
||||
APP_NAME=vmrestore CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 $(MAKE) app-local-goos-goarch
|
||||
APP_NAME=vmrestore EXTRA_GO_BUILD_TAGS=$(VMRESTORE_GO_BUILD_TAGS) CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 $(MAKE) app-local-goos-goarch
|
||||
|
||||
vmrestore-openbsd-amd64:
|
||||
APP_NAME=vmrestore CGO_ENABLED=0 GOOS=openbsd GOARCH=amd64 $(MAKE) app-local-goos-goarch
|
||||
APP_NAME=vmrestore EXTRA_GO_BUILD_TAGS=$(VMRESTORE_GO_BUILD_TAGS) CGO_ENABLED=0 GOOS=openbsd GOARCH=amd64 $(MAKE) app-local-goos-goarch
|
||||
|
||||
vmrestore-windows-amd64:
|
||||
GOARCH=amd64 APP_NAME=vmrestore $(MAKE) app-local-windows-goarch
|
||||
GOARCH=amd64 APP_NAME=vmrestore EXTRA_GO_BUILD_TAGS=$(VMRESTORE_GO_BUILD_TAGS) $(MAKE) app-local-windows-goarch
|
||||
|
||||
vmrestore-pure:
|
||||
APP_NAME=vmrestore $(MAKE) app-local-pure
|
||||
APP_NAME=vmrestore EXTRA_GO_BUILD_TAGS=$(VMRESTORE_GO_BUILD_TAGS) $(MAKE) app-local-pure
|
||||
|
||||
@@ -10,7 +10,7 @@ func TestParseIntervalSuccess(t *testing.T) {
|
||||
t.Helper()
|
||||
interval, err := parseInterval(s)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error in parseInterva(%q): %s", s, err)
|
||||
t.Fatalf("unexpected error in parseInterval(%q): %s", s, err)
|
||||
}
|
||||
if interval != intervalExpected {
|
||||
t.Fatalf("unexpected result for parseInterval(%q); got %d; want %d", s, interval, intervalExpected)
|
||||
|
||||
@@ -17,7 +17,7 @@ func TestScanStringSuccess(t *testing.T) {
|
||||
t.Fatalf("unexpected string scanned from %s; got %s; want %s", s, result, sExpected)
|
||||
}
|
||||
if !strings.HasPrefix(s, result) {
|
||||
t.Fatalf("invalid prefix for scanne string %s: %s", s, result)
|
||||
t.Fatalf("invalid prefix for scanned string %s: %s", s, result)
|
||||
}
|
||||
}
|
||||
f(`""`, `""`)
|
||||
|
||||
@@ -210,7 +210,7 @@ func (p *parser) parseMetricExprOrFuncCall() (Expr, error) {
|
||||
}
|
||||
return fe, nil
|
||||
default:
|
||||
// Metric epxression or bool expression or None.
|
||||
// Metric expression or bool expression or None.
|
||||
if isBool(ident) {
|
||||
be := &BoolExpr{
|
||||
B: strings.EqualFold(ident, "true"),
|
||||
|
||||
@@ -269,7 +269,7 @@ func (rss *Results) runParallel(qt *querytracer.Tracer, f func(rs *Result, worke
|
||||
}
|
||||
|
||||
// Slow path - spin up multiple local workers for parallel data processing.
|
||||
// Do not use global workers pool, since it increases inter-CPU memory ping-poing,
|
||||
// Do not use global workers pool, since it increases inter-CPU memory ping-pong,
|
||||
// which reduces the scalability on systems with many CPU cores.
|
||||
|
||||
// Prepare the work for workers.
|
||||
@@ -485,7 +485,7 @@ func (pts *packedTimeseries) unpackTo(dst []*sortBlock, tbf *tmpBlocksFile, tr s
|
||||
}
|
||||
|
||||
// Slow path - spin up multiple local workers for parallel data unpacking.
|
||||
// Do not use global workers pool, since it increases inter-CPU memory ping-poing,
|
||||
// Do not use global workers pool, since it increases inter-CPU memory ping-pong,
|
||||
// which reduces the scalability on systems with many CPU cores.
|
||||
|
||||
// Prepare the work for workers.
|
||||
|
||||
@@ -135,7 +135,7 @@ func (tbf *tmpBlocksFile) WriteBlockRefData(b []byte) (tmpBlockAddr, error) {
|
||||
return addr, nil
|
||||
}
|
||||
|
||||
// Len() returnt tbf size in bytes.
|
||||
// Len() return tbf size in bytes.
|
||||
func (tbf *tmpBlocksFile) Len() uint64 {
|
||||
return tbf.offset
|
||||
}
|
||||
|
||||
@@ -188,7 +188,7 @@ func newBinaryOpFunc(bf func(left, right float64, isBool bool) float64) binaryOp
|
||||
rightValues := right[i].Values
|
||||
dstValues := dst[i].Values
|
||||
if len(leftValues) != len(rightValues) || len(leftValues) != len(dstValues) {
|
||||
logger.Panicf("BUG: len(leftVaues) must match len(rightValues) and len(dstValues); got %d vs %d vs %d",
|
||||
logger.Panicf("BUG: len(leftValues) must match len(rightValues) and len(dstValues); got %d vs %d vs %d",
|
||||
len(leftValues), len(rightValues), len(dstValues))
|
||||
}
|
||||
for j, a := range leftValues {
|
||||
|
||||
@@ -55,7 +55,7 @@ func TestValidateMaxPointsPerSeriesFailure(t *testing.T) {
|
||||
f := func(start, end, step int64, maxPoints int) {
|
||||
t.Helper()
|
||||
if err := ValidateMaxPointsPerSeries(start, end, step, maxPoints); err == nil {
|
||||
t.Fatalf("expecint non-nil error for ValidateMaxPointsPerSeries(start=%d, end=%d, step=%d, maxPoints=%d)", start, end, step, maxPoints)
|
||||
t.Fatalf("expecting non-nil error for ValidateMaxPointsPerSeries(start=%d, end=%d, step=%d, maxPoints=%d)", start, end, step, maxPoints)
|
||||
}
|
||||
}
|
||||
// zero step
|
||||
|
||||
@@ -2443,13 +2443,14 @@ func rollupFake(_ *rollupFuncArg) float64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// getScalar expects result from a [scalar](https://prometheus.io/docs/prometheus/latest/querying/basics/#expression-language-data-types).
|
||||
func getScalar(arg any, argNum int) ([]float64, error) {
|
||||
ts, ok := arg.([]*timeseries)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf(`unexpected type for arg #%d; got %T; want %T`, argNum+1, arg, ts)
|
||||
return nil, fmt.Errorf(`arg #%d must be a scalar`, argNum+1)
|
||||
}
|
||||
if len(ts) != 1 {
|
||||
return nil, fmt.Errorf(`arg #%d must contain a single timeseries; got %d timeseries`, argNum+1, len(ts))
|
||||
return nil, fmt.Errorf(`arg #%d must be a scalar`, argNum+1)
|
||||
}
|
||||
return ts[0].Values, nil
|
||||
}
|
||||
@@ -2466,14 +2467,15 @@ func getIntNumber(arg any, argNum int) (int, error) {
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// getString expects result from a string expression, which contains a single timeseries with only NaN values.
|
||||
func getString(tss []*timeseries, argNum int) (string, error) {
|
||||
if len(tss) != 1 {
|
||||
return "", fmt.Errorf(`arg #%d must contain a single timeseries; got %d timeseries`, argNum+1, len(tss))
|
||||
return "", fmt.Errorf(`arg #%d must be a string`, argNum+1)
|
||||
}
|
||||
ts := tss[0]
|
||||
for _, v := range ts.Values {
|
||||
if !math.IsNaN(v) {
|
||||
return "", fmt.Errorf(`arg #%d contains non-string timeseries`, argNum+1)
|
||||
return "", fmt.Errorf(`arg #%d must be a string`, argNum+1)
|
||||
}
|
||||
}
|
||||
return string(ts.MetricName.MetricGroup), nil
|
||||
|
||||
1
app/vmselect/vmui/assets/index-B7vIex3g.css
Normal file
1
app/vmselect/vmui/assets/index-B7vIex3g.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
205
app/vmselect/vmui/assets/index-SqjehVXD.js
Normal file
205
app/vmselect/vmui/assets/index-SqjehVXD.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -36,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-BT5pWGkz.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="./assets/vendor-BVRvRxZ2.js">
|
||||
<script type="module" crossorigin src="./assets/index-SqjehVXD.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-BHg4iVVe.css">
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-B7vIex3g.css">
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:20-alpine3.19
|
||||
FROM node:22-alpine3.22
|
||||
|
||||
# Sets a custom location for the npm cache, preventing access errors in system directories
|
||||
ENV NPM_CONFIG_CACHE=/build/.npm
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.24.6 AS build-web-stage
|
||||
FROM golang:1.25.0 AS build-web-stage
|
||||
COPY build /build
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
489
app/vmui/packages/vmui/package-lock.json
generated
489
app/vmui/packages/vmui/package-lock.json
generated
@@ -1177,7 +1177,7 @@
|
||||
"version": "0.3.12",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz",
|
||||
"integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||
@@ -1188,24 +1188,36 @@
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/source-map": {
|
||||
"version": "0.3.10",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.10.tgz",
|
||||
"integrity": "sha512-0pPkgz9dY+bijgistcTTJ5mR+ocqRXLuhXHYdzoMmmoJ2C9S46RCm2GMUbatPEUK9Yjy26IrAy8D/M00lLkv+Q==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
"@jridgewell/trace-mapping": "^0.3.25"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz",
|
||||
"integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.29",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz",
|
||||
"integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.1.0",
|
||||
@@ -1250,6 +1262,316 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz",
|
||||
"integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"detect-libc": "^1.0.3",
|
||||
"is-glob": "^4.0.3",
|
||||
"micromatch": "^4.0.5",
|
||||
"node-addon-api": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@parcel/watcher-android-arm64": "2.5.1",
|
||||
"@parcel/watcher-darwin-arm64": "2.5.1",
|
||||
"@parcel/watcher-darwin-x64": "2.5.1",
|
||||
"@parcel/watcher-freebsd-x64": "2.5.1",
|
||||
"@parcel/watcher-linux-arm-glibc": "2.5.1",
|
||||
"@parcel/watcher-linux-arm-musl": "2.5.1",
|
||||
"@parcel/watcher-linux-arm64-glibc": "2.5.1",
|
||||
"@parcel/watcher-linux-arm64-musl": "2.5.1",
|
||||
"@parcel/watcher-linux-x64-glibc": "2.5.1",
|
||||
"@parcel/watcher-linux-x64-musl": "2.5.1",
|
||||
"@parcel/watcher-win32-arm64": "2.5.1",
|
||||
"@parcel/watcher-win32-ia32": "2.5.1",
|
||||
"@parcel/watcher-win32-x64": "2.5.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-android-arm64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz",
|
||||
"integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-darwin-arm64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz",
|
||||
"integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-darwin-x64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz",
|
||||
"integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-freebsd-x64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz",
|
||||
"integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-arm-glibc": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz",
|
||||
"integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-arm-musl": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz",
|
||||
"integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-arm64-glibc": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz",
|
||||
"integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-arm64-musl": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz",
|
||||
"integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-x64-glibc": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz",
|
||||
"integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-x64-musl": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz",
|
||||
"integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-win32-arm64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz",
|
||||
"integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-win32-ia32": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz",
|
||||
"integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-win32-x64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz",
|
||||
"integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@preact/preset-vite": {
|
||||
"version": "2.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@preact/preset-vite/-/preset-vite-2.10.2.tgz",
|
||||
@@ -1750,9 +2072,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "24.0.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.12.tgz",
|
||||
"integrity": "sha512-LtOrbvDf5ndC9Xi+4QZjVL0woFymF/xSTKZKPgrrl7H7XoeDvnD+E2IclKVDyaK9UM756W/3BXqSU+JEHopA9g==",
|
||||
"version": "24.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz",
|
||||
"integrity": "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2221,7 +2543,7 @@
|
||||
"version": "8.15.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
@@ -2523,7 +2845,7 @@
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fill-range": "^7.1.1"
|
||||
@@ -2572,6 +2894,14 @@
|
||||
"devOptional": true,
|
||||
"license": "MIT/X11"
|
||||
},
|
||||
"node_modules/buffer-from": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/cac": {
|
||||
"version": "6.7.14",
|
||||
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
|
||||
@@ -2702,6 +3032,23 @@
|
||||
"node": ">= 16"
|
||||
}
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
||||
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"readdirp": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.16.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/classnames": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
|
||||
@@ -2750,6 +3097,14 @@
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
@@ -3060,6 +3415,20 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
|
||||
"integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"detect-libc": "bin/detect-libc.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/doctrine": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
|
||||
@@ -3808,7 +4177,7 @@
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
@@ -4522,7 +4891,7 @@
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -4577,7 +4946,7 @@
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-extglob": "^2.1.1"
|
||||
@@ -4616,7 +4985,7 @@
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.12.0"
|
||||
@@ -5119,7 +5488,7 @@
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"braces": "^3.0.3",
|
||||
@@ -5184,6 +5553,14 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-addon-api": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
|
||||
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/node-html-parser": {
|
||||
"version": "6.1.13",
|
||||
"resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-6.1.13.tgz",
|
||||
@@ -5512,7 +5889,7 @@
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
@@ -5754,6 +6131,21 @@
|
||||
"react-dom": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||||
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 14.18.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "individual",
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/redent": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
|
||||
@@ -6054,6 +6446,28 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sass": {
|
||||
"version": "1.89.2",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.89.2.tgz",
|
||||
"integrity": "sha512-xCmtksBKd/jdJ9Bt9p7nPKiuqrlBMBuuGkQlkhZjjQk3Ty48lv93k5Dq6OPkKt4XwxDJ7tvlfrTa1MPA9bf+QA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"chokidar": "^4.0.0",
|
||||
"immutable": "^5.0.2",
|
||||
"source-map-js": ">=0.6.2 <2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"sass": "sass.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@parcel/watcher": "^2.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/sass-embedded": {
|
||||
"version": "1.89.2",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.89.2.tgz",
|
||||
@@ -6583,6 +6997,29 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-support": {
|
||||
"version": "0.5.21",
|
||||
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
|
||||
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"buffer-from": "^1.0.0",
|
||||
"source-map": "^0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-support/node_modules/source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"license": "BSD-3-Clause",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/stack-trace": {
|
||||
"version": "1.0.0-pre2",
|
||||
"resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-1.0.0-pre2.tgz",
|
||||
@@ -6849,6 +7286,26 @@
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/terser": {
|
||||
"version": "5.43.1",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.43.1.tgz",
|
||||
"integrity": "sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==",
|
||||
"license": "BSD-2-Clause",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/source-map": "^0.3.3",
|
||||
"acorn": "^8.14.0",
|
||||
"commander": "^2.20.0",
|
||||
"source-map-support": "~0.5.20"
|
||||
},
|
||||
"bin": {
|
||||
"terser": "bin/terser"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/tinybench": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
||||
@@ -6959,7 +7416,7 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-number": "^7.0.0"
|
||||
|
||||
@@ -18,84 +18,96 @@ import QueryAnalyzer from "./pages/QueryAnalyzer";
|
||||
import DownsamplingFilters from "./pages/DownsamplingFilters";
|
||||
import RetentionFilters from "./pages/RetentionFilters";
|
||||
import RawQueryPage from "./pages/RawQueryPage";
|
||||
import ExploreRules from "./pages/ExploreAlerts/ExploreRules";
|
||||
import ExploreNotifiers from "./pages/ExploreAlerts/ExploreNotifiers";
|
||||
|
||||
const App: FC = () => {
|
||||
const [loadedTheme, setLoadedTheme] = useState(false);
|
||||
|
||||
return <>
|
||||
<HashRouter>
|
||||
<AppContextProvider>
|
||||
<>
|
||||
<ThemeProvider onLoaded={setLoadedTheme}/>
|
||||
{loadedTheme && (
|
||||
<Routes>
|
||||
<Route
|
||||
path={"/"}
|
||||
element={<MainLayout/>}
|
||||
>
|
||||
return (
|
||||
<>
|
||||
<HashRouter>
|
||||
<AppContextProvider>
|
||||
<>
|
||||
<ThemeProvider onLoaded={setLoadedTheme} />
|
||||
{loadedTheme && (
|
||||
<Routes>
|
||||
<Route
|
||||
path={router.home}
|
||||
element={<CustomPanel/>}
|
||||
/>
|
||||
<Route
|
||||
path={router.rawQuery}
|
||||
element={<RawQueryPage/>}
|
||||
/>
|
||||
<Route
|
||||
path={router.metrics}
|
||||
element={<ExploreMetrics/>}
|
||||
/>
|
||||
<Route
|
||||
path={router.cardinality}
|
||||
element={<CardinalityPanel/>}
|
||||
/>
|
||||
<Route
|
||||
path={router.topQueries}
|
||||
element={<TopQueries/>}
|
||||
/>
|
||||
<Route
|
||||
path={router.trace}
|
||||
element={<TracePage/>}
|
||||
/>
|
||||
<Route
|
||||
path={router.queryAnalyzer}
|
||||
element={<QueryAnalyzer/>}
|
||||
/>
|
||||
<Route
|
||||
path={router.dashboards}
|
||||
element={<DashboardsLayout/>}
|
||||
/>
|
||||
<Route
|
||||
path={router.withTemplate}
|
||||
element={<WithTemplate/>}
|
||||
/>
|
||||
<Route
|
||||
path={router.relabel}
|
||||
element={<Relabel/>}
|
||||
/>
|
||||
<Route
|
||||
path={router.activeQueries}
|
||||
element={<ActiveQueries/>}
|
||||
/>
|
||||
<Route
|
||||
path={router.icons}
|
||||
element={<PreviewIcons/>}
|
||||
/>
|
||||
<Route
|
||||
path={router.downsamplingDebug}
|
||||
element={<DownsamplingFilters/>}
|
||||
/>
|
||||
<Route
|
||||
path={router.retentionDebug}
|
||||
element={<RetentionFilters/>}
|
||||
/>
|
||||
</Route>
|
||||
</Routes>
|
||||
)}
|
||||
</>
|
||||
</AppContextProvider>
|
||||
</HashRouter>
|
||||
</>;
|
||||
path={"/"}
|
||||
element={<MainLayout />}
|
||||
>
|
||||
<Route
|
||||
path={router.home}
|
||||
element={<CustomPanel />}
|
||||
/>
|
||||
<Route
|
||||
path={router.rawQuery}
|
||||
element={<RawQueryPage />}
|
||||
/>
|
||||
<Route
|
||||
path={router.metrics}
|
||||
element={<ExploreMetrics />}
|
||||
/>
|
||||
<Route
|
||||
path={router.cardinality}
|
||||
element={<CardinalityPanel />}
|
||||
/>
|
||||
<Route
|
||||
path={router.topQueries}
|
||||
element={<TopQueries />}
|
||||
/>
|
||||
<Route
|
||||
path={router.trace}
|
||||
element={<TracePage />}
|
||||
/>
|
||||
<Route
|
||||
path={router.queryAnalyzer}
|
||||
element={<QueryAnalyzer />}
|
||||
/>
|
||||
<Route
|
||||
path={router.dashboards}
|
||||
element={<DashboardsLayout />}
|
||||
/>
|
||||
<Route
|
||||
path={router.withTemplate}
|
||||
element={<WithTemplate />}
|
||||
/>
|
||||
<Route
|
||||
path={router.relabel}
|
||||
element={<Relabel />}
|
||||
/>
|
||||
<Route
|
||||
path={router.activeQueries}
|
||||
element={<ActiveQueries />}
|
||||
/>
|
||||
<Route
|
||||
path={router.icons}
|
||||
element={<PreviewIcons />}
|
||||
/>
|
||||
<Route
|
||||
path={router.downsamplingDebug}
|
||||
element={<DownsamplingFilters />}
|
||||
/>
|
||||
<Route
|
||||
path={router.retentionDebug}
|
||||
element={<RetentionFilters />}
|
||||
/>
|
||||
<Route
|
||||
path={router.rules}
|
||||
element={<ExploreRules />}
|
||||
/>
|
||||
<Route
|
||||
path={router.notifiers}
|
||||
element={<ExploreNotifiers />}
|
||||
/>
|
||||
</Route>
|
||||
</Routes>
|
||||
)}
|
||||
</>
|
||||
</AppContextProvider>
|
||||
</HashRouter>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export const getAccountIds = (server: string) =>
|
||||
`${server.replace(/^(.+)(\/select.+)/, "$1")}/admin/tenants`;
|
||||
import { getUrlWithoutTenant } from "../utils/tenants";
|
||||
export const getAccountIds = (server: string) => `${getUrlWithoutTenant(server)}/admin/tenants`;
|
||||
|
||||
23
app/vmui/packages/vmui/src/api/explore-alerts.ts
Normal file
23
app/vmui/packages/vmui/src/api/explore-alerts.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
export const getGroupsUrl = (server: string): string => {
|
||||
return `${server}/vmalert/api/v1/rules?datasource_type=prometheus`;
|
||||
};
|
||||
|
||||
export const getItemUrl = (
|
||||
server: string,
|
||||
groupId: string,
|
||||
id: string,
|
||||
mode: string,
|
||||
): string => {
|
||||
return `${server}/vmalert/api/v1/${mode}?group_id=${groupId}&${mode}_id=${id}`;
|
||||
};
|
||||
|
||||
export const getGroupUrl = (
|
||||
server: string,
|
||||
id: string,
|
||||
): string => {
|
||||
return `${server}/vmalert/api/v1/group?group_id=${id}`;
|
||||
};
|
||||
|
||||
export const getNotifiersUrl = (server: string): string => {
|
||||
return `${server}/vmalert/api/v1/notifiers`;
|
||||
};
|
||||
@@ -15,3 +15,24 @@ export const getExportDataUrl = (server: string, query: string, period: TimePara
|
||||
if (reduceMemUsage) params.set("reduce_mem_usage", "1");
|
||||
return `${server}/api/v1/export?${params}`;
|
||||
};
|
||||
|
||||
export const getExportCSVDataUrl = (server: string, query: string[], period: TimeParams, reduceMemUsage: boolean): string => {
|
||||
const params = new URLSearchParams({
|
||||
start: period.start.toString(),
|
||||
end: period.end.toString(),
|
||||
format: "__name__,__value__,__timestamp__:unix_ms",
|
||||
});
|
||||
query.forEach((q => params.append("match[]", q)));
|
||||
if (reduceMemUsage) params.set("reduce_mem_usage", "1");
|
||||
return `${server}/api/v1/export/csv?${params}`;
|
||||
};
|
||||
|
||||
export const getExportJSONDataUrl = (server: string, query: string[], period: TimeParams, reduceMemUsage: boolean): string => {
|
||||
const params = new URLSearchParams({
|
||||
start: period.start.toString(),
|
||||
end: period.end.toString(),
|
||||
});
|
||||
query.forEach((q => params.append("match[]", q)));
|
||||
if (reduceMemUsage) params.set("reduce_mem_usage", "1");
|
||||
return `${server}/api/v1/export?${params}`;
|
||||
};
|
||||
|
||||
@@ -30,7 +30,13 @@ const delayOptions: AutoRefreshOption[] = [
|
||||
{ seconds: 7200, title: "2h" }
|
||||
];
|
||||
|
||||
export const ExecutionControls: FC = () => {
|
||||
interface ExecutionControlsProps {
|
||||
tooltip: string;
|
||||
useAutorefresh?: boolean;
|
||||
closeModal: () => void;
|
||||
}
|
||||
|
||||
export const ExecutionControls: FC<ExecutionControlsProps> = ({ tooltip, useAutorefresh, closeModal }) => {
|
||||
const { isMobile } = useDeviceDetect();
|
||||
|
||||
const dispatch = useTimeDispatch();
|
||||
@@ -56,6 +62,9 @@ export const ExecutionControls: FC = () => {
|
||||
|
||||
const handleUpdate = () => {
|
||||
dispatch({ type: "RUN_QUERY" });
|
||||
if (!useAutorefresh && isMobile) {
|
||||
closeModal();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -77,91 +86,118 @@ export const ExecutionControls: FC = () => {
|
||||
handleChange(d);
|
||||
};
|
||||
|
||||
return <>
|
||||
<div className="vm-execution-controls">
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-execution-controls-buttons": true,
|
||||
"vm-execution-controls-buttons_mobile": isMobile,
|
||||
"vm-header-button": !appModeEnable,
|
||||
})}
|
||||
>
|
||||
{!isMobile && (
|
||||
<Tooltip title="Refresh dashboard">
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleUpdate}
|
||||
startIcon={<RefreshIcon/>}
|
||||
ariaLabel="refresh dashboard"
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
{isMobile ? (
|
||||
<div
|
||||
className="vm-mobile-option"
|
||||
onClick={toggleOpenOptions}
|
||||
>
|
||||
<span className="vm-mobile-option__icon"><RestartIcon/></span>
|
||||
<div className="vm-mobile-option-text">
|
||||
<span className="vm-mobile-option-text__label">Auto-refresh</span>
|
||||
<span className="vm-mobile-option-text__value">{selectedDelay.title}</span>
|
||||
</div>
|
||||
<span className="vm-mobile-option__arrow"><ArrowDownIcon/></span>
|
||||
</div>
|
||||
) : (
|
||||
<Tooltip title="Auto-refresh control">
|
||||
<div ref={optionsButtonRef}>
|
||||
return (
|
||||
<>
|
||||
<div className="vm-execution-controls">
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-execution-controls-buttons": true,
|
||||
"vm-execution-controls-buttons_mobile": isMobile,
|
||||
"vm-header-button": !appModeEnable,
|
||||
"vm-autorefresh": useAutorefresh,
|
||||
})}
|
||||
>
|
||||
{useAutorefresh ? (
|
||||
isMobile ? (
|
||||
<div
|
||||
className="vm-mobile-option"
|
||||
onClick={toggleOpenOptions}
|
||||
>
|
||||
<span className="vm-mobile-option__icon"><RestartIcon/></span>
|
||||
<div className="vm-mobile-option-text">
|
||||
<span className="vm-mobile-option-text__label">Auto-refresh</span>
|
||||
<span className="vm-mobile-option-text__value">{selectedDelay.title}</span>
|
||||
</div>
|
||||
<span className="vm-mobile-option__arrow"><ArrowDownIcon/></span>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<Tooltip title={tooltip}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleUpdate}
|
||||
startIcon={<RefreshIcon/>}
|
||||
ariaLabel={tooltip}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="Auto-refresh control">
|
||||
<div ref={optionsButtonRef}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
fullWidth
|
||||
endIcon={(
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-execution-controls-buttons__arrow": true,
|
||||
"vm-execution-controls-buttons__arrow_open": openOptions,
|
||||
})}
|
||||
>
|
||||
<ArrowDownIcon/>
|
||||
</div>
|
||||
)}
|
||||
onClick={toggleOpenOptions}
|
||||
>
|
||||
{selectedDelay.title}
|
||||
</Button>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</>
|
||||
)
|
||||
) : (
|
||||
isMobile ? (
|
||||
<div
|
||||
className="vm-mobile-option"
|
||||
onClick={handleUpdate}
|
||||
>
|
||||
<span className="vm-mobile-option__icon"><RestartIcon/></span>
|
||||
<div className="vm-mobile-option-text">
|
||||
<span className="vm-mobile-option-text__label">Refresh</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
fullWidth
|
||||
endIcon={(
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-execution-controls-buttons__arrow": true,
|
||||
"vm-execution-controls-buttons__arrow_open": openOptions,
|
||||
})}
|
||||
>
|
||||
<ArrowDownIcon/>
|
||||
</div>
|
||||
)}
|
||||
onClick={toggleOpenOptions}
|
||||
>
|
||||
{selectedDelay.title}
|
||||
</Button>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
onClick={handleUpdate}
|
||||
startIcon={<RefreshIcon/>}
|
||||
ariaLabel={tooltip}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Popper
|
||||
open={openOptions}
|
||||
placement="bottom-right"
|
||||
onClose={handleCloseOptions}
|
||||
buttonRef={optionsButtonRef}
|
||||
title={isMobile ? "Auto-refresh duration" : undefined}
|
||||
>
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-execution-controls-list": true,
|
||||
"vm-execution-controls-list_mobile": isMobile,
|
||||
})}
|
||||
>
|
||||
{delayOptions.map(d => (
|
||||
{useAutorefresh && (
|
||||
<Popper
|
||||
open={openOptions}
|
||||
placement="bottom-right"
|
||||
onClose={handleCloseOptions}
|
||||
buttonRef={optionsButtonRef}
|
||||
title={isMobile ? "Auto-refresh duration" : undefined}
|
||||
>
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-list-item": true,
|
||||
"vm-list-item_mobile": isMobile,
|
||||
"vm-list-item_active": d.seconds === selectedDelay.seconds
|
||||
"vm-execution-controls-list": true,
|
||||
"vm-execution-controls-list_mobile": isMobile,
|
||||
})}
|
||||
key={d.seconds}
|
||||
onClick={createHandlerChange(d)}
|
||||
>
|
||||
{d.title}
|
||||
{delayOptions.map(d => (
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-list-item": true,
|
||||
"vm-list-item_mobile": isMobile,
|
||||
"vm-list-item_active": d.seconds === selectedDelay.seconds
|
||||
})}
|
||||
key={d.seconds}
|
||||
onClick={createHandlerChange(d)}
|
||||
>
|
||||
{d.title}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Popper>
|
||||
</>;
|
||||
</Popper>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7,7 +7,10 @@
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
border-radius: calc($button-radius + 1px);
|
||||
min-width: 107px;
|
||||
|
||||
:is(.vm-autorefresh) {
|
||||
min-width: 107px;
|
||||
}
|
||||
|
||||
&_mobile {
|
||||
flex-direction: column;
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
import { FC, useCallback } from "preact/compat";
|
||||
import { useCallback, useRef } from "preact/compat";
|
||||
import Tooltip from "../Main/Tooltip/Tooltip";
|
||||
import Button from "../Main/Button/Button";
|
||||
import { DownloadIcon } from "../Main/Icons";
|
||||
import Popper from "../Main/Popper/Popper";
|
||||
import { useRef } from "react";
|
||||
import "./style.scss";
|
||||
import useBoolean from "../../hooks/useBoolean";
|
||||
|
||||
interface DownloadButtonProps {
|
||||
interface DownloadButtonProps<T extends string> {
|
||||
title: string;
|
||||
downloadFormatOptions?: string[];
|
||||
onDownload: (format?: string) => void;
|
||||
downloadFormatOptions?: T[];
|
||||
onDownload: (format?: T) => void;
|
||||
}
|
||||
|
||||
/** TODO: Currently unused, later will be added for the exporting metrics */
|
||||
const DownloadButton: FC<DownloadButtonProps> = ({ title, downloadFormatOptions, onDownload }) => {
|
||||
const DownloadButton = <T extends string>({ title, downloadFormatOptions, onDownload }: DownloadButtonProps<T>) => {
|
||||
const {
|
||||
value: isPopupOpen,
|
||||
setTrue: onOpenPopup,
|
||||
@@ -35,9 +33,19 @@ const DownloadButton: FC<DownloadButtonProps> = ({ title, downloadFormatOptions,
|
||||
}
|
||||
}, [onDownload, onClosePopup, isPopupOpen, onOpenPopup]);
|
||||
|
||||
const isDownloadFormat = useCallback((format: string): format is T => {
|
||||
return (downloadFormatOptions as string[])?.includes(format);
|
||||
}, [downloadFormatOptions]);
|
||||
|
||||
const onDownloadFormatClick = useCallback((event: Event) => {
|
||||
const button = event.currentTarget as HTMLButtonElement;
|
||||
onDownload(button.textContent ?? undefined);
|
||||
const format = button.textContent;
|
||||
if (format && isDownloadFormat(format)) {
|
||||
onDownload(format);
|
||||
} else {
|
||||
onDownload();
|
||||
}
|
||||
onClosePopup();
|
||||
}, [onDownload]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import "./style.scss";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
export type BadgeColor = "firing" | "inactive" | "pending" | "no-match" | "unhealthy" | "ok" | "passive";
|
||||
|
||||
interface BadgeItem {
|
||||
value?: number | string;
|
||||
color: BadgeColor;
|
||||
}
|
||||
|
||||
interface BadgesProps {
|
||||
items: Record<string, BadgeItem>;
|
||||
align?: "center" | "start" | "end";
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
const Badges = ({ items, children, align = "start" }: BadgesProps) => {
|
||||
return (
|
||||
<div
|
||||
className="vm-badges"
|
||||
style={{ "justify-content": align }}
|
||||
>
|
||||
{Object.entries(items).map(([name, props]) => (
|
||||
<span
|
||||
key={name}
|
||||
className={`vm-badge ${props.color}`}
|
||||
>{props.value ? `${name}: ${props.value}` : name}</span>
|
||||
))}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Badges;
|
||||
@@ -0,0 +1,69 @@
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
$badge-colors: (
|
||||
"firing": $color-error,
|
||||
"inactive": $color-success,
|
||||
"pending": $color-warning,
|
||||
"no-match": $color-notice,
|
||||
"unhealthy": $color-broken,
|
||||
"ok": $color-info,
|
||||
"passive": $color-passive,
|
||||
"all": $color-passive,
|
||||
);
|
||||
|
||||
.vm-badges {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: $padding-small;
|
||||
&.align-center {
|
||||
justify-content: center;
|
||||
}
|
||||
.vm-badge {
|
||||
padding: 0 $padding-tiny;
|
||||
width: fit-content;
|
||||
@each $class, $color in $badge-colors {
|
||||
&.#{$class} {
|
||||
border: 1px solid $color;
|
||||
color: $color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.vm-badge-base {
|
||||
font-weight: 400;
|
||||
border-radius: $border-radius-small;
|
||||
}
|
||||
|
||||
.vm-badge-menu-item {
|
||||
@extend .vm-badge-base;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
line-height: 22px;
|
||||
@each $class, $color in $badge-colors {
|
||||
&.#{$class} {
|
||||
border-right: $border-radius-small solid $color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.vm-badge-item {
|
||||
@extend .vm-badge-base;
|
||||
@each $class, $color in $badge-colors {
|
||||
&.#{$class} {
|
||||
border-left: $border-radius-small solid $color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.vm-badge {
|
||||
@extend .vm-badge-base;
|
||||
background-color: transparent;
|
||||
padding: 0 $padding-tiny;
|
||||
line-height: 22px;
|
||||
max-width: 300px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import "./style.scss";
|
||||
import { Alert as APIAlert } from "../../../types";
|
||||
import { createSearchParams } from "react-router-dom";
|
||||
import Button from "../../Main/Button/Button";
|
||||
import Badges from "../Badges";
|
||||
import {
|
||||
SearchIcon,
|
||||
} from "../../Main/Icons";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
interface BaseAlertProps {
|
||||
item: APIAlert;
|
||||
}
|
||||
|
||||
const BaseAlert = ({ item }: BaseAlertProps) => {
|
||||
const query = item?.expression;
|
||||
|
||||
const openQueryLink = () => {
|
||||
const params = {
|
||||
"g0.expr": query,
|
||||
"g0.end_time": ""
|
||||
};
|
||||
window.open(`#/?${createSearchParams(params).toString()}`, "_blank", "noopener noreferrer");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="vm-explore-alerts-alert-item">
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
style={{ "text-align": "end" }}
|
||||
colSpan={2}
|
||||
>
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
color="gray"
|
||||
startIcon={<SearchIcon />}
|
||||
onClick={openQueryLink}
|
||||
>
|
||||
<span className="vm-button-text">Run query</span>
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="vm-col-md">Query</td>
|
||||
<td>
|
||||
<pre>
|
||||
<code className="language-promql">{query}</code>
|
||||
</pre>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="vm-col-md">Active at</td>
|
||||
<td>{dayjs(item.activeAt).format("DD MMM YYYY HH:mm:ss")}</td>
|
||||
</tr>
|
||||
{!!Object.keys(item?.labels || {}).length && (
|
||||
<tr>
|
||||
<td className="vm-col-md">Labels</td>
|
||||
<td>
|
||||
<Badges
|
||||
items={Object.fromEntries(Object.entries(item.labels).map(([name, value]) => [name, {
|
||||
color: "passive",
|
||||
value: value,
|
||||
}]))}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
{!!Object.keys(item.annotations || {}).length && (
|
||||
<>
|
||||
<span className="title">Annotations</span>
|
||||
<table>
|
||||
<tbody>
|
||||
{Object.entries(item.annotations || {}).map(([name, value]) => (
|
||||
<tr key={name}>
|
||||
<td className="vm-col-md">{name}</td>
|
||||
<td>{value}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BaseAlert;
|
||||
@@ -0,0 +1,74 @@
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-modal {
|
||||
.vm-explore-alerts-alert-item {
|
||||
table {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.vm-explore-alerts-alert-item {
|
||||
row-gap: $padding-global;
|
||||
margin-right: $padding-global;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.title {
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
a:hover > pre {
|
||||
background-color: $color-background-badge;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
background-color: $color-background-hover;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
pre {
|
||||
background-color: $color-background-badge;
|
||||
padding: 0 $padding-global;
|
||||
border-radius: $border-radius-small;
|
||||
word-break: break-word;
|
||||
white-space: pre-wrap;
|
||||
.keyword,
|
||||
.function,
|
||||
.attr-name,
|
||||
.range-duration {
|
||||
color: $color-keyword;
|
||||
}
|
||||
}
|
||||
|
||||
.vm-col-sm {
|
||||
width: 10%;
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.vm-col-md {
|
||||
width: 15%;
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
td, th {
|
||||
line-height: 30px;
|
||||
padding: 4px $padding-small;
|
||||
}
|
||||
th {
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
padding: 0 $padding-small;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import "./style.scss";
|
||||
import { Group as APIGroup } from "../../../types";
|
||||
import dayjs from "dayjs";
|
||||
import { formatDuration } from "../helpers";
|
||||
import Badges from "../Badges";
|
||||
|
||||
interface BaseGroupProps {
|
||||
group: APIGroup;
|
||||
}
|
||||
|
||||
const BaseGroup = ({ group }: BaseGroupProps) => {
|
||||
return (
|
||||
<div className="vm-explore-alerts-group">
|
||||
<div></div>
|
||||
<table>
|
||||
<tbody>
|
||||
{!!group.interval && (
|
||||
<tr>
|
||||
<td className="vm-col-md">Interval</td>
|
||||
<td>{formatDuration(group.interval)}</td>
|
||||
</tr>
|
||||
)}
|
||||
{!!group.lastEvaluation && (
|
||||
<tr>
|
||||
<td className="vm-col-md">Last evaluation</td>
|
||||
<td>{dayjs(group.lastEvaluation).format("DD MMM YYYY HH:mm:ss")}</td>
|
||||
</tr>
|
||||
)}
|
||||
{!!group.eval_offset && (
|
||||
<tr>
|
||||
<td className="vm-col-md">Eval offset</td>
|
||||
<td>{formatDuration(group.eval_offset)}</td>
|
||||
</tr>
|
||||
)}
|
||||
{!!group.eval_delay && (
|
||||
<tr>
|
||||
<td className="vm-col-md">Eval delay</td>
|
||||
<td>{formatDuration(group.eval_delay)}</td>
|
||||
</tr>
|
||||
)}
|
||||
{!!group.file && (
|
||||
<tr>
|
||||
<td className="vm-col-md">File</td>
|
||||
<td>{group.file}</td>
|
||||
</tr>
|
||||
)}
|
||||
{!!group.concurrency && (
|
||||
<tr>
|
||||
<td className="vm-col-md">Concurrency</td>
|
||||
<td>{group.concurrency}</td>
|
||||
</tr>
|
||||
)}
|
||||
{!!group?.labels?.length && (
|
||||
<tr>
|
||||
<td className="vm-col-md">Labels</td>
|
||||
<td>
|
||||
<Badges
|
||||
items={Object.fromEntries(Object.entries(group.labels).map(([name, value]) => [name, {
|
||||
color: "passive",
|
||||
value: value,
|
||||
}]))}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{!!group?.params?.length && (
|
||||
<tr>
|
||||
<td className="vm-col-md">Params</td>
|
||||
<td>
|
||||
<Badges
|
||||
items={Object.fromEntries(group.params.map(value => [value, {
|
||||
color: "passive",
|
||||
}]))}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{!!group?.headers?.length && (
|
||||
<tr>
|
||||
<td className="vm-col-md">Headers</td>
|
||||
<td>
|
||||
<Badges
|
||||
items={Object.fromEntries(group.headers.map(value => [value, {
|
||||
color: "passive",
|
||||
}]))}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{!!group?.notifier_headers?.length && (
|
||||
<tr>
|
||||
<td className="vm-col-md">Notifier headers</td>
|
||||
<td>
|
||||
<Badges
|
||||
items={Object.fromEntries(group.notifier_headers.map(value => [value, {
|
||||
color: "passive",
|
||||
}]))}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BaseGroup;
|
||||
@@ -0,0 +1,78 @@
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-modal {
|
||||
.vm-explore-alerts-group {
|
||||
table {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.vm-explore-alerts-group {
|
||||
row-gap: $padding-global;
|
||||
margin-right: $padding-global;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.title {
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
pre {
|
||||
position: relative;
|
||||
background-color: $color-background-badge;
|
||||
padding: 0 $padding-global;
|
||||
border-radius: $border-radius-small;
|
||||
word-break: break-word;
|
||||
white-space: pre-wrap;
|
||||
.keyword,
|
||||
.function,
|
||||
.attr-name,
|
||||
.range-duration {
|
||||
color: $color-keyword;
|
||||
}
|
||||
div {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
column-gap: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.vm-col-sm {
|
||||
width: 10%;
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.vm-col-md {
|
||||
width: 15%;
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
tr.hoverable {
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background-color: $color-background-hover;
|
||||
}
|
||||
}
|
||||
td, th {
|
||||
line-height: 30px;
|
||||
padding: 4px $padding-small;
|
||||
}
|
||||
th {
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
padding: 0 $padding-small;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
import "./style.scss";
|
||||
import { Rule as APIRule } from "../../../types";
|
||||
import { useNavigate, createSearchParams } from "react-router-dom";
|
||||
import { SearchIcon, DetailsIcon } from "../../Main/Icons";
|
||||
import Button from "../../Main/Button/Button";
|
||||
import Alert from "../../Main/Alert/Alert";
|
||||
import Badges, { BadgeColor } from "../Badges";
|
||||
import dayjs from "dayjs";
|
||||
import { formatDuration } from "../helpers";
|
||||
|
||||
interface BaseRuleProps {
|
||||
item: APIRule;
|
||||
}
|
||||
|
||||
const BaseRule = ({ item }: BaseRuleProps) => {
|
||||
const query = item?.query;
|
||||
const navigate = useNavigate();
|
||||
const openAlertLink = (id: string) => {
|
||||
return () => {
|
||||
navigate({
|
||||
pathname: "/rules",
|
||||
search: `group_id=${item.group_id}&alert_id=${id}`,
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
const openQueryLink = () => {
|
||||
const params = {
|
||||
"g0.expr": query,
|
||||
"g0.end_time": ""
|
||||
};
|
||||
window.open(`#/?${createSearchParams(params).toString()}`, "_blank", "noopener noreferrer");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="vm-explore-alerts-rule-item">
|
||||
<div></div>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
style={{ "text-align": "end" }}
|
||||
colSpan={2}
|
||||
>
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
color="gray"
|
||||
startIcon={<SearchIcon />}
|
||||
onClick={openQueryLink}
|
||||
>
|
||||
<span className="vm-button-text">Run query</span>
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="vm-col-md">Query</td>
|
||||
<td>
|
||||
<pre>
|
||||
<code className="language-promql">{query}</code>
|
||||
</pre>
|
||||
</td>
|
||||
</tr>
|
||||
{!!item.duration && (
|
||||
<tr>
|
||||
<td className="vm-col-md">For</td>
|
||||
<td>{formatDuration(item.duration)}</td>
|
||||
</tr>
|
||||
)}
|
||||
{!!item.lastEvaluation && (
|
||||
<tr>
|
||||
<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 className="vm-col-md">Last error</td>
|
||||
<td>
|
||||
<Alert variant="error">{item.lastError}</Alert>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{!!Object.keys(item?.labels || {}).length && (
|
||||
<tr>
|
||||
<td className="vm-col-md">Labels</td>
|
||||
<td>
|
||||
<Badges
|
||||
items={Object.fromEntries(Object.entries(item.labels).map(([name, value]) => [name, {
|
||||
color: "passive",
|
||||
value: value,
|
||||
}]))}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
{!!Object.keys(item?.annotations || {}).length && (
|
||||
<>
|
||||
<span className="title">Annotations</span>
|
||||
<table className="fixed">
|
||||
<tbody>
|
||||
{Object.entries(item.annotations || {}).map(([name, value]) => (
|
||||
<tr key={name}>
|
||||
<td className="vm-col-md">{name}</td>
|
||||
<td>{value}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
)}
|
||||
{!!item?.updates?.length && (
|
||||
<>
|
||||
<span className="title">{`Last updates ${item.updates.length}/${item.max_updates_entries}`}</span>
|
||||
<table className="fixed">
|
||||
<thead>
|
||||
<tr>
|
||||
<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>
|
||||
{item.updates.map((update) => (
|
||||
<tr
|
||||
key={update.at}
|
||||
>
|
||||
<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>
|
||||
</table>
|
||||
</>
|
||||
)}
|
||||
{!!item?.alerts?.length && (
|
||||
<>
|
||||
<span className="title">Alerts</span>
|
||||
<table className="fixed">
|
||||
<thead>
|
||||
<tr>
|
||||
<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>
|
||||
{item.alerts.map((alert) => (
|
||||
<tr
|
||||
id={`alert-${alert.id}`}
|
||||
key={alert.id}
|
||||
>
|
||||
<td className="vm-col-sm">
|
||||
{dayjs(alert.activeAt).format("DD MMM YYYY HH:mm:ss")}
|
||||
</td>
|
||||
<td className="vm-col-sm">
|
||||
<Badges
|
||||
items={{ [alert.state]: { color: alert.state as BadgeColor } }}
|
||||
/>
|
||||
</td>
|
||||
<td className="vm-col-sm">
|
||||
<Badges
|
||||
items={{ [alert.value]: { color: "passive" } }}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<Badges
|
||||
align="center"
|
||||
items={Object.fromEntries(Object.entries(alert.labels || {}).map(([name, value]) => [name, {
|
||||
color: "passive",
|
||||
value: value,
|
||||
}]))}
|
||||
/>
|
||||
</td>
|
||||
<td className="vm-col-hidden">
|
||||
<Button
|
||||
className="vm-button-borderless"
|
||||
size="small"
|
||||
variant="outlined"
|
||||
color="gray"
|
||||
startIcon={<DetailsIcon />}
|
||||
onClick={openAlertLink(alert.id)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BaseRule;
|
||||
@@ -0,0 +1,88 @@
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-modal {
|
||||
.vm-explore-alerts-rule-item {
|
||||
table {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.vm-explore-alerts-rule-item {
|
||||
row-gap: $padding-global;
|
||||
margin-right: $padding-global;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.title {
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
pre {
|
||||
position: relative;
|
||||
background-color: $color-background-badge;
|
||||
padding: 0 $padding-global;
|
||||
border-radius: $border-radius-small;
|
||||
word-break: break-word;
|
||||
white-space: pre-wrap;
|
||||
.keyword,
|
||||
.function,
|
||||
.attr-name,
|
||||
.range-duration {
|
||||
color: $color-keyword;
|
||||
}
|
||||
div {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
column-gap: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.vm-col-hidden {
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
.vm-button {
|
||||
color: $color-passive;
|
||||
border: 1px solid var(--color-passive);
|
||||
}
|
||||
|
||||
.vm-col-sm {
|
||||
width: 10%;
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.vm-col-md {
|
||||
width: 15%;
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
table {
|
||||
&.fixed {
|
||||
table-layout: fixed;
|
||||
}
|
||||
width: 100%;
|
||||
td, th {
|
||||
line-height: 30px;
|
||||
padding: 4px $padding-small;
|
||||
vertical-align: middle;
|
||||
}
|
||||
td.align-center {
|
||||
text-align: center
|
||||
}
|
||||
th {
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
padding: 0 $padding-small;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { FC } from "preact/compat";
|
||||
import "./style.scss";
|
||||
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Group as APIGroup } from "../../../types";
|
||||
import { DetailsIcon } from "../../Main/Icons";
|
||||
import Button from "../../Main/Button/Button";
|
||||
import Badges, { BadgeColor } from "../Badges";
|
||||
import classNames from "classnames";
|
||||
interface GroupHeaderControlsProps {
|
||||
group: APIGroup;
|
||||
}
|
||||
|
||||
const GroupHeaderHeader: FC<GroupHeaderControlsProps> = ({ group }) => {
|
||||
const { isMobile } = useDeviceDetect();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const openGroupModal = async () => {
|
||||
navigate({
|
||||
pathname: "/rules",
|
||||
search: `group_id=${group.id}`,
|
||||
});
|
||||
};
|
||||
|
||||
const headerClasses = classNames({
|
||||
"vm-explore-alerts-group-header": true,
|
||||
"vm-explore-alerts-group-header_mobile": isMobile,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={headerClasses}>
|
||||
<div className="vm-explore-alerts-group-header__desc">
|
||||
<div className="vm-explore-alerts-group-header__name">{group.name}</div>
|
||||
{!isMobile && (
|
||||
<div className="vm-explore-alerts-group-header__file">{group.file}</div>
|
||||
)}
|
||||
</div>
|
||||
<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"
|
||||
color="gray"
|
||||
variant="outlined"
|
||||
startIcon={<DetailsIcon />}
|
||||
onClick={openGroupModal}
|
||||
/>
|
||||
</Badges>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GroupHeaderHeader;
|
||||
@@ -0,0 +1,60 @@
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-explore-alerts-group-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: $padding-tiny 0 $padding-tiny $padding-global;
|
||||
justify-content: space-between;
|
||||
|
||||
.vm-button_small {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.vm-button-borderless {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
@media(max-width: 768px) {
|
||||
.vm-button-text {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&_mobile {
|
||||
.vm-button-text {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__desc {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $padding-tiny;
|
||||
}
|
||||
|
||||
&__index {
|
||||
color: $color-text-secondary;
|
||||
font-size: $font-size-small;
|
||||
}
|
||||
|
||||
&__name {
|
||||
flex-grow: 1;
|
||||
font-weight: bold;
|
||||
max-width: 100%;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
line-height: 130%;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
&__file {
|
||||
color: $color-text-disabled;
|
||||
}
|
||||
|
||||
code {
|
||||
padding: 0.2em 0.4em;
|
||||
font-size: 85%;
|
||||
background-color: $color-hover-black;
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import { FC } from "preact/compat";
|
||||
import "./style.scss";
|
||||
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||
import useCopyToClipboard from "../../../hooks/useCopyToClipboard";
|
||||
import { useAppState } from "../../../state/common/StateContext";
|
||||
import Tooltip from "../../Main/Tooltip/Tooltip";
|
||||
import classNames from "classnames";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import Badges, { BadgeColor } from "../Badges";
|
||||
import {
|
||||
LinkIcon,
|
||||
GroupIcon,
|
||||
AlertIcon,
|
||||
AlertingRuleIcon,
|
||||
RecordingRuleIcon,
|
||||
DetailsIcon,
|
||||
} from "../../Main/Icons";
|
||||
import Button from "../../Main/Button/Button";
|
||||
|
||||
interface ItemHeaderControlsProps {
|
||||
entity: string;
|
||||
type?: string;
|
||||
groupId: string;
|
||||
states?: Record<string, number>;
|
||||
id?: string;
|
||||
name: string;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
const ItemHeader: FC<ItemHeaderControlsProps> = ({ name, id, groupId, entity, type, states, onClose }) => {
|
||||
const { isMobile } = useDeviceDetect();
|
||||
const { serverUrl } = useAppState();
|
||||
const navigate = useNavigate();
|
||||
const copyToClipboard = useCopyToClipboard();
|
||||
|
||||
const openItemLink = () => {
|
||||
navigate({
|
||||
pathname: "/rules",
|
||||
search: `group_id=${groupId}&${entity}_id=${id}`,
|
||||
});
|
||||
};
|
||||
|
||||
const copyLink = async () => {
|
||||
let link = `${serverUrl}/vmui/#/rules?group_id=${groupId}`;
|
||||
if (type) link = `${link}&${entity}_id=${id}`;
|
||||
await copyToClipboard(link, `Link to ${entity} has been copied`);
|
||||
};
|
||||
|
||||
const headerClasses = classNames({
|
||||
"vm-explore-alerts-item-header": true,
|
||||
"vm-explore-alerts-item-header_mobile": isMobile,
|
||||
});
|
||||
|
||||
const renderIcon = () => {
|
||||
switch(entity) {
|
||||
case "alert":
|
||||
return (
|
||||
<Tooltip title="Alert">
|
||||
<AlertIcon />
|
||||
</Tooltip>
|
||||
);
|
||||
case "group":
|
||||
return (
|
||||
<Tooltip title="Group">
|
||||
<GroupIcon />
|
||||
</Tooltip>
|
||||
);
|
||||
default:
|
||||
switch(type) {
|
||||
case "alerting":
|
||||
return (
|
||||
<Tooltip title="Alerting rule">
|
||||
<AlertingRuleIcon />
|
||||
</Tooltip>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Tooltip title="Recording rule">
|
||||
<RecordingRuleIcon />
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={headerClasses}
|
||||
id={`rule-${id}`}
|
||||
>
|
||||
<div className="vm-explore-alerts-item-header__title">
|
||||
{renderIcon()}
|
||||
<div className="vm-explore-alerts-item-header__name">{name}</div>
|
||||
</div>
|
||||
<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"
|
||||
size="small"
|
||||
variant="outlined"
|
||||
color="gray"
|
||||
startIcon={<LinkIcon />}
|
||||
onClick={copyLink}
|
||||
>
|
||||
<span className="vm-button-text">Copy Link</span>
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
className="vm-button-borderless"
|
||||
size="small"
|
||||
variant="outlined"
|
||||
color="gray"
|
||||
startIcon={<DetailsIcon />}
|
||||
onClick={openItemLink}
|
||||
/>
|
||||
)}
|
||||
</Badges>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ItemHeader;
|
||||
@@ -0,0 +1,70 @@
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-explore-alerts-item-header {
|
||||
display: flex;
|
||||
grid-template-columns: auto 1fr auto auto;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: $padding-global;
|
||||
|
||||
.vm-button_small {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
@media(max-width: 768px) {
|
||||
.vm-button-text {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.vm-button-borderless {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.vm-back-button {
|
||||
svg {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
|
||||
&_mobile {
|
||||
grid-template-columns: 1fr auto;
|
||||
.vm-button-text {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__index {
|
||||
color: $color-text-secondary;
|
||||
font-size: $font-size-small;
|
||||
}
|
||||
|
||||
&__name {
|
||||
font-weight: bold;
|
||||
max-width: 100%;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
line-height: 130%;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
&__title {
|
||||
display: flex;
|
||||
column-gap: $padding-global;
|
||||
svg {
|
||||
fill: $color-text-disabled;
|
||||
width: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
&__file {
|
||||
color: $color-text-disabled;
|
||||
}
|
||||
|
||||
code {
|
||||
padding: 0.2em 0.4em;
|
||||
font-size: 85%;
|
||||
background-color: $color-hover-black;
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { FC } from "preact/compat";
|
||||
import "./style.scss";
|
||||
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||
import { Notifier } from "../../../types";
|
||||
import classNames from "classnames";
|
||||
|
||||
interface NotifierHeaderControlsProps {
|
||||
notifier: Notifier;
|
||||
}
|
||||
|
||||
const NotifierHeaderHeader: FC<NotifierHeaderControlsProps> = ({
|
||||
notifier,
|
||||
}) => {
|
||||
const { isMobile } = useDeviceDetect();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-explore-alerts-notifier-header": true,
|
||||
"vm-explore-alerts-notifier-header_mobile": isMobile,
|
||||
})}
|
||||
>
|
||||
<div className="vm-explore-alerts-notifier-header__name">
|
||||
{notifier.kind}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotifierHeaderHeader;
|
||||
@@ -0,0 +1,40 @@
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
&__index {
|
||||
color: $color-text-secondary;
|
||||
font-size: $font-size-small;
|
||||
}
|
||||
|
||||
&__name {
|
||||
flex-grow: 1;
|
||||
font-weight: bold;
|
||||
max-width: 100%;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
line-height: 130%;
|
||||
}
|
||||
|
||||
&__file {
|
||||
color: $color-text-disabled;
|
||||
}
|
||||
|
||||
code {
|
||||
padding: 0.2em 0.4em;
|
||||
font-size: 85%;
|
||||
background-color: $color-hover-black;
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { FC } from "preact/compat";
|
||||
import Select from "../../Main/Select/Select";
|
||||
import { SearchIcon } from "../../Main/Icons";
|
||||
import TextField from "../../Main/TextField/TextField";
|
||||
import "./style.scss";
|
||||
import classNames from "classnames";
|
||||
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||
|
||||
interface NotifiersHeaderProps {
|
||||
kinds: string[];
|
||||
allKinds: string[];
|
||||
onChangeKinds: (input: string) => void;
|
||||
onChangeSearch: (input: string) => void;
|
||||
}
|
||||
|
||||
const NotifiersHeader: FC<NotifiersHeaderProps> = ({
|
||||
kinds,
|
||||
allKinds,
|
||||
onChangeKinds,
|
||||
onChangeSearch,
|
||||
}) => {
|
||||
const { isMobile } = useDeviceDetect();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-explore-alerts-header": true,
|
||||
"vm-explore-alerts-header_mobile": isMobile,
|
||||
"vm-block": true,
|
||||
"vm-block_mobile": isMobile,
|
||||
})}
|
||||
>
|
||||
<div className="vm-explore-alerts-header__rule_type">
|
||||
<Select
|
||||
value={kinds}
|
||||
list={allKinds}
|
||||
label="Notifier type"
|
||||
placeholder="Please select notifier type"
|
||||
onChange={onChangeKinds}
|
||||
autofocus={!!kinds.length && !isMobile}
|
||||
includeAll
|
||||
searchable
|
||||
/>
|
||||
</div>
|
||||
<div className="vm-explore-alerts-header-search">
|
||||
<TextField
|
||||
label="Search"
|
||||
placeholder="Filter by kind, address or labels"
|
||||
startIcon={<SearchIcon />}
|
||||
onChange={onChangeSearch}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotifiersHeader;
|
||||
@@ -0,0 +1,65 @@
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-explore-alerts-header {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: $padding-global calc($padding-small + 10px);
|
||||
width: 100%;
|
||||
|
||||
&_mobile {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
&__rule_type {
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
&__state {
|
||||
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 {
|
||||
padding: 11px 28px;
|
||||
}
|
||||
.vm-text-field__icon-start {
|
||||
height: 42px;
|
||||
}
|
||||
}
|
||||
|
||||
&__clear-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { FC } from "preact/compat";
|
||||
import ItemHeader from "../ItemHeader";
|
||||
import Accordion from "../../Main/Accordion/Accordion";
|
||||
import "./style.scss";
|
||||
import { Rule as APIRule } from "../../../types";
|
||||
import BaseRule from "../BaseRule";
|
||||
|
||||
interface RuleProps {
|
||||
states: Record<string, number>;
|
||||
rule: APIRule;
|
||||
}
|
||||
|
||||
const Rule: FC<RuleProps> = ({ states, rule }) => {
|
||||
const state = Object.keys(states).length > 0 ? Object.keys(states)[0] : "ok";
|
||||
return (
|
||||
<div className={`vm-explore-alerts-rule vm-badge-item ${state.replace(" ", "-")}`}>
|
||||
<Accordion
|
||||
key={`rule-${rule.id}`}
|
||||
title={<ItemHeader
|
||||
entity="rule"
|
||||
type={rule.type}
|
||||
groupId={rule.group_id}
|
||||
states={states}
|
||||
id={rule.id}
|
||||
name={rule.name}
|
||||
/>}
|
||||
>
|
||||
<BaseRule item={rule} />
|
||||
</Accordion>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Rule;
|
||||
@@ -0,0 +1,18 @@
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-explore-alerts-rule {
|
||||
padding: $padding-tiny;
|
||||
padding-right: 0;
|
||||
display: flex;
|
||||
row-gap: $padding-tiny;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
|
||||
&:has(>details[open]) {
|
||||
background-color: $color-background-item;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $color-background-item;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { FC, useMemo } from "preact/compat";
|
||||
import Select from "../../Main/Select/Select";
|
||||
import { SearchIcon } from "../../Main/Icons";
|
||||
import TextField from "../../Main/TextField/TextField";
|
||||
import "./style.scss";
|
||||
import classNames from "classnames";
|
||||
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||
|
||||
interface RulesHeaderProps {
|
||||
types: string[];
|
||||
allTypes: string[];
|
||||
allStates: string[];
|
||||
states: string[];
|
||||
onChangeTypes: (input: string) => void;
|
||||
onChangeStates: (input: string) => void;
|
||||
onChangeSearch: (input: string) => void;
|
||||
}
|
||||
|
||||
const RulesHeader: FC<RulesHeaderProps> = ({
|
||||
types,
|
||||
allTypes,
|
||||
allStates,
|
||||
states,
|
||||
onChangeTypes,
|
||||
onChangeStates,
|
||||
onChangeSearch,
|
||||
}) => {
|
||||
const noStateText = useMemo(
|
||||
() => (types.length ? "" : "No states. Please select rule states"),
|
||||
[types],
|
||||
);
|
||||
const { isMobile } = useDeviceDetect();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-explore-alerts-header": true,
|
||||
"vm-explore-alerts-header_mobile": isMobile,
|
||||
"vm-block": true,
|
||||
"vm-block_mobile": isMobile,
|
||||
})}
|
||||
>
|
||||
<div className="vm-explore-alerts-header__rule_type">
|
||||
<Select
|
||||
value={types}
|
||||
list={allTypes}
|
||||
label="Rules type"
|
||||
placeholder="Please select rule type"
|
||||
onChange={onChangeTypes}
|
||||
autofocus={!!types.length && !isMobile}
|
||||
includeAll
|
||||
searchable
|
||||
/>
|
||||
</div>
|
||||
<div className="vm-explore-alerts-header__state">
|
||||
<Select
|
||||
itemClassName="vm-badge-menu-item"
|
||||
value={states}
|
||||
list={allStates}
|
||||
label="State"
|
||||
placeholder="Please rule state"
|
||||
onChange={onChangeStates}
|
||||
noOptionsText={noStateText}
|
||||
includeAll
|
||||
searchable
|
||||
/>
|
||||
</div>
|
||||
<div className="vm-explore-alerts-header-search">
|
||||
<TextField
|
||||
label="Search"
|
||||
placeholder="Filter by rule, name or labels"
|
||||
startIcon={<SearchIcon />}
|
||||
onChange={onChangeSearch}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default RulesHeader;
|
||||
@@ -0,0 +1,65 @@
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-explore-alerts-header {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: $padding-global calc($padding-small + 10px);
|
||||
width: 100%;
|
||||
|
||||
&_mobile {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
&__rule_type {
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
&__state {
|
||||
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 {
|
||||
padding: 11px 28px;
|
||||
}
|
||||
.vm-text-field__icon-start {
|
||||
height: 42px;
|
||||
}
|
||||
}
|
||||
|
||||
&__clear-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
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 from "../Badges";
|
||||
|
||||
interface TargetProps {
|
||||
target: APITarget;
|
||||
}
|
||||
|
||||
const Target: FC<TargetProps> = ({ target }) => {
|
||||
const state = target?.lastError ? "unhealthy" : "ok";
|
||||
return (
|
||||
<div className={`vm-explore-alerts-target vm-badge-item ${state.replace(" ", "-")}`}>
|
||||
{(!!target?.labels?.length || !!target?.lastError) ? (
|
||||
<Accordion
|
||||
key={`target-${target.address}`}
|
||||
title={(
|
||||
<div className="vm-explore-alerts-target-header__name">{target.address}</div>
|
||||
)}
|
||||
>
|
||||
<div className="vm-explore-alerts-target-item">
|
||||
<table>
|
||||
<tbody>
|
||||
{!!target?.labels?.length && (
|
||||
<tr>
|
||||
<td className="vm-col-md">Labels</td>
|
||||
<td>
|
||||
<Badges
|
||||
items={Object.fromEntries(Object.entries(target.labels).map(([name, value]) => [name, {
|
||||
value: value,
|
||||
color: "passive",
|
||||
}]))}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{!!target.lastError && (
|
||||
<tr>
|
||||
<td className="vm-col-md">Last error</td>
|
||||
<td>
|
||||
<Alert variant="error">{target.lastError}</Alert>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Accordion>
|
||||
) : (
|
||||
<span>{target.address}</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Target;
|
||||
@@ -0,0 +1,48 @@
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-explore-alerts-target {
|
||||
row-gap: $padding-global;
|
||||
margin-right: $padding-global;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.vm-col-md {
|
||||
width: 40%;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
td {
|
||||
vertical-align: middle;
|
||||
padding: $padding-global $padding-small;
|
||||
}
|
||||
th {
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
padding: 0 $padding-small;
|
||||
}
|
||||
}
|
||||
|
||||
padding: $padding-tiny;
|
||||
padding-right: 0;
|
||||
display: flex;
|
||||
row-gap: $padding-tiny;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
border-radius: $border-radius-small;
|
||||
|
||||
&:has(>details[open]) {
|
||||
background-color: $color-background-item;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $color-background-item;
|
||||
}
|
||||
|
||||
.vm-explore-alerts-item-header__name {
|
||||
line-height: 22px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import dayjs from "dayjs";
|
||||
|
||||
export const formatDuration = (raw: number) => {
|
||||
const duration = dayjs.duration(Math.round(raw * 1000));
|
||||
const fmt = [];
|
||||
if (duration.get("day")) fmt.push("D[d]");
|
||||
if (duration.get("hour")) fmt.push("H[h]");
|
||||
if (duration.get("minute")) fmt.push("m[m]");
|
||||
if (duration.get("millisecond")) {
|
||||
fmt.push("s.SSS[s]");
|
||||
} else if (!fmt.length || duration.get("second")) {
|
||||
fmt.push("s[s]");
|
||||
}
|
||||
return duration.format(fmt.join(" "));
|
||||
};
|
||||
@@ -1,9 +1,11 @@
|
||||
import { FC, useState, useEffect } from "preact/compat";
|
||||
import { JSX } from "preact";
|
||||
import { ArrowDownIcon } from "../Icons";
|
||||
import "./style.scss";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
interface AccordionProps {
|
||||
id?: string
|
||||
title: ReactNode
|
||||
children: ReactNode
|
||||
defaultExpanded?: boolean
|
||||
@@ -14,21 +16,24 @@ const Accordion: FC<AccordionProps> = ({
|
||||
defaultExpanded = false,
|
||||
onChange,
|
||||
title,
|
||||
children
|
||||
children,
|
||||
id,
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(defaultExpanded);
|
||||
|
||||
const toggleOpen = () => {
|
||||
const toggleOpen = (event: JSX.TargetedMouseEvent<HTMLElement>) => {
|
||||
const selection = window.getSelection();
|
||||
if (selection && selection.toString()) {
|
||||
if ((event.target as HTMLElement).closest("button")) {
|
||||
event.preventDefault();
|
||||
return; // If the text is selected, cancel the execution of toggle.
|
||||
}
|
||||
|
||||
setIsOpen((prev) => {
|
||||
const newState = !prev;
|
||||
onChange && onChange(newState);
|
||||
return newState;
|
||||
});
|
||||
if (selection && selection.toString()) {
|
||||
event.preventDefault();
|
||||
return; // If the text is selected, cancel the execution of toggle.
|
||||
}
|
||||
const details = event.currentTarget.parentElement as HTMLDetailsElement;
|
||||
onChange && onChange(details.open);
|
||||
setIsOpen(details.open);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -37,23 +42,23 @@ const Accordion: FC<AccordionProps> = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<header
|
||||
className={`vm-accordion-header ${isOpen && "vm-accordion-header_open"}`}
|
||||
onClick={toggleOpen}
|
||||
<details
|
||||
className="vm-accordion-section"
|
||||
key="content"
|
||||
open={isOpen}
|
||||
id={id}
|
||||
>
|
||||
{title}
|
||||
<div className={`vm-accordion-header__arrow ${isOpen && "vm-accordion-header__arrow_open"}`}>
|
||||
<ArrowDownIcon />
|
||||
</div>
|
||||
</header>
|
||||
{isOpen && (
|
||||
<section
|
||||
className="vm-accordion-section"
|
||||
key="content"
|
||||
<summary
|
||||
className="vm-accordion-header"
|
||||
onClick={toggleOpen}
|
||||
>
|
||||
{children}
|
||||
</section>
|
||||
)}
|
||||
{title}
|
||||
<div className="vm-accordion-header__arrow">
|
||||
<ArrowDownIcon />
|
||||
</div>
|
||||
</summary>
|
||||
{children}
|
||||
</details>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -17,10 +17,6 @@
|
||||
transform: rotate(0);
|
||||
transition: transform 200ms ease-in-out;
|
||||
|
||||
&_open {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 14px;
|
||||
height: auto;
|
||||
@@ -28,6 +24,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
.vm-accordion-section[open] > summary {
|
||||
& > .vm-accordion-header {
|
||||
&__arrow {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.accordion-section {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -1,41 +1,27 @@
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-alert {
|
||||
position: relative;
|
||||
z-index: 20;
|
||||
position: sticky;
|
||||
top: $padding-global;
|
||||
display: grid;
|
||||
grid-template-columns: 20px 1fr;
|
||||
align-items: center;
|
||||
gap: $padding-small;
|
||||
padding: $padding-global;
|
||||
background-color: $color-background-block;
|
||||
border-radius: $border-radius-medium;
|
||||
box-shadow: $box-shadow;
|
||||
font-size: $font-size;
|
||||
font-weight: normal;
|
||||
color: $color-text;
|
||||
line-height: 1.5;
|
||||
opacity: 0.8;
|
||||
|
||||
&_mobile {
|
||||
align-items: flex-start;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
&:after {
|
||||
position: absolute;
|
||||
content: '';
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: $border-radius-medium;
|
||||
z-index: 1;
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
&_mobile:after {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
&__icon,
|
||||
&__content {
|
||||
position: relative;
|
||||
@@ -48,54 +34,53 @@
|
||||
justify-content: center;
|
||||
align-self: flex-start;
|
||||
min-height: 24px;
|
||||
filter: brightness(0.6);
|
||||
}
|
||||
|
||||
&__content {
|
||||
filter: brightness(0.6);
|
||||
white-space: pre-line;
|
||||
text-wrap: balance;
|
||||
overflow-wrap: anywhere;
|
||||
filter: brightness(0.6);
|
||||
}
|
||||
|
||||
&_success {
|
||||
color: $color-success;
|
||||
|
||||
&:after {
|
||||
background-color: $color-success;
|
||||
}
|
||||
background-color: $color-background-success;
|
||||
}
|
||||
|
||||
&_error {
|
||||
color: $color-error;
|
||||
|
||||
&:after {
|
||||
background-color: $color-error;
|
||||
}
|
||||
background-color: $color-background-error;
|
||||
}
|
||||
|
||||
&_info {
|
||||
color: $color-info;
|
||||
|
||||
&:after {
|
||||
background-color: $color-info;
|
||||
}
|
||||
background-color: $color-background-info;
|
||||
}
|
||||
|
||||
&_warning {
|
||||
color: $color-warning;
|
||||
|
||||
&:after {
|
||||
background-color: $color-warning;
|
||||
}
|
||||
background-color: $color-background-warning;
|
||||
}
|
||||
|
||||
&_dark {
|
||||
&:after {
|
||||
opacity: 0.1;
|
||||
}
|
||||
}
|
||||
|
||||
&_dark &__content {
|
||||
&_dark &__content, &_dark &__icon {
|
||||
filter: none;
|
||||
}
|
||||
|
||||
&_dark:is(&_success) {
|
||||
border: 0.5px solid $color-success;
|
||||
}
|
||||
|
||||
&_dark:is(&_error) {
|
||||
border: 0.5px solid $color-error;
|
||||
}
|
||||
|
||||
&_dark:is(&_info) {
|
||||
border: 0.5px solid $color-info;
|
||||
}
|
||||
|
||||
&_dark:is(&_warning) {
|
||||
border: 0.5px solid $color-warning;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ export interface AutocompleteOptions {
|
||||
}
|
||||
|
||||
interface AutocompleteProps {
|
||||
itemClassName?: string
|
||||
value: string
|
||||
options: AutocompleteOptions[]
|
||||
anchor: React.RefObject<HTMLElement>
|
||||
@@ -41,6 +42,7 @@ enum FocusType {
|
||||
|
||||
const Autocomplete: FC<AutocompleteProps> = ({
|
||||
value,
|
||||
itemClassName,
|
||||
options,
|
||||
anchor,
|
||||
disabled,
|
||||
@@ -212,7 +214,9 @@ const Autocomplete: FC<AutocompleteProps> = ({
|
||||
>
|
||||
{selected?.includes(option.value) && <DoneIcon/>}
|
||||
<>{option.icon}</>
|
||||
<span>{option.value}</span>
|
||||
<div className={`vm-list-item-inner ${itemClassName} ${option.value.toLowerCase().replace(" ", "-")}`}>
|
||||
<span>{option.value}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,61 @@
|
||||
import { getCssVariable } from "../../../utils/theme";
|
||||
|
||||
export const LinkIcon = () => (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M10.975 14.51a1.05 1.05 0 0 0 0-1.485 2.95 2.95 0 0 1 0-4.172l3.536-3.535a2.95 2.95 0 1 1 4.172 4.172l-1.093 1.092a1.05 1.05 0 0 0 1.485 1.485l1.093-1.092a5.05 5.05 0 0 0-7.142-7.142L9.49 7.368a5.05 5.05 0 0 0 0 7.142c.41.41 1.075.41 1.485 0m2.05-5.02a1.05 1.05 0 0 0 0 1.485 2.95 2.95 0 0 1 0 4.172l-3.5 3.5a2.95 2.95 0 1 1-4.171-4.172l1.025-1.025a1.05 1.05 0 0 0-1.485-1.485L3.87 12.99a5.05 5.05 0 0 0 7.142 7.142l3.5-3.5a5.05 5.05 0 0 0 0-7.142 1.05 1.05 0 0 0-1.485 0z"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const GroupIcon = () => (
|
||||
<svg
|
||||
fill="currentColor"
|
||||
viewBox="0 0 512 512"
|
||||
>
|
||||
<path d="M170.667 64v42.667h-64v298.666h64V448H64V64zM448 64v384H341.333v-42.667h64V106.667h-64V64zm-85.333 256v42.667H149.333V320zm0-85.333v42.666H149.333v-42.666zm0-85.334V192H149.333v-42.667z"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const DetailsIcon = () => (
|
||||
<svg
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path d="M12 3a2 2 0 1 0-4 0 2 2 0 0 0 4 0m-2 5a2 2 0 1 1 0 4 2 2 0 0 1 0-4m0 7a2 2 0 1 1 0 4 2 2 0 0 1 0-4"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const AlertIcon = () => (
|
||||
<svg
|
||||
viewBox="-1 0 30 30"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="m3 24 3-6v-8a8 8 0 0 1 16 0v8l3 6zm11 4a2.99 2.99 0 0 1-2.816-2h5.632A2.99 2.99 0 0 1 14 28m10-10v-8c0-5.522-4.478-10-10-10S4 4.478 4 10v8l-4 8h9.101a5 5 0 0 0 9.798 0H28z"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const AlertingRuleIcon = () => (
|
||||
<svg
|
||||
viewBox="411.014 448.582 21.637 17.836"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M411.47 453.837a.72.72 0 0 0-.422.679c0 .319.137.535.422.667.139.065.322.068 3.814.069l3.668.001.164-.083c.484-.245.535-.904.096-1.254l-.141-.112-3.746-.008c-3.05-.006-3.766.001-3.855.041m-.016 5.54c-.284.124-.44.372-.439.701.001.354.204.614.553.708.092.025 1.214.034 3.267.028 3.481-.011 3.23.008 3.468-.264a.714.714 0 0 0-.003-.963c-.249-.283-.03-.267-3.564-.266-2.735 0-3.171.008-3.282.056m-.018 5.016a.72.72 0 0 0-.422.679c0 .319.137.535.422.667.139.065.322.068 3.814.069l3.668.001.164-.083c.484-.245.535-.904.096-1.254l-.141-.112-3.746-.008c-3.05-.006-3.766.001-3.855.041m.161-15.761a.85.85 0 0 0-.389.417c-.023.07-.034.224-.026.343.018.239.078.345.301.525l.139.112 8.729.01 8.73.009.163-.079c.193-.093.36-.332.395-.565a.75.75 0 0 0-.388-.75c-.142-.072-.227-.073-8.844-.072-7.105.002-8.721.011-8.81.05m14.005 2.595c-.286.18-.371.401-.371.961v.334l-.499.024c-.598.028-.961.126-1.456.392a3.5 3.5 0 0 0-1.721 2.199c-.081.307-.091.479-.115 1.923-.027 1.566-.028 1.59-.138 1.966-.145.496-.557 1.361-.929 1.945a5 5 0 0 0-.368.677c-.1.292-.095.679.013.982.112.32.461.686.75.789.276.099 1.255.259 2.268.373l.84.095.028.287q.089.935.767 1.579a2.383 2.383 0 0 0 3.659-.434c.227-.351.36-.745.396-1.161l.023-.283.291-.027c.956-.093 2.47-.32 2.715-.408.393-.14.694-.464.817-.875.16-.539.093-.833-.354-1.554-.373-.601-.832-1.565-.956-2.007-.083-.29-.093-.448-.119-1.903-.027-1.427-.039-1.619-.118-1.924a3.5 3.5 0 0 0-1.895-2.327c-.422-.202-.758-.282-1.309-.312l-.489-.025-.022-.473c-.022-.521-.062-.621-.325-.806-.124-.088-.182-.096-.69-.096-.489.001-.57.012-.693.089m2.696 2.786c.546.176.994.583 1.249 1.135l.149.326.025 1.543c.027 1.672.046 1.837.286 2.598.166.52.621 1.468.974 2.028.189.303.274.472.244.492-.104.066-1.778.288-2.915.387-.788.068-3.246.068-4.037 0-1.154-.099-2.811-.32-2.919-.39-.035-.023.03-.16.223-.469.375-.603.805-1.493.976-2.024.246-.763.272-1 .272-2.426 0-.701.019-1.398.043-1.549.083-.554.47-1.148.931-1.429.103-.063.308-.157.453-.209.258-.092.315-.094 2.025-.096 1.642-.001 1.776.005 2.021.083m-1.384 10.771a1.06 1.06 0 0 1-.748.2c-.394-.066-.776-.451-.835-.841l-.026-.168h2.005v.108z"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const RecordingRuleIcon = () => (
|
||||
<svg
|
||||
viewBox="411.014 448.582 23.358 18.492"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M411.47 453.837a.72.72 0 0 0-.422.679c0 .319.137.535.422.667.139.065.322.068 3.814.069l3.668.001.164-.083c.484-.245.535-.904.096-1.254l-.141-.112-3.746-.008c-3.05-.006-3.766.001-3.855.041m15.967 7.103a1.592 1.612 0 1 1 1.592-1.612 1.592 1.612 0 0 1-1.592 1.612m0-1.612"/>
|
||||
<path d="M427.405 466.377a6.966 7.052 0 1 1 6.965-7.053 6.974 7.06 0 0 1-6.965 7.053m0-12.09a4.975 5.037 0 1 0 4.975 5.037 4.981 5.043 0 0 0-4.975-5.037"/>
|
||||
<path d="M421.832 467.074a.996 1.008 0 0 1-.708-1.715l3.582-3.675a.995 1.008 0 0 1 1.417 1.415l-3.582 3.675a.995 1.007 0 0 1-.709.3m-10.378-7.697c-.284.124-.44.372-.439.701.001.354.204.614.553.708.092.025 1.214.034 3.267.028 3.481-.011 3.23.008 3.468-.264a.714.714 0 0 0-.003-.963c-.249-.283-.03-.267-3.564-.266-2.735 0-3.171.008-3.282.056m-.018 5.016a.72.72 0 0 0-.422.679c0 .319.137.535.422.667.139.065.322.068 3.814.069l3.668.001.164-.083c.484-.245.535-.904.096-1.254l-.141-.112-3.746-.008c-3.05-.006-3.766.001-3.855.041m.161-15.761a.85.85 0 0 0-.389.417c-.023.07-.034.224-.026.343.018.239.078.345.301.525l.139.112 8.729.01 8.73.009.163-.079c.193-.093.36-.332.395-.565a.75.75 0 0 0-.388-.75c-.142-.072-.227-.073-8.844-.072-7.105.002-8.721.011-8.81.05"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const LogoIcon = () => (
|
||||
<svg
|
||||
viewBox="0 0 74 24"
|
||||
@@ -578,97 +634,13 @@ export const CommentIcon = () => (
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const FilterIcon = () => (
|
||||
export const DebugIcon = () => (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
d="M4.25 5.61C6.27 8.2 10 13 10 13v6c0 .55.45 1 1 1h2c.55 0 1-.45 1-1v-6s3.72-4.8 5.74-7.39c.51-.66.04-1.61-.79-1.61H5.04c-.83 0-1.3.95-.79 1.61"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const FilterOffIcon = () => (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
d="M19.79 5.61C20.3 4.95 19.83 4 19 4H6.83l7.97 7.97zM2.81 2.81 1.39 4.22 10 13v6c0 .55.45 1 1 1h2c.55 0 1-.45 1-1v-2.17l5.78 5.78 1.41-1.41z"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const OpenNewIcon = () => (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
d="M19 19H5V5h7V3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2v-7h-2zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3z"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const ModalIcon = () => (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M19 4H5c-1.11 0-2 .9-2 2v12c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.89-2-2-2m0 14H5V8h14z"></path>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const PauseIcon = () => (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const ScrollToTopIcon = () => (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
d="M8 12l4-4 4 4m-4-4v12"
|
||||
strokeWidth="2"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
d="M20 8h-2.81c-.45-.78-1.07-1.45-1.82-1.96L17 4.41 15.59 3l-2.17 2.17C12.96 5.06 12.49 5 12 5c-.49 0-.96.06-1.41.17L8.41 3 7 4.41l1.62 1.63C7.88 6.55 7.26 7.22 6.81 8H4v2h2.09c-.05.33-.09.66-.09 1v1H4v2h2v1c0 .34.04.67.09 1H4v2h2.81c1.04 1.79 2.97 3 5.19 3s4.15-1.21 5.19-3H20v-2h-2.09c.05-.33.09-.66.09-1v-1h2v-2h-2v-1c0-.34-.04-.67-.09-1H20V8zm-6 8h-4v-2h4v2zm0-4h-4v-2h4v2z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const SortIcon = () => (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M4 3 L4 15 L1.5 15 L5.5 21 L9.5 15 L7 15 L7 3 Z"/>
|
||||
<path d="M13 21 L13 9 L10.5 9 L14.5 3 L18.5 9 L16 9 L16 21 Z"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const SortArrowDownIcon = () => (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M10.5 3 L10.5 15 L8 15 L12 21 L16 15 L13.5 15 L13.5 3 Z"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const SortArrowUpIcon = () => (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M10.5 21 L10.5 9 L8 9 L12 3 L16 9 L13.5 9 L13.5 21 Z"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { FC, useCallback, useEffect, createPortal } from "preact/compat";
|
||||
import { JSX } from "preact/jsx-runtime";
|
||||
import { CloseIcon } from "../Icons";
|
||||
import Button from "../Button/Button";
|
||||
import { ReactNode, MouseEvent } from "react";
|
||||
@@ -9,7 +10,7 @@ import { useLocation, useNavigate } from "react-router-dom";
|
||||
import useEventListener from "../../../hooks/useEventListener";
|
||||
|
||||
interface ModalProps {
|
||||
title?: string
|
||||
title: JSX.Element | string
|
||||
children: ReactNode
|
||||
onClose: () => void
|
||||
className?: string
|
||||
|
||||
@@ -5,10 +5,11 @@ import { MouseEvent } from "react";
|
||||
|
||||
interface MultipleSelectedValueProps {
|
||||
values: string[]
|
||||
itemClassName?: string
|
||||
onRemoveItem: (val: string) => void
|
||||
}
|
||||
|
||||
const MultipleSelectedValue: FC<MultipleSelectedValueProps> = ({ values, onRemoveItem }) => {
|
||||
const MultipleSelectedValue: FC<MultipleSelectedValueProps> = ({ values, itemClassName, onRemoveItem }) => {
|
||||
const { isMobile } = useDeviceDetect();
|
||||
|
||||
const createHandleClick = (value: string) => (e: MouseEvent<HTMLDivElement>) => {
|
||||
@@ -27,7 +28,7 @@ const MultipleSelectedValue: FC<MultipleSelectedValueProps> = ({ values, onRemov
|
||||
return <>
|
||||
{values.map(item => (
|
||||
<div
|
||||
className="vm-select-input-content__selected"
|
||||
className={`vm-select-input-content__selected ${itemClassName} ${item.toLowerCase().replace(" ", "-")}`}
|
||||
key={item}
|
||||
>
|
||||
<span>{item}</span>
|
||||
|
||||
@@ -11,6 +11,7 @@ import useEventListener from "../../../hooks/useEventListener";
|
||||
import useClickOutside from "../../../hooks/useClickOutside";
|
||||
|
||||
interface SelectProps {
|
||||
itemClassName?: string
|
||||
value: string | string[]
|
||||
list: string[]
|
||||
label?: string
|
||||
@@ -20,6 +21,7 @@ interface SelectProps {
|
||||
searchable?: boolean
|
||||
autofocus?: boolean
|
||||
disabled?: boolean
|
||||
includeAll?: boolean
|
||||
onChange: (value: string) => void
|
||||
}
|
||||
|
||||
@@ -27,12 +29,14 @@ const Select: FC<SelectProps> = ({
|
||||
value,
|
||||
list,
|
||||
label,
|
||||
itemClassName,
|
||||
placeholder,
|
||||
noOptionsText,
|
||||
clearable = false,
|
||||
searchable = false,
|
||||
autofocus,
|
||||
disabled,
|
||||
includeAll,
|
||||
onChange
|
||||
}) => {
|
||||
const { isDarkTheme } = useAppState();
|
||||
@@ -46,7 +50,7 @@ const Select: FC<SelectProps> = ({
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const isMultiple = Array.isArray(value);
|
||||
const selectedValues = Array.isArray(value) ? value : undefined;
|
||||
const selectedValues = Array.isArray(value) ? value.slice() : [];
|
||||
const hideInput = isMobile && isMultiple && !!selectedValues?.length;
|
||||
|
||||
const textFieldValue = useMemo(() => {
|
||||
@@ -119,6 +123,9 @@ const Select: FC<SelectProps> = ({
|
||||
useEventListener("keyup", handleKeyUp);
|
||||
useClickOutside(autocompleteAnchorEl, handleCloseList, wrapperRef);
|
||||
|
||||
includeAll && !list.includes("All") && list.push("All");
|
||||
includeAll && !selectedValues?.length && selectedValues.push("All");
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames({
|
||||
@@ -135,11 +142,12 @@ const Select: FC<SelectProps> = ({
|
||||
<div className="vm-select-input-content">
|
||||
{!!selectedValues?.length && (
|
||||
<MultipleSelectedValue
|
||||
itemClassName={itemClassName}
|
||||
values={selectedValues}
|
||||
onRemoveItem={handleSelected}
|
||||
/>
|
||||
)}
|
||||
{!hideInput && (
|
||||
{!hideInput && !selectedValues?.length && (
|
||||
<input
|
||||
value={textFieldValue}
|
||||
type="text"
|
||||
@@ -171,9 +179,10 @@ const Select: FC<SelectProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
<Autocomplete
|
||||
itemClassName={itemClassName}
|
||||
label={label}
|
||||
value={autocompleteValue}
|
||||
options={list.map(el => ({ value: el }))}
|
||||
options={list.map(l => ({ value: l }))}
|
||||
anchor={autocompleteAnchorEl}
|
||||
selected={selectedValues}
|
||||
minLength={1}
|
||||
|
||||
17
app/vmui/packages/vmui/src/constants/alerts.ts
Normal file
17
app/vmui/packages/vmui/src/constants/alerts.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { RuleType } from "../types";
|
||||
|
||||
export const RULE_TYPES: RuleType[] = [
|
||||
{
|
||||
id: "alerts",
|
||||
title: "Alerts",
|
||||
isDefault: true,
|
||||
},
|
||||
{
|
||||
title: "Recording",
|
||||
id: "recording",
|
||||
},
|
||||
{
|
||||
title: "All",
|
||||
id: "all",
|
||||
},
|
||||
];
|
||||
@@ -7,4 +7,11 @@ export const APP_TYPE = import.meta.env.VITE_APP_TYPE;
|
||||
export const APP_TYPE_VM = APP_TYPE === AppType.victoriametrics;
|
||||
export const APP_TYPE_ANOMALY = APP_TYPE === AppType.vmanomaly;
|
||||
|
||||
|
||||
export const isDefaultDatasourceType = (datasourceType: string): boolean => {
|
||||
switch (APP_TYPE) {
|
||||
case AppType.victoriametrics:
|
||||
return datasourceType == "prometheus" || datasourceType == "";
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
export const darkPalette = {
|
||||
"color-primary": "#589DF6",
|
||||
"color-primary": "#589df6",
|
||||
"color-secondary": "#316eca",
|
||||
"color-error": "#e5534b",
|
||||
"color-background-error": "#240705",
|
||||
"color-warning": "#c69026",
|
||||
"color-background-warning": "#221906",
|
||||
"color-info": "#539bf5",
|
||||
"color-background-info": "#021327",
|
||||
"color-success": "#57ab5a",
|
||||
"color-background-success": "#0e1b0e",
|
||||
"color-passive": "#a7acb3",
|
||||
"color-background-body": "#22272e",
|
||||
"color-background-block": "#2d333b",
|
||||
"color-background-tooltip": "rgba(22, 22, 22, 0.8)",
|
||||
"color-background-item": "#313944",
|
||||
"color-background-badge": "#4e5a6a",
|
||||
"color-background-hover": "#3D4652",
|
||||
"color-text": "#cdd9e5",
|
||||
"color-text-secondary": "#768390",
|
||||
"color-text-disabled": "#636e7b",
|
||||
@@ -25,15 +33,23 @@ export const darkPalette = {
|
||||
};
|
||||
|
||||
export const lightPalette = {
|
||||
"color-primary": "#3F51B5",
|
||||
"color-secondary": "#E91E63",
|
||||
"color-error": "#FD080E",
|
||||
"color-warning": "#FF8308",
|
||||
"color-info": "#03A9F4",
|
||||
"color-success": "#4CAF50",
|
||||
"color-primary": "#3f51b5",
|
||||
"color-secondary": "#e91e63",
|
||||
"color-error": "#fd080e",
|
||||
"color-background-error": "#ffd7d8",
|
||||
"color-warning": "#ff8308",
|
||||
"color-background-warning": "#ffd6ad",
|
||||
"color-info": "#03a9f4",
|
||||
"color-background-info": "#d7f2fe",
|
||||
"color-success": "#4caf50",
|
||||
"color-background-success": "#d4ecd5",
|
||||
"color-passive": "#5d6267",
|
||||
"color-background-body": "#FEFEFF",
|
||||
"color-background-block": "#FFFFFF",
|
||||
"color-background-tooltip": "rgba(80,80,80,0.9)",
|
||||
"color-background-item": "#f8f9fa",
|
||||
"color-background-badge": "#e1e4e7",
|
||||
"color-background-hover": "#edf0f2",
|
||||
"color-text": "#110f0f",
|
||||
"color-text-secondary": "#706F6F",
|
||||
"color-text-disabled": "#A09F9F",
|
||||
|
||||
@@ -18,7 +18,7 @@ const useFetchFlags = () => {
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const url = getUrlWithoutTenant(serverUrl).replace(/\/prometheus\/?$/, "");
|
||||
const url = getUrlWithoutTenant(serverUrl);
|
||||
const response = await fetch(`${url}/flags`);
|
||||
const data = await response.text();
|
||||
const flags = data.split("\n").filter(flag => flag.trim() !== "")
|
||||
|
||||
@@ -7,12 +7,20 @@ const useSearchParamsFromObject = () => {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
const setSearchParamsFromKeys = useCallback((objectParams: Record<string, string | number>) => {
|
||||
const hasSearchParams = !!Array.from(searchParams.values()).length;
|
||||
const hasSearchParams = !!searchParams.size;
|
||||
let hasChanged = false;
|
||||
|
||||
const newSearchParams = new URLSearchParams(searchParams);
|
||||
searchParams.keys().forEach(key => {
|
||||
if (!(key in objectParams)) {
|
||||
newSearchParams.delete(key);
|
||||
hasChanged = true;
|
||||
}
|
||||
});
|
||||
|
||||
Object.entries(objectParams).forEach(([key, value]) => {
|
||||
if (searchParams.get(key) !== `${value}`) {
|
||||
searchParams.set(key, `${value}`);
|
||||
if (newSearchParams.get(key) !== `${value}`) {
|
||||
newSearchParams.set(key, `${value}`);
|
||||
hasChanged = true;
|
||||
}
|
||||
});
|
||||
@@ -20,7 +28,7 @@ const useSearchParamsFromObject = () => {
|
||||
if (!hasChanged) return;
|
||||
|
||||
if (hasSearchParams) {
|
||||
setSearchParams(searchParams);
|
||||
setSearchParams(newSearchParams);
|
||||
} else {
|
||||
navigate(`?${searchParams.toString()}`, { replace: true });
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@ const ControlsAnomalyLayout: FC<ControlsProps> = ({
|
||||
displaySidebar,
|
||||
isMobile,
|
||||
headerSetup,
|
||||
accountIds
|
||||
accountIds,
|
||||
closeModal,
|
||||
}) => {
|
||||
|
||||
return (
|
||||
@@ -28,7 +29,11 @@ const ControlsAnomalyLayout: FC<ControlsProps> = ({
|
||||
{headerSetup?.stepControl && <StepConfigurator/>}
|
||||
{headerSetup?.timeSelector && <TimeSelector/>}
|
||||
{headerSetup?.cardinalityDatePicker && <CardinalityDatePicker/>}
|
||||
{headerSetup?.executionControls && <ExecutionControls/>}
|
||||
{headerSetup?.executionControls && <ExecutionControls
|
||||
tooltip={headerSetup?.executionControls?.tooltip}
|
||||
useAutorefresh={headerSetup?.executionControls?.useAutorefresh}
|
||||
closeModal={closeModal}
|
||||
/>}
|
||||
<GlobalSettings/>
|
||||
{!displaySidebar && <ShortcutKeys/>}
|
||||
</div>
|
||||
|
||||
@@ -105,6 +105,7 @@ const Header: FC<HeaderProps> = ({ controlsComponent }) => {
|
||||
controlsComponent={controlsComponent}
|
||||
displaySidebar={displaySidebar}
|
||||
isMobile={isMobile}
|
||||
closeModal={() => {}}
|
||||
/>
|
||||
</header>;
|
||||
};
|
||||
|
||||
@@ -18,6 +18,7 @@ export interface ControlsProps {
|
||||
isMobile?: boolean;
|
||||
headerSetup?: RouterOptionsHeader;
|
||||
accountIds?: string[];
|
||||
closeModal: () => void;
|
||||
}
|
||||
|
||||
const HeaderControls: FC<ControlsProps & HeaderProps> = ({
|
||||
@@ -45,6 +46,7 @@ const HeaderControls: FC<ControlsProps & HeaderProps> = ({
|
||||
isMobile={isMobile}
|
||||
accountIds={accountIds}
|
||||
headerSetup={headerSetup}
|
||||
closeModal={handleCloseList}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -14,7 +14,8 @@ const ControlsMainLayout: FC<ControlsProps> = ({
|
||||
displaySidebar,
|
||||
isMobile,
|
||||
headerSetup,
|
||||
accountIds
|
||||
accountIds,
|
||||
closeModal,
|
||||
}) => {
|
||||
|
||||
return (
|
||||
@@ -28,7 +29,11 @@ const ControlsMainLayout: FC<ControlsProps> = ({
|
||||
{headerSetup?.stepControl && <StepConfigurator/>}
|
||||
{headerSetup?.timeSelector && <TimeSelector/>}
|
||||
{headerSetup?.cardinalityDatePicker && <CardinalityDatePicker/>}
|
||||
{headerSetup?.executionControls && <ExecutionControls/>}
|
||||
{headerSetup?.executionControls && <ExecutionControls
|
||||
tooltip={headerSetup?.executionControls?.tooltip}
|
||||
useAutorefresh={headerSetup?.executionControls?.useAutorefresh}
|
||||
closeModal={closeModal}
|
||||
/>}
|
||||
<GlobalSettings/>
|
||||
{!displaySidebar && <ShortcutKeys/>}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { FC, useCallback, useEffect, useRef, useState } from "preact/compat";
|
||||
import { DownloadIcon } from "../../../components/Main/Icons";
|
||||
import { DebugIcon } from "../../../components/Main/Icons";
|
||||
import Button from "../../../components/Main/Button/Button";
|
||||
import Tooltip from "../../../components/Main/Tooltip/Tooltip";
|
||||
import useBoolean from "../../../hooks/useBoolean";
|
||||
@@ -217,17 +217,17 @@ const DownloadReport: FC<Props> = ({ fetchUrl, reportType = ReportType.QUERY_DAT
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip title={"Export query"}>
|
||||
<Tooltip title={"Debug query"}>
|
||||
<Button
|
||||
variant="text"
|
||||
startIcon={<DownloadIcon/>}
|
||||
startIcon={<DebugIcon />}
|
||||
onClick={toggleOpen}
|
||||
ariaLabel="export query"
|
||||
ariaLabel="Debug query"
|
||||
/>
|
||||
</Tooltip>
|
||||
{openModal && (
|
||||
<Modal
|
||||
title={"Export query"}
|
||||
title={"Debug query"}
|
||||
onClose={handleClose}
|
||||
isOpen={openModal}
|
||||
>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FC, useEffect, useState } from "preact/compat";
|
||||
import { FC, useEffect, useState, useMemo, useRef, useCallback } from "preact/compat";
|
||||
import QueryConfigurator from "./QueryConfigurator/QueryConfigurator";
|
||||
import { useFetchQuery } from "../../hooks/useFetchQuery";
|
||||
import { DisplayTypeSwitch } from "./DisplayTypeSwitch";
|
||||
@@ -12,13 +12,17 @@ import Alert from "../../components/Main/Alert/Alert";
|
||||
import classNames from "classnames";
|
||||
import useDeviceDetect from "../../hooks/useDeviceDetect";
|
||||
import InstantQueryTip from "./InstantQueryTip/InstantQueryTip";
|
||||
import { useRef } from "react";
|
||||
import CustomPanelTraces from "./CustomPanelTraces/CustomPanelTraces";
|
||||
import WarningLimitSeries from "./WarningLimitSeries/WarningLimitSeries";
|
||||
import CustomPanelTabs from "./CustomPanelTabs";
|
||||
import { DisplayType } from "../../types";
|
||||
import DownloadReport from "./DownloadReport/DownloadReport";
|
||||
import WarningHeatmapToLine from "./WarningHeatmapToLine/WarningHeatmapToLine";
|
||||
import DownloadButton from "../../components/DownloadButton/DownloadButton";
|
||||
import { downloadCSV, downloadJSON } from "../../utils/file";
|
||||
import { convertMetricsDataToCSV } from "./utils";
|
||||
|
||||
type ExportFormats = "csv" | "json";
|
||||
|
||||
const CustomPanel: FC = () => {
|
||||
useSetQueryParams();
|
||||
@@ -55,6 +59,27 @@ const CustomPanel: FC = () => {
|
||||
showAllSeries
|
||||
});
|
||||
|
||||
const fileDownloaders = useMemo(() => {
|
||||
const getFilename = (format: ExportFormats) => {
|
||||
return `vmui_export_${query.join("_")}.${format}`;
|
||||
};
|
||||
|
||||
return {
|
||||
csv: async () => {
|
||||
if(!liveData) return;
|
||||
const csvData = convertMetricsDataToCSV(liveData);
|
||||
downloadCSV(csvData, getFilename("csv"));
|
||||
},
|
||||
json: async () => {
|
||||
downloadJSON(JSON.stringify(liveData), getFilename("json"));
|
||||
},
|
||||
};
|
||||
}, [liveData, query]);
|
||||
|
||||
const onDownloadClick = useCallback((format?: ExportFormats) => {
|
||||
format && fileDownloaders[format]();
|
||||
}, [fileDownloaders]);
|
||||
|
||||
const showInstantQueryTip = !liveData?.length && (displayType !== DisplayType.chart);
|
||||
const showError = !hideError && error;
|
||||
|
||||
@@ -110,7 +135,7 @@ const CustomPanel: FC = () => {
|
||||
"vm-block_mobile": isMobile,
|
||||
})}
|
||||
>
|
||||
{isLoading && <LineLoader />}
|
||||
{isLoading && <LineLoader/>}
|
||||
<div
|
||||
className="vm-custom-panel-body-header"
|
||||
ref={controlsRef}
|
||||
@@ -118,7 +143,13 @@ const CustomPanel: FC = () => {
|
||||
<div className="vm-custom-panel-body-header__tabs">
|
||||
<DisplayTypeSwitch/>
|
||||
</div>
|
||||
{(graphData || liveData) && <DownloadReport fetchUrl={fetchUrl}/>}
|
||||
{displayType === "table" && (
|
||||
<DownloadButton
|
||||
title={"Export query"}
|
||||
onDownload={onDownloadClick}
|
||||
downloadFormatOptions={["json", "csv"]}
|
||||
/>)}
|
||||
{(graphData || liveData) && displayType !== "code" && <DownloadReport fetchUrl={fetchUrl}/>}
|
||||
</div>
|
||||
<CustomPanelTabs
|
||||
graphData={graphData}
|
||||
|
||||
86
app/vmui/packages/vmui/src/pages/CustomPanel/utils.test.ts
Normal file
86
app/vmui/packages/vmui/src/pages/CustomPanel/utils.test.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { convertMetricsDataToCSV } from "./utils";
|
||||
import { InstantMetricResult } from "../../api/types";
|
||||
|
||||
describe("convertMetricsDataToCSV", () => {
|
||||
it("should return an empty string if headers are empty", () => {
|
||||
const data: InstantMetricResult[] = [];
|
||||
expect(convertMetricsDataToCSV(data)).toBe("");
|
||||
});
|
||||
|
||||
it("should return a valid CSV string for single metric entry with value", () => {
|
||||
const data: InstantMetricResult[] = [
|
||||
{
|
||||
value: [1623945600, "123"],
|
||||
group: 0,
|
||||
metric: {
|
||||
header1: "123",
|
||||
header2: "value2"
|
||||
}
|
||||
},
|
||||
];
|
||||
const result = convertMetricsDataToCSV(data);
|
||||
expect(result).toBe("header1,header2\n123,value2");
|
||||
});
|
||||
|
||||
it("should return a valid CSV string for multiple metric entries with values", () => {
|
||||
const data: InstantMetricResult[] = [
|
||||
{
|
||||
value: [1623945600, "123"],
|
||||
group: 0,
|
||||
metric: {
|
||||
header1: "123",
|
||||
header2: "value2"
|
||||
}
|
||||
},
|
||||
{
|
||||
value: [1623949200, "456"],
|
||||
group: 0,
|
||||
metric: {
|
||||
header1: "456",
|
||||
header2: "value4"
|
||||
}
|
||||
},
|
||||
];
|
||||
const result = convertMetricsDataToCSV(data);
|
||||
expect(result).toBe("header1,header2\n123,value2\n456,value4");
|
||||
});
|
||||
|
||||
it("should handle metric entries with multiple values field", () => {
|
||||
const data: InstantMetricResult[] = [
|
||||
{
|
||||
values: [[1623945600, "123"], [1623949200, "456"]],
|
||||
group: 0,
|
||||
metric: {
|
||||
header1: "123-456",
|
||||
header2: "values"
|
||||
}
|
||||
},
|
||||
];
|
||||
const result = convertMetricsDataToCSV(data);
|
||||
expect(result).toBe("header1,header2\n123-456,values");
|
||||
});
|
||||
|
||||
it("should handle a combination of metric entries with value and values", () => {
|
||||
const data: InstantMetricResult[] = [
|
||||
{
|
||||
value: [1623945600, "123"],
|
||||
group: 0,
|
||||
metric: {
|
||||
header1: "123",
|
||||
header2: "first"
|
||||
}
|
||||
},
|
||||
{
|
||||
values: [[1623949200, "456"], [1623952800, "789"]],
|
||||
group: 0,
|
||||
metric: {
|
||||
header1: "456-789",
|
||||
header2: "second"
|
||||
}
|
||||
},
|
||||
];
|
||||
const result = convertMetricsDataToCSV(data);
|
||||
expect(result).toBe("header1,header2\n123,first\n456-789,second");
|
||||
});
|
||||
});
|
||||
18
app/vmui/packages/vmui/src/pages/CustomPanel/utils.ts
Normal file
18
app/vmui/packages/vmui/src/pages/CustomPanel/utils.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { InstantMetricResult } from "../../api/types";
|
||||
import { getColumns, MetricCategory } from "../../hooks/useSortedCategories";
|
||||
import { formatValueToCSV } from "../../utils/csv";
|
||||
|
||||
const getHeaders = (data: InstantMetricResult[]): string => {
|
||||
return getColumns(data).map(({ key }) => key).join(",");
|
||||
};
|
||||
|
||||
const getRows = (data: InstantMetricResult[], headers: MetricCategory[]) => {
|
||||
return data?.map(d => headers.map(c => formatValueToCSV(d.metric[c.key] || "-")).join(","));
|
||||
};
|
||||
|
||||
export const convertMetricsDataToCSV = (data: InstantMetricResult[]): string => {
|
||||
const headers = getHeaders(data);
|
||||
if (!headers.length) return "";
|
||||
const rows = getRows(data, getColumns(data));
|
||||
return [headers, ...rows].join("\n");
|
||||
};
|
||||
@@ -0,0 +1,62 @@
|
||||
import Spinner from "../../components/Main/Spinner/Spinner";
|
||||
import Alert from "../../components/Main/Alert/Alert";
|
||||
import { useFetchItem } from "./hooks/useFetchItem";
|
||||
import "./style.scss";
|
||||
import { Alert as APIAlert } from "../../types";
|
||||
import ItemHeader from "../../components/ExploreAlerts/ItemHeader";
|
||||
import BaseAlert from "../../components/ExploreAlerts/BaseAlert";
|
||||
import Modal from "../../components/Main/Modal/Modal";
|
||||
|
||||
interface ExploreAlertProps {
|
||||
groupId: string;
|
||||
id: string;
|
||||
mode: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const ExploreAlert = ({ groupId, id, mode, onClose }: ExploreAlertProps) => {
|
||||
const {
|
||||
item,
|
||||
isLoading,
|
||||
error,
|
||||
} = useFetchItem<APIAlert>({ groupId, id, mode });
|
||||
|
||||
if (isLoading) return (
|
||||
<Spinner />
|
||||
);
|
||||
|
||||
if (error) return (
|
||||
<Alert variant="error">{error}</Alert>
|
||||
);
|
||||
|
||||
const noItemFound = `No alert with group ID=${groupId}, alert ID=${id} found!`;
|
||||
const states = {
|
||||
firing: 1,
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className="vm-explore-alerts"
|
||||
title={item ? (
|
||||
<ItemHeader
|
||||
entity="alert"
|
||||
type="alerting"
|
||||
groupId={item.group_id}
|
||||
id={item.id}
|
||||
name={item.name}
|
||||
states={states}
|
||||
onClose={onClose}
|
||||
/>
|
||||
) : "Alert not found"}
|
||||
onClose={onClose}
|
||||
>
|
||||
<div className="vm-explore-alerts">
|
||||
{item && (<BaseAlert item={item} />) || (
|
||||
<Alert variant="info">{noItemFound}</Alert>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExploreAlert;
|
||||
@@ -0,0 +1,55 @@
|
||||
import Spinner from "../../components/Main/Spinner/Spinner";
|
||||
import Alert from "../../components/Main/Alert/Alert";
|
||||
import { useFetchGroup } from "./hooks/useFetchGroup";
|
||||
import "./style.scss";
|
||||
import { Group as APIGroup } from "../../types";
|
||||
import ItemHeader from "../../components/ExploreAlerts/ItemHeader";
|
||||
import BaseGroup from "../../components/ExploreAlerts/BaseGroup";
|
||||
import Modal from "../../components/Main/Modal/Modal";
|
||||
|
||||
interface ExploreGroupProps {
|
||||
id: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const ExploreGroup = ({ id, onClose }: ExploreGroupProps) => {
|
||||
const {
|
||||
group,
|
||||
isLoading,
|
||||
error,
|
||||
} = useFetchGroup<APIGroup>({ id });
|
||||
|
||||
if (isLoading) return (
|
||||
<Spinner />
|
||||
);
|
||||
|
||||
if (error) return (
|
||||
<Alert variant="error">{error}</Alert>
|
||||
);
|
||||
|
||||
const noGroupFound = `No group ID=${id} found!`;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className="vm-explore-alerts"
|
||||
title={group ? (
|
||||
<ItemHeader
|
||||
entity="group"
|
||||
groupId={id}
|
||||
name={group.name}
|
||||
states={group.states}
|
||||
onClose={onClose}
|
||||
/>
|
||||
) : "Rule not found"}
|
||||
onClose={onClose}
|
||||
>
|
||||
<div className="vm-explore-alerts">
|
||||
{group && (<BaseGroup group={group} />) || (
|
||||
<Alert variant="info">{noGroupFound}</Alert>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExploreGroup;
|
||||
@@ -0,0 +1,142 @@
|
||||
import { FC, useEffect, useState } from "preact/compat";
|
||||
import { useLocation } from "react-router";
|
||||
import { useNotifiersSetQueryParams as useSetQueryParams } from "./hooks/useSetQueryParams";
|
||||
import Spinner from "../../components/Main/Spinner/Spinner";
|
||||
import Alert from "../../components/Main/Alert/Alert";
|
||||
import Accordion from "../../components/Main/Accordion/Accordion";
|
||||
import { useFetchNotifiers } from "./hooks/useFetchNotifiers";
|
||||
import "./style.scss";
|
||||
import NotifiersHeader from "../../components/ExploreAlerts/NotifiersHeader";
|
||||
import NotifierHeader from "../../components/ExploreAlerts/NotifierHeader";
|
||||
import Target from "../../components/ExploreAlerts/Target";
|
||||
import { Notifier as APINotifier, Target as APITarget } from "../../types";
|
||||
import { getQueryStringValue } from "../../utils/query-string";
|
||||
import { getChanges } from "./helpers";
|
||||
import debounce from "lodash.debounce";
|
||||
|
||||
const defaultKindsStr = getQueryStringValue("kinds", "") as string;
|
||||
const defaultKinds = defaultKindsStr.split("&").filter((rt) => rt) as string[];
|
||||
const defaultSearchInput = getQueryStringValue("search", "") as string;
|
||||
|
||||
const ExploreNotifiers: FC = () => {
|
||||
const {
|
||||
notifiers,
|
||||
isLoading,
|
||||
error,
|
||||
} = useFetchNotifiers();
|
||||
|
||||
const [searchInput, setSearchInput] = useState(defaultSearchInput);
|
||||
const [kinds, setKinds] = useState(defaultKinds);
|
||||
|
||||
useSetQueryParams({
|
||||
kinds: kinds.join("&"),
|
||||
search: searchInput,
|
||||
});
|
||||
|
||||
const location = useLocation();
|
||||
const pageLoaded = !isLoading && !error && !!notifiers?.length;
|
||||
const savedScrollTop = localStorage.getItem("scrollTop");
|
||||
|
||||
useEffect(() => {
|
||||
if (!pageLoaded) return;
|
||||
if (location.hash) {
|
||||
const target = document.querySelector(location.hash);
|
||||
if (target) {
|
||||
let parent = target.closest("details");
|
||||
while (parent) {
|
||||
parent.open = true;
|
||||
if (!parent?.parentElement) return;
|
||||
parent = parent.parentElement.closest("details");
|
||||
}
|
||||
target.scrollIntoView();
|
||||
}
|
||||
} else {
|
||||
if (savedScrollTop) {
|
||||
window.scrollTo(0, parseInt(savedScrollTop));
|
||||
}
|
||||
const handleBeforeUnload = () => {
|
||||
localStorage.setItem("scrollTop", (window.scrollY || 0).toString());
|
||||
};
|
||||
window.addEventListener("beforeunload", handleBeforeUnload);
|
||||
return () => {
|
||||
window.removeEventListener("beforeunload", handleBeforeUnload);
|
||||
};
|
||||
}
|
||||
}, [location, savedScrollTop, pageLoaded]);
|
||||
|
||||
const handleChangeSearch = (input: string) => {
|
||||
if (!input) {
|
||||
setSearchInput("");
|
||||
} else {
|
||||
setSearchInput(input);
|
||||
}
|
||||
};
|
||||
|
||||
const allKinds: Set<string> = new Set();
|
||||
const filteredNotifiers: APINotifier[] = [];
|
||||
|
||||
notifiers.forEach((notifier) => {
|
||||
const filteredTargets: APITarget[] = [];
|
||||
const targets = notifier.targets || [];
|
||||
targets.forEach((target) => {
|
||||
allKinds.add(notifier.kind);
|
||||
if (kinds?.length && !kinds.includes(notifier.kind)) return;
|
||||
if (
|
||||
searchInput &&
|
||||
!target.address.toLowerCase().includes(searchInput.toLowerCase()) &&
|
||||
!notifier.kind.toLowerCase().includes(searchInput.toLowerCase())
|
||||
)
|
||||
return;
|
||||
filteredTargets.push(target);
|
||||
});
|
||||
if (filteredTargets.length) {
|
||||
const n = Object.assign({}, notifier);
|
||||
n.targets = filteredTargets;
|
||||
filteredNotifiers.push(n);
|
||||
}
|
||||
});
|
||||
|
||||
const handleChangeKinds = (title: string) => {
|
||||
setKinds(getChanges(title, kinds));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="vm-explore-alerts">
|
||||
<NotifiersHeader
|
||||
kinds={kinds}
|
||||
allKinds={Array.from(allKinds)}
|
||||
onChangeKinds={handleChangeKinds}
|
||||
onChangeSearch={debounce(handleChangeSearch, 500)}
|
||||
/>
|
||||
{(isLoading && <Spinner />) || (error && <Alert variant="error">{error}</Alert>) || (
|
||||
!filteredNotifiers.length && <Alert variant="info">No notifiers found!</Alert>
|
||||
) || (
|
||||
<div className="vm-explore-alerts-body">
|
||||
{filteredNotifiers.map((notifier) => (
|
||||
<div
|
||||
key={notifier.kind}
|
||||
className="vm-explore-alert-group vm-block vm-block_empty-padding"
|
||||
>
|
||||
<Accordion
|
||||
key={`notifier-${notifier.kind}`}
|
||||
id={`notifier-${notifier.kind}`}
|
||||
title={<NotifierHeader notifier={notifier} />}
|
||||
>
|
||||
<div className="vm-explore-alerts-items">
|
||||
{notifier.targets.map((target) => (
|
||||
<Target
|
||||
key={`target-${target.address}`}
|
||||
target={target}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Accordion>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExploreNotifiers;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user