mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2025-03-11 15:34:56 +00:00
Merge branch 'public-single-node' into pmm-6401-read-prometheus-data-files
This commit is contained in:
commit
a39140baef
296 changed files with 8482 additions and 2780 deletions
2
.github/workflows/check-licenses.yml
vendored
2
.github/workflows/check-licenses.yml
vendored
|
@ -17,7 +17,7 @@ jobs:
|
|||
- name: Setup Go
|
||||
uses: actions/setup-go@main
|
||||
with:
|
||||
go-version: 1.19.5
|
||||
go-version: 1.20.0
|
||||
id: go
|
||||
- name: Code checkout
|
||||
uses: actions/checkout@master
|
||||
|
|
4
.github/workflows/codeql-analysis-js.yml
vendored
4
.github/workflows/codeql-analysis-js.yml
vendored
|
@ -13,6 +13,10 @@ on:
|
|||
schedule:
|
||||
- cron: "30 18 * * 2"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
|
|
8
.github/workflows/codeql-analysis.yml
vendored
8
.github/workflows/codeql-analysis.yml
vendored
|
@ -15,6 +15,7 @@ on:
|
|||
push:
|
||||
branches: [master, cluster]
|
||||
paths-ignore:
|
||||
- "docs/**"
|
||||
- "**.md"
|
||||
- "**.txt"
|
||||
- "**.js"
|
||||
|
@ -22,12 +23,17 @@ on:
|
|||
# The branches below must be a subset of the branches above
|
||||
branches: [master, cluster]
|
||||
paths-ignore:
|
||||
- "docs/**"
|
||||
- "**.md"
|
||||
- "**.txt"
|
||||
- "**.js"
|
||||
schedule:
|
||||
- cron: "30 18 * * 2"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
|
@ -51,7 +57,7 @@ jobs:
|
|||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.19.5
|
||||
go-version: 1.20.0
|
||||
check-latest: true
|
||||
cache: true
|
||||
if: ${{ matrix.language == 'go' }}
|
||||
|
|
66
.github/workflows/main-test.yml
vendored
66
.github/workflows/main-test.yml
vendored
|
@ -1,66 +0,0 @@
|
|||
name: main - test
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- cluster
|
||||
paths-ignore:
|
||||
- "docs/**"
|
||||
- "**.md"
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- cluster
|
||||
paths-ignore:
|
||||
- "docs/**"
|
||||
- "**.md"
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Code checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.19.5
|
||||
check-latest: true
|
||||
cache: true
|
||||
|
||||
- name: Dependencies
|
||||
run: |
|
||||
make install-golangci-lint
|
||||
make check-all
|
||||
git diff --exit-code
|
||||
|
||||
test:
|
||||
needs: lint
|
||||
strategy:
|
||||
matrix:
|
||||
scenario: ["test-full", "test-pure", "test-full-386"]
|
||||
name: test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Code checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.19.5
|
||||
check-latest: true
|
||||
cache: true
|
||||
|
||||
- name: run tests
|
||||
run: |
|
||||
make ${{ matrix.scenario}}
|
||||
|
||||
- name: Publish coverage
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
file: ./coverage.txt
|
85
.github/workflows/main.yml
vendored
85
.github/workflows/main.yml
vendored
|
@ -1,30 +1,95 @@
|
|||
name: main
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["main - test"]
|
||||
types:
|
||||
- completed
|
||||
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- cluster
|
||||
paths-ignore:
|
||||
- "docs/**"
|
||||
- "**.md"
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- cluster
|
||||
paths-ignore:
|
||||
- "docs/**"
|
||||
- "**.md"
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
lint:
|
||||
name: lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Code checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.workflow_run.head_branch }}
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.19.5
|
||||
go-version: 1.20.0
|
||||
check-latest: true
|
||||
cache: true
|
||||
|
||||
- name: Dependencies
|
||||
run: |
|
||||
make install-golangci-lint
|
||||
make check-all
|
||||
git diff --exit-code
|
||||
|
||||
test:
|
||||
needs: lint
|
||||
strategy:
|
||||
matrix:
|
||||
scenario: ["test-full", "test-pure", "test-full-386"]
|
||||
name: test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Code checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.20.0
|
||||
check-latest: true
|
||||
cache: true
|
||||
|
||||
- name: run tests
|
||||
run: |
|
||||
make ${{ matrix.scenario}}
|
||||
|
||||
- name: Publish coverage
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
file: ./coverage.txt
|
||||
|
||||
build:
|
||||
needs: test
|
||||
name: build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Code checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Go
|
||||
id: go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.20.0
|
||||
check-latest: true
|
||||
cache: true
|
||||
|
||||
- uses: actions/cache@v3
|
||||
with:
|
||||
path: gocache-for-docker
|
||||
key: gocache-docker-${{ runner.os }}-${{ steps.go.outputs.go-version }}-${{ hashFiles('go.mod') }}
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
make victoria-metrics-crossbuild
|
||||
|
|
31
.github/workflows/nightly-build.yml
vendored
31
.github/workflows/nightly-build.yml
vendored
|
@ -12,28 +12,37 @@ jobs:
|
|||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
name: Login to Docker Hub
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
-
|
||||
name: Setup Go
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@main
|
||||
with:
|
||||
go-version: 1.19.5
|
||||
go-version: 1.20.0
|
||||
id: go
|
||||
-
|
||||
name: Setup docker scan
|
||||
|
||||
- name: Setup docker scan
|
||||
run: |
|
||||
mkdir -p ~/.docker/cli-plugins && \
|
||||
curl https://github.com/docker/scan-cli-plugin/releases/latest/download/docker-scan_linux_amd64 -L -s -S -o ~/.docker/cli-plugins/docker-scan &&\
|
||||
chmod +x ~/.docker/cli-plugins/docker-scan
|
||||
-
|
||||
name: Code checkout
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Code checkout
|
||||
uses: actions/checkout@master
|
||||
-
|
||||
name: Publish
|
||||
|
||||
- uses: actions/cache@v3
|
||||
with:
|
||||
path: gocache-for-docker
|
||||
key: gocache-docker-${{ runner.os }}-${{ steps.go.outputs.go-version }}-${{ hashFiles('go.mod') }}
|
||||
|
||||
- name: build & publish
|
||||
run: |
|
||||
docker scan --login --token "$SNYK_TOKEN" --accept-license
|
||||
LATEST_TAG=nightly PKG_TAG=nightly make publish
|
||||
env:
|
||||
SNYK_TOKEN: ${{ secrets.SNYK_AUTH_TOKEN }}
|
||||
|
|
2
Makefile
2
Makefile
|
@ -380,7 +380,7 @@ golangci-lint: install-golangci-lint
|
|||
golangci-lint run
|
||||
|
||||
install-golangci-lint:
|
||||
which golangci-lint || curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(shell go env GOPATH)/bin v1.50.1
|
||||
which golangci-lint || curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(shell go env GOPATH)/bin v1.51.1
|
||||
|
||||
govulncheck: install-govulncheck
|
||||
govulncheck ./...
|
||||
|
|
|
@ -324,7 +324,7 @@ Extra labels can be added to metrics collected by `vmagent` via the following me
|
|||
## Automatically generated metrics
|
||||
|
||||
`vmagent` automatically generates the following metrics per each scrape of every [Prometheus-compatible target](#how-to-collect-metrics-in-prometheus-format)
|
||||
and attaches target-specific `instance` and `job` labels to these metrics:
|
||||
and attaches `instance`, `job` and other target-specific labels to these metrics:
|
||||
|
||||
* `up` - this metric exposes `1` value on successful scrape and `0` value on unsuccessful scrape. This allows monitoring
|
||||
failing scrapes with the following [MetricsQL query](https://docs.victoriametrics.com/MetricsQL.html):
|
||||
|
|
|
@ -907,10 +907,6 @@ The shortlist of configuration flags is the following:
|
|||
Address to listen for http connections. See also -httpListenAddr.useProxyProtocol (default ":8880")
|
||||
-httpListenAddr.useProxyProtocol
|
||||
Whether to use proxy protocol for connections accepted at -httpListenAddr . See https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt
|
||||
-insert.maxQueueDuration duration
|
||||
The maximum duration to wait in the queue when -maxConcurrentInserts concurrent insert requests are executed (default 1m0s)
|
||||
-internStringMaxLen int
|
||||
The maximum length for strings to intern. Lower limit may save memory at the cost of higher CPU usage. See https://en.wikipedia.org/wiki/String_interning (default 300)
|
||||
-loggerDisableTimestamps
|
||||
Whether to disable writing timestamps in logs
|
||||
-loggerErrorsPerSecondLimit int
|
||||
|
@ -927,13 +923,6 @@ The shortlist of configuration flags is the following:
|
|||
Timezone to use for timestamps in logs. Timezone must be a valid IANA Time Zone. For example: America/New_York, Europe/Berlin, Etc/GMT+3 or Local (default "UTC")
|
||||
-loggerWarnsPerSecondLimit int
|
||||
Per-second limit on the number of WARN messages. If more than the given number of warns are emitted per second, then the remaining warns are suppressed. Zero values disable the rate limit
|
||||
-maxConcurrentInserts int
|
||||
The maximum number of concurrent insert requests. Default value should work for most cases, since it minimizes the memory usage. The default value can be increased when clients send data over slow networks. See also -insert.maxQueueDuration (default 8)
|
||||
-memory.allowedBytes size
|
||||
Allowed size of system memory VictoriaMetrics caches may occupy. This option overrides -memory.allowedPercent if set to a non-zero value. Too low a value may increase the cache miss rate usually resulting in higher CPU and disk IO usage. Too high a value may evict too much data from OS page cache resulting in higher disk IO usage
|
||||
Supports the following optional suffixes for size values: KB, MB, GB, TB, KiB, MiB, GiB, TiB (default 0)
|
||||
-memory.allowedPercent float
|
||||
Allowed percent of system memory VictoriaMetrics caches may occupy. See also -memory.allowedBytes. Too low a value may increase cache miss rate usually resulting in higher CPU and disk IO usage. Too high a value may evict too much data from OS page cache which will result in higher disk IO usage (default 60)
|
||||
-metricsAuthKey string
|
||||
Auth key for /metrics endpoint. It must be passed via authKey query arg. It overrides httpAuth.* settings
|
||||
-notifier.basicAuth.password array
|
||||
|
@ -1023,7 +1012,7 @@ The shortlist of configuration flags is the following:
|
|||
-remoteRead.headers string
|
||||
Optional HTTP headers to send with each request to the corresponding -remoteRead.url. For example, -remoteRead.headers='My-Auth:foobar' would send 'My-Auth: foobar' HTTP header with every request to the corresponding -remoteRead.url. Multiple headers must be delimited by '^^': -remoteRead.headers='header1:value1^^header2:value2'
|
||||
-remoteRead.ignoreRestoreErrors
|
||||
Whether to ignore errors from remote storage when restoring alerts state on startup. (default true)
|
||||
Whether to ignore errors from remote storage when restoring alerts state on startup. DEPRECATED - this flag has no effect and will be removed in the next releases. (default true)
|
||||
-remoteRead.lookback duration
|
||||
Lookback defines how far to look into past for alerts timeseries. For example, if lookback=1h then range from now() to now()-1h will be scanned. (default 1h0m0s)
|
||||
-remoteRead.oauth2.clientID string
|
||||
|
@ -1101,7 +1090,7 @@ The shortlist of configuration flags is the following:
|
|||
-replay.disableProgressBar
|
||||
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.
|
||||
-replay.maxDatapointsPerQuery int
|
||||
Max number of data points expected in one request. The higher the value, the less requests will be made during replay. (default 1000)
|
||||
Max number of data points expected in one request. It affects the max time range for every `/query_range` request during the replay. The higher the value, the less requests will be made during replay. (default 1000)
|
||||
-replay.ruleRetryAttempts int
|
||||
Defines how many retries to make before giving up on rule if request for it returns an error. (default 5)
|
||||
-replay.rulesDelay duration
|
||||
|
|
|
@ -421,7 +421,9 @@ func (ar *AlertingRule) UpdateWith(r Rule) error {
|
|||
ar.Labels = nr.Labels
|
||||
ar.Annotations = nr.Annotations
|
||||
ar.EvalInterval = nr.EvalInterval
|
||||
ar.Debug = nr.Debug
|
||||
ar.q = nr.q
|
||||
ar.state = nr.state
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -498,6 +500,7 @@ func (ar *AlertingRule) ToAPI() APIRule {
|
|||
LastSamples: lastState.samples,
|
||||
MaxUpdates: ar.state.size(),
|
||||
Updates: ar.state.getAll(),
|
||||
Debug: ar.Debug,
|
||||
|
||||
// encode as strings to avoid rounding in JSON
|
||||
ID: fmt.Sprintf("%d", ar.ID()),
|
||||
|
@ -604,54 +607,59 @@ func alertForToTimeSeries(a *notifier.Alert, timestamp int64) prompbmarshal.Time
|
|||
return newTimeSeries([]float64{float64(a.ActiveAt.Unix())}, []int64{timestamp}, labels)
|
||||
}
|
||||
|
||||
// Restore restores the state of active alerts basing on previously written time series.
|
||||
// Restore restores only ActiveAt field. Field State will be always Pending and supposed
|
||||
// to be updated on next Exec, as well as Value field.
|
||||
// Only rules with For > 0 will be restored.
|
||||
func (ar *AlertingRule) Restore(ctx context.Context, q datasource.Querier, lookback time.Duration, labels map[string]string) error {
|
||||
if q == nil {
|
||||
return fmt.Errorf("querier is nil")
|
||||
// Restore restores the value of ActiveAt field for active alerts,
|
||||
// based on previously written time series `alertForStateMetricName`.
|
||||
// Only rules with For > 0 can be restored.
|
||||
func (ar *AlertingRule) Restore(ctx context.Context, q datasource.Querier, ts time.Time, lookback time.Duration) error {
|
||||
if ar.For < 1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
ts := time.Now()
|
||||
qFn := func(query string) ([]datasource.Metric, error) {
|
||||
res, _, err := ar.q.Query(ctx, query, ts)
|
||||
return res, err
|
||||
ar.alertsMu.Lock()
|
||||
defer ar.alertsMu.Unlock()
|
||||
|
||||
if len(ar.alerts) < 1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// account for external labels in filter
|
||||
var labelsFilter string
|
||||
for k, v := range labels {
|
||||
labelsFilter += fmt.Sprintf(",%s=%q", k, v)
|
||||
}
|
||||
|
||||
expr := fmt.Sprintf("last_over_time(%s{alertname=%q%s}[%ds])",
|
||||
alertForStateMetricName, ar.Name, labelsFilter, int(lookback.Seconds()))
|
||||
qMetrics, _, err := q.Query(ctx, expr, ts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, m := range qMetrics {
|
||||
ls := &labelSet{
|
||||
origin: make(map[string]string, len(m.Labels)),
|
||||
processed: make(map[string]string, len(m.Labels)),
|
||||
for _, a := range ar.alerts {
|
||||
if a.Restored || a.State != notifier.StatePending {
|
||||
continue
|
||||
}
|
||||
for _, l := range m.Labels {
|
||||
if l.Name == "__name__" {
|
||||
continue
|
||||
}
|
||||
ls.origin[l.Name] = l.Value
|
||||
ls.processed[l.Name] = l.Value
|
||||
|
||||
var labelsFilter []string
|
||||
for k, v := range a.Labels {
|
||||
labelsFilter = append(labelsFilter, fmt.Sprintf("%s=%q", k, v))
|
||||
}
|
||||
a, err := ar.newAlert(m, ls, time.Unix(int64(m.Values[0]), 0), qFn)
|
||||
sort.Strings(labelsFilter)
|
||||
expr := fmt.Sprintf("last_over_time(%s{%s}[%ds])",
|
||||
alertForStateMetricName, strings.Join(labelsFilter, ","), int(lookback.Seconds()))
|
||||
|
||||
ar.logDebugf(ts, nil, "restoring alert state via query %q", expr)
|
||||
|
||||
qMetrics, _, err := q.Query(ctx, expr, ts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create alert: %w", err)
|
||||
return err
|
||||
}
|
||||
a.ID = hash(ls.processed)
|
||||
a.State = notifier.StatePending
|
||||
|
||||
if len(qMetrics) < 1 {
|
||||
ar.logDebugf(ts, nil, "no response was received from restore query")
|
||||
continue
|
||||
}
|
||||
|
||||
// only one series expected in response
|
||||
m := qMetrics[0]
|
||||
// __name__ supposed to be alertForStateMetricName
|
||||
m.DelLabel("__name__")
|
||||
|
||||
// we assume that restore query contains all label matchers,
|
||||
// so all received labels will match anyway if their number is equal.
|
||||
if len(m.Labels) != len(a.Labels) {
|
||||
ar.logDebugf(ts, nil, "state restore query returned not expected label-set %v", m.Labels)
|
||||
continue
|
||||
}
|
||||
a.ActiveAt = time.Unix(int64(m.Values[0]), 0)
|
||||
a.Restored = true
|
||||
ar.alerts[a.ID] = a
|
||||
logger.Infof("alert %q (%d) restored to state at %v", a.Name, a.ID, a.ActiveAt)
|
||||
}
|
||||
return nil
|
||||
|
|
|
@ -6,12 +6,15 @@ import (
|
|||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/config"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/datasource"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/notifier"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promutils"
|
||||
)
|
||||
|
||||
func TestAlertingRule_ToTimeSeries(t *testing.T) {
|
||||
|
@ -502,118 +505,156 @@ func TestAlertingRule_ExecRange(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestAlertingRule_Restore(t *testing.T) {
|
||||
testCases := []struct {
|
||||
rule *AlertingRule
|
||||
metrics []datasource.Metric
|
||||
expAlerts map[uint64]*notifier.Alert
|
||||
}{
|
||||
{
|
||||
newTestRuleWithLabels("no extra labels"),
|
||||
[]datasource.Metric{
|
||||
metricWithValueAndLabels(t, float64(time.Now().Truncate(time.Hour).Unix()),
|
||||
"__name__", alertForStateMetricName,
|
||||
),
|
||||
},
|
||||
map[uint64]*notifier.Alert{
|
||||
hash(nil): {State: notifier.StatePending,
|
||||
ActiveAt: time.Now().Truncate(time.Hour)},
|
||||
},
|
||||
},
|
||||
{
|
||||
newTestRuleWithLabels("metric labels"),
|
||||
[]datasource.Metric{
|
||||
metricWithValueAndLabels(t, float64(time.Now().Truncate(time.Hour).Unix()),
|
||||
"__name__", alertForStateMetricName,
|
||||
alertNameLabel, "metric labels",
|
||||
alertGroupNameLabel, "groupID",
|
||||
"foo", "bar",
|
||||
"namespace", "baz",
|
||||
),
|
||||
},
|
||||
map[uint64]*notifier.Alert{
|
||||
hash(map[string]string{
|
||||
alertNameLabel: "metric labels",
|
||||
alertGroupNameLabel: "groupID",
|
||||
"foo": "bar",
|
||||
"namespace": "baz",
|
||||
}): {State: notifier.StatePending,
|
||||
ActiveAt: time.Now().Truncate(time.Hour)},
|
||||
},
|
||||
},
|
||||
{
|
||||
newTestRuleWithLabels("rule labels", "source", "vm"),
|
||||
[]datasource.Metric{
|
||||
metricWithValueAndLabels(t, float64(time.Now().Truncate(time.Hour).Unix()),
|
||||
"__name__", alertForStateMetricName,
|
||||
"foo", "bar",
|
||||
"namespace", "baz",
|
||||
// extra labels set by rule
|
||||
"source", "vm",
|
||||
),
|
||||
},
|
||||
map[uint64]*notifier.Alert{
|
||||
hash(map[string]string{
|
||||
"foo": "bar",
|
||||
"namespace": "baz",
|
||||
"source": "vm",
|
||||
}): {State: notifier.StatePending,
|
||||
ActiveAt: time.Now().Truncate(time.Hour)},
|
||||
},
|
||||
},
|
||||
{
|
||||
newTestRuleWithLabels("multiple alerts"),
|
||||
[]datasource.Metric{
|
||||
metricWithValueAndLabels(t, float64(time.Now().Truncate(time.Hour).Unix()),
|
||||
"__name__", alertForStateMetricName,
|
||||
"host", "localhost-1",
|
||||
),
|
||||
metricWithValueAndLabels(t, float64(time.Now().Truncate(2*time.Hour).Unix()),
|
||||
"__name__", alertForStateMetricName,
|
||||
"host", "localhost-2",
|
||||
),
|
||||
metricWithValueAndLabels(t, float64(time.Now().Truncate(3*time.Hour).Unix()),
|
||||
"__name__", alertForStateMetricName,
|
||||
"host", "localhost-3",
|
||||
),
|
||||
},
|
||||
map[uint64]*notifier.Alert{
|
||||
hash(map[string]string{"host": "localhost-1"}): {State: notifier.StatePending,
|
||||
ActiveAt: time.Now().Truncate(time.Hour)},
|
||||
hash(map[string]string{"host": "localhost-2"}): {State: notifier.StatePending,
|
||||
ActiveAt: time.Now().Truncate(2 * time.Hour)},
|
||||
hash(map[string]string{"host": "localhost-3"}): {State: notifier.StatePending,
|
||||
ActiveAt: time.Now().Truncate(3 * time.Hour)},
|
||||
},
|
||||
},
|
||||
func TestGroup_Restore(t *testing.T) {
|
||||
defaultTS := time.Now()
|
||||
fqr := &fakeQuerierWithRegistry{}
|
||||
fn := func(rules []config.Rule, expAlerts map[uint64]*notifier.Alert) {
|
||||
t.Helper()
|
||||
defer fqr.reset()
|
||||
|
||||
for _, r := range rules {
|
||||
fqr.set(r.Expr, metricWithValueAndLabels(t, 0, "__name__", r.Alert))
|
||||
}
|
||||
|
||||
fg := newGroup(config.Group{Name: "TestRestore", Rules: rules}, fqr, time.Second, nil)
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
nts := func() []notifier.Notifier { return []notifier.Notifier{&fakeNotifier{}} }
|
||||
fg.start(context.Background(), nts, nil, fqr)
|
||||
wg.Done()
|
||||
}()
|
||||
fg.close()
|
||||
wg.Wait()
|
||||
|
||||
gotAlerts := make(map[uint64]*notifier.Alert)
|
||||
for _, rs := range fg.Rules {
|
||||
alerts := rs.(*AlertingRule).alerts
|
||||
for k, v := range alerts {
|
||||
if !v.Restored {
|
||||
// set not restored alerts to predictable timestamp
|
||||
v.ActiveAt = defaultTS
|
||||
}
|
||||
gotAlerts[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
if len(gotAlerts) != len(expAlerts) {
|
||||
t.Fatalf("expected %d alerts; got %d", len(expAlerts), len(gotAlerts))
|
||||
}
|
||||
for key, exp := range expAlerts {
|
||||
got, ok := gotAlerts[key]
|
||||
if !ok {
|
||||
t.Fatalf("expected to have key %d", key)
|
||||
}
|
||||
if got.State != notifier.StatePending {
|
||||
t.Fatalf("expected state %d; got %d", notifier.StatePending, got.State)
|
||||
}
|
||||
if got.ActiveAt != exp.ActiveAt {
|
||||
t.Fatalf("expected ActiveAt %v; got %v", exp.ActiveAt, got.ActiveAt)
|
||||
}
|
||||
}
|
||||
}
|
||||
fakeGroup := Group{Name: "TestRule_Exec"}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.rule.Name, func(t *testing.T) {
|
||||
fq := &fakeQuerier{}
|
||||
tc.rule.GroupID = fakeGroup.ID()
|
||||
tc.rule.q = fq
|
||||
fq.add(tc.metrics...)
|
||||
if err := tc.rule.Restore(context.TODO(), fq, time.Hour, nil); err != nil {
|
||||
t.Fatalf("unexpected err: %s", err)
|
||||
}
|
||||
if len(tc.rule.alerts) != len(tc.expAlerts) {
|
||||
t.Fatalf("expected %d alerts; got %d", len(tc.expAlerts), len(tc.rule.alerts))
|
||||
}
|
||||
for key, exp := range tc.expAlerts {
|
||||
got, ok := tc.rule.alerts[key]
|
||||
if !ok {
|
||||
t.Fatalf("expected to have key %d", key)
|
||||
}
|
||||
if got.State != exp.State {
|
||||
t.Fatalf("expected state %d; got %d", exp.State, got.State)
|
||||
}
|
||||
if got.ActiveAt != exp.ActiveAt {
|
||||
t.Fatalf("expected ActiveAt %v; got %v", exp.ActiveAt, got.ActiveAt)
|
||||
}
|
||||
}
|
||||
|
||||
stateMetric := func(name string, value time.Time, labels ...string) datasource.Metric {
|
||||
labels = append(labels, "__name__", alertForStateMetricName)
|
||||
labels = append(labels, alertNameLabel, name)
|
||||
labels = append(labels, alertGroupNameLabel, "TestRestore")
|
||||
return metricWithValueAndLabels(t, float64(value.Unix()), labels...)
|
||||
}
|
||||
|
||||
// one active alert, no previous state
|
||||
fn(
|
||||
[]config.Rule{{Alert: "foo", Expr: "foo", For: promutils.NewDuration(time.Second)}},
|
||||
map[uint64]*notifier.Alert{
|
||||
hash(map[string]string{alertNameLabel: "foo", alertGroupNameLabel: "TestRestore"}): {
|
||||
ActiveAt: defaultTS,
|
||||
},
|
||||
})
|
||||
fqr.reset()
|
||||
|
||||
// one active alert with state restore
|
||||
ts := time.Now().Truncate(time.Hour)
|
||||
fqr.set(`last_over_time(ALERTS_FOR_STATE{alertgroup="TestRestore",alertname="foo"}[3600s])`,
|
||||
stateMetric("foo", ts))
|
||||
fn(
|
||||
[]config.Rule{{Alert: "foo", Expr: "foo", For: promutils.NewDuration(time.Second)}},
|
||||
map[uint64]*notifier.Alert{
|
||||
hash(map[string]string{alertNameLabel: "foo", alertGroupNameLabel: "TestRestore"}): {
|
||||
ActiveAt: ts},
|
||||
})
|
||||
|
||||
// two rules, two active alerts, one with state restored
|
||||
ts = time.Now().Truncate(time.Hour)
|
||||
fqr.set(`last_over_time(ALERTS_FOR_STATE{alertgroup="TestRestore",alertname="bar"}[3600s])`,
|
||||
stateMetric("foo", ts))
|
||||
fn(
|
||||
[]config.Rule{
|
||||
{Alert: "foo", Expr: "foo", For: promutils.NewDuration(time.Second)},
|
||||
{Alert: "bar", Expr: "bar", For: promutils.NewDuration(time.Second)},
|
||||
},
|
||||
map[uint64]*notifier.Alert{
|
||||
hash(map[string]string{alertNameLabel: "foo", alertGroupNameLabel: "TestRestore"}): {
|
||||
ActiveAt: defaultTS,
|
||||
},
|
||||
hash(map[string]string{alertNameLabel: "bar", alertGroupNameLabel: "TestRestore"}): {
|
||||
ActiveAt: ts},
|
||||
})
|
||||
|
||||
// two rules, two active alerts, two with state restored
|
||||
ts = time.Now().Truncate(time.Hour)
|
||||
fqr.set(`last_over_time(ALERTS_FOR_STATE{alertgroup="TestRestore",alertname="foo"}[3600s])`,
|
||||
stateMetric("foo", ts))
|
||||
fqr.set(`last_over_time(ALERTS_FOR_STATE{alertgroup="TestRestore",alertname="bar"}[3600s])`,
|
||||
stateMetric("bar", ts))
|
||||
fn(
|
||||
[]config.Rule{
|
||||
{Alert: "foo", Expr: "foo", For: promutils.NewDuration(time.Second)},
|
||||
{Alert: "bar", Expr: "bar", For: promutils.NewDuration(time.Second)},
|
||||
},
|
||||
map[uint64]*notifier.Alert{
|
||||
hash(map[string]string{alertNameLabel: "foo", alertGroupNameLabel: "TestRestore"}): {
|
||||
ActiveAt: ts,
|
||||
},
|
||||
hash(map[string]string{alertNameLabel: "bar", alertGroupNameLabel: "TestRestore"}): {
|
||||
ActiveAt: ts},
|
||||
})
|
||||
|
||||
// one active alert but wrong state restore
|
||||
ts = time.Now().Truncate(time.Hour)
|
||||
fqr.set(`last_over_time(ALERTS_FOR_STATE{alertname="bar",alertgroup="TestRestore"}[3600s])`,
|
||||
stateMetric("wrong alert", ts))
|
||||
fn(
|
||||
[]config.Rule{{Alert: "foo", Expr: "foo", For: promutils.NewDuration(time.Second)}},
|
||||
map[uint64]*notifier.Alert{
|
||||
hash(map[string]string{alertNameLabel: "foo", alertGroupNameLabel: "TestRestore"}): {
|
||||
ActiveAt: defaultTS,
|
||||
},
|
||||
})
|
||||
|
||||
// one active alert with labels
|
||||
ts = time.Now().Truncate(time.Hour)
|
||||
fqr.set(`last_over_time(ALERTS_FOR_STATE{alertgroup="TestRestore",alertname="foo",env="dev"}[3600s])`,
|
||||
stateMetric("foo", ts, "env", "dev"))
|
||||
fn(
|
||||
[]config.Rule{{Alert: "foo", Expr: "foo", Labels: map[string]string{"env": "dev"}, For: promutils.NewDuration(time.Second)}},
|
||||
map[uint64]*notifier.Alert{
|
||||
hash(map[string]string{alertNameLabel: "foo", alertGroupNameLabel: "TestRestore", "env": "dev"}): {
|
||||
ActiveAt: ts,
|
||||
},
|
||||
})
|
||||
|
||||
// one active alert with restore labels missmatch
|
||||
ts = time.Now().Truncate(time.Hour)
|
||||
fqr.set(`last_over_time(ALERTS_FOR_STATE{alertgroup="TestRestore",alertname="foo",env="dev"}[3600s])`,
|
||||
stateMetric("foo", ts, "env", "dev", "team", "foo"))
|
||||
fn(
|
||||
[]config.Rule{{Alert: "foo", Expr: "foo", Labels: map[string]string{"env": "dev"}, For: promutils.NewDuration(time.Second)}},
|
||||
map[uint64]*notifier.Alert{
|
||||
hash(map[string]string{alertNameLabel: "foo", alertGroupNameLabel: "TestRestore", "env": "dev"}): {
|
||||
ActiveAt: defaultTS,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlertingRule_Exec_Negative(t *testing.T) {
|
||||
|
|
|
@ -72,6 +72,15 @@ func (m *Metric) AddLabel(key, value string) {
|
|||
m.Labels = append(m.Labels, Label{Name: key, Value: value})
|
||||
}
|
||||
|
||||
// DelLabel deletes the given label from the label set
|
||||
func (m *Metric) DelLabel(key string) {
|
||||
for i, l := range m.Labels {
|
||||
if l.Name == key {
|
||||
m.Labels = append(m.Labels[:i], m.Labels[i+1:]...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Label returns the given label value.
|
||||
// If label is missing empty string will be returned
|
||||
func (m *Metric) Label(key string) string {
|
||||
|
|
|
@ -158,23 +158,23 @@ func (g *Group) ID() uint64 {
|
|||
}
|
||||
|
||||
// Restore restores alerts state for group rules
|
||||
func (g *Group) Restore(ctx context.Context, qb datasource.QuerierBuilder, lookback time.Duration, labels map[string]string) error {
|
||||
labels = mergeLabels(g.Name, "", labels, g.Labels)
|
||||
func (g *Group) Restore(ctx context.Context, qb datasource.QuerierBuilder, ts time.Time, lookback time.Duration) error {
|
||||
for _, rule := range g.Rules {
|
||||
rr, ok := rule.(*AlertingRule)
|
||||
ar, ok := rule.(*AlertingRule)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if rr.For < 1 {
|
||||
if ar.For < 1 {
|
||||
continue
|
||||
}
|
||||
// ignore QueryParams on purpose, because they could contain
|
||||
// query filters. This may affect the restore procedure.
|
||||
q := qb.BuildWithParams(datasource.QuerierParams{
|
||||
DataSourceType: g.Type.String(),
|
||||
Headers: g.Headers,
|
||||
DataSourceType: g.Type.String(),
|
||||
EvaluationInterval: g.Interval,
|
||||
QueryParams: g.Params,
|
||||
Headers: g.Headers,
|
||||
Debug: ar.Debug,
|
||||
})
|
||||
if err := rr.Restore(ctx, q, lookback, labels); err != nil {
|
||||
if err := ar.Restore(ctx, q, ts, lookback); err != nil {
|
||||
return fmt.Errorf("error while restoring rule %q: %w", rule, err)
|
||||
}
|
||||
}
|
||||
|
@ -251,7 +251,7 @@ func (g *Group) close() {
|
|||
|
||||
var skipRandSleepOnGroupStart bool
|
||||
|
||||
func (g *Group) start(ctx context.Context, nts func() []notifier.Notifier, rw *remotewrite.Client) {
|
||||
func (g *Group) start(ctx context.Context, nts func() []notifier.Notifier, rw *remotewrite.Client, rr datasource.QuerierBuilder) {
|
||||
defer func() { close(g.finishedCh) }()
|
||||
|
||||
e := &executor{
|
||||
|
@ -259,26 +259,6 @@ func (g *Group) start(ctx context.Context, nts func() []notifier.Notifier, rw *r
|
|||
notifiers: nts,
|
||||
previouslySentSeriesToRW: make(map[uint64]map[string][]prompbmarshal.Label)}
|
||||
|
||||
// Spread group rules evaluation over time in order to reduce load on VictoriaMetrics.
|
||||
if !skipRandSleepOnGroupStart {
|
||||
randSleep := uint64(float64(g.Interval) * (float64(g.ID()) / (1 << 64)))
|
||||
sleepOffset := uint64(time.Now().UnixNano()) % uint64(g.Interval)
|
||||
if randSleep < sleepOffset {
|
||||
randSleep += uint64(g.Interval)
|
||||
}
|
||||
randSleep -= sleepOffset
|
||||
sleepTimer := time.NewTimer(time.Duration(randSleep))
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
sleepTimer.Stop()
|
||||
return
|
||||
case <-g.doneCh:
|
||||
sleepTimer.Stop()
|
||||
return
|
||||
case <-sleepTimer.C:
|
||||
}
|
||||
}
|
||||
|
||||
evalTS := time.Now()
|
||||
|
||||
logger.Infof("group %q started; interval=%v; concurrency=%d", g.Name, g.Interval, g.Concurrency)
|
||||
|
@ -309,6 +289,16 @@ func (g *Group) start(ctx context.Context, nts func() []notifier.Notifier, rw *r
|
|||
|
||||
t := time.NewTicker(g.Interval)
|
||||
defer t.Stop()
|
||||
|
||||
// restore the rules state after the first evaluation
|
||||
// so only active alerts can be restored.
|
||||
if rr != nil {
|
||||
err := g.Restore(ctx, rr, evalTS, *remoteReadLookBack)
|
||||
if err != nil {
|
||||
logger.Errorf("error while restoring ruleState for group %q: %s", g.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
|
|
|
@ -209,7 +209,7 @@ func TestGroupStart(t *testing.T) {
|
|||
fs.add(m1)
|
||||
fs.add(m2)
|
||||
go func() {
|
||||
g.start(context.Background(), func() []notifier.Notifier { return []notifier.Notifier{fn} }, nil)
|
||||
g.start(context.Background(), func() []notifier.Notifier { return []notifier.Notifier{fn} }, nil, fs)
|
||||
close(finished)
|
||||
}()
|
||||
|
||||
|
|
|
@ -61,6 +61,49 @@ func (fq *fakeQuerier) Query(_ context.Context, _ string, _ time.Time) ([]dataso
|
|||
return cp, req, nil
|
||||
}
|
||||
|
||||
type fakeQuerierWithRegistry struct {
|
||||
sync.Mutex
|
||||
registry map[string][]datasource.Metric
|
||||
}
|
||||
|
||||
func (fqr *fakeQuerierWithRegistry) set(key string, metrics ...datasource.Metric) {
|
||||
fqr.Lock()
|
||||
if fqr.registry == nil {
|
||||
fqr.registry = make(map[string][]datasource.Metric)
|
||||
}
|
||||
fqr.registry[key] = metrics
|
||||
fqr.Unlock()
|
||||
}
|
||||
|
||||
func (fqr *fakeQuerierWithRegistry) reset() {
|
||||
fqr.Lock()
|
||||
fqr.registry = nil
|
||||
fqr.Unlock()
|
||||
}
|
||||
|
||||
func (fqr *fakeQuerierWithRegistry) BuildWithParams(_ datasource.QuerierParams) datasource.Querier {
|
||||
return fqr
|
||||
}
|
||||
|
||||
func (fqr *fakeQuerierWithRegistry) QueryRange(ctx context.Context, q string, _, _ time.Time) ([]datasource.Metric, error) {
|
||||
req, _, err := fqr.Query(ctx, q, time.Now())
|
||||
return req, err
|
||||
}
|
||||
|
||||
func (fqr *fakeQuerierWithRegistry) Query(_ context.Context, expr string, _ time.Time) ([]datasource.Metric, *http.Request, error) {
|
||||
fqr.Lock()
|
||||
defer fqr.Unlock()
|
||||
|
||||
req, _ := http.NewRequest(http.MethodPost, "foo.com", nil)
|
||||
metrics, ok := fqr.registry[expr]
|
||||
if !ok {
|
||||
return nil, req, nil
|
||||
}
|
||||
cp := make([]datasource.Metric, len(metrics))
|
||||
copy(cp, metrics)
|
||||
return cp, req, nil
|
||||
}
|
||||
|
||||
type fakeNotifier struct {
|
||||
sync.Mutex
|
||||
alerts []notifier.Alert
|
||||
|
|
|
@ -73,7 +73,7 @@ absolute path to all .tpl files in root.`)
|
|||
|
||||
remoteReadLookBack = flag.Duration("remoteRead.lookback", time.Hour, "Lookback defines how far to look into past for alerts timeseries."+
|
||||
" For example, if lookback=1h then range from now() to now()-1h will be scanned.")
|
||||
remoteReadIgnoreRestoreErrors = flag.Bool("remoteRead.ignoreRestoreErrors", true, "Whether to ignore errors from remote storage when restoring alerts state on startup.")
|
||||
remoteReadIgnoreRestoreErrors = flag.Bool("remoteRead.ignoreRestoreErrors", true, "Whether to ignore errors from remote storage when restoring alerts state on startup. DEPRECATED - this flag has no effect and will be removed in the next releases.")
|
||||
|
||||
disableAlertGroupLabel = flag.Bool("disableAlertgroupLabel", false, "Whether to disable adding group's Name as label to generated alerts and time series.")
|
||||
|
||||
|
@ -94,6 +94,10 @@ func main() {
|
|||
logger.Init()
|
||||
pushmetrics.Init()
|
||||
|
||||
if !*remoteReadIgnoreRestoreErrors {
|
||||
logger.Warnf("flag `remoteRead.ignoreRestoreErrors` is deprecated and will be removed in next releases.")
|
||||
}
|
||||
|
||||
err := templates.Load(*ruleTemplatesPath, true)
|
||||
if err != nil {
|
||||
logger.Fatalf("failed to parse %q: %s", *ruleTemplatesPath, err)
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"net/url"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/config"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/datasource"
|
||||
|
@ -82,24 +83,38 @@ func (m *manager) close() {
|
|||
m.wg.Wait()
|
||||
}
|
||||
|
||||
func (m *manager) startGroup(ctx context.Context, group *Group, restore bool) error {
|
||||
if restore && m.rr != nil {
|
||||
err := group.Restore(ctx, m.rr, *remoteReadLookBack, m.labels)
|
||||
if err != nil {
|
||||
if !*remoteReadIgnoreRestoreErrors {
|
||||
return fmt.Errorf("failed to restore ruleState for group %q: %w", group.Name, err)
|
||||
}
|
||||
logger.Errorf("error while restoring ruleState for group %q: %s", group.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *manager) startGroup(ctx context.Context, g *Group, restore bool) error {
|
||||
m.wg.Add(1)
|
||||
id := group.ID()
|
||||
id := g.ID()
|
||||
go func() {
|
||||
group.start(ctx, m.notifiers, m.rw)
|
||||
// Spread group rules evaluation over time in order to reduce load on VictoriaMetrics.
|
||||
if !skipRandSleepOnGroupStart {
|
||||
randSleep := uint64(float64(g.Interval) * (float64(g.ID()) / (1 << 64)))
|
||||
sleepOffset := uint64(time.Now().UnixNano()) % uint64(g.Interval)
|
||||
if randSleep < sleepOffset {
|
||||
randSleep += uint64(g.Interval)
|
||||
}
|
||||
randSleep -= sleepOffset
|
||||
sleepTimer := time.NewTimer(time.Duration(randSleep))
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
sleepTimer.Stop()
|
||||
return
|
||||
case <-g.doneCh:
|
||||
sleepTimer.Stop()
|
||||
return
|
||||
case <-sleepTimer.C:
|
||||
}
|
||||
}
|
||||
if restore {
|
||||
g.start(ctx, m.notifiers, m.rw, m.rr)
|
||||
} else {
|
||||
g.start(ctx, m.notifiers, m.rw, nil)
|
||||
}
|
||||
|
||||
m.wg.Done()
|
||||
}()
|
||||
m.groups[id] = group
|
||||
m.groups[id] = g
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@ var (
|
|||
"and processing need to wait for previous rule results to be persisted by remote storage before evaluating the next rule."+
|
||||
"Keep it equal or bigger than -remoteWrite.flushInterval.")
|
||||
replayMaxDatapoints = flag.Int("replay.maxDatapointsPerQuery", 1e3,
|
||||
"Max number of data points expected in one request. The higher the value, the less requests will be made during replay.")
|
||||
"Max number of data points expected in one request. It affects the max time range for every `/query_range` request during the replay. The higher the value, the less requests will be made during replay.")
|
||||
replayRuleRetryAttempts = flag.Int("replay.ruleRetryAttempts", 5,
|
||||
"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. "+
|
||||
|
|
|
@ -37,8 +37,6 @@ type ruleState struct {
|
|||
sync.RWMutex
|
||||
entries []ruleStateEntry
|
||||
cur int
|
||||
// disabled defines whether ruleState tracks ruleStateEntry
|
||||
disabled bool
|
||||
}
|
||||
|
||||
type ruleStateEntry struct {
|
||||
|
@ -61,7 +59,7 @@ type ruleStateEntry struct {
|
|||
|
||||
func newRuleState(size int) *ruleState {
|
||||
if size < 1 {
|
||||
return &ruleState{disabled: true}
|
||||
size = 1
|
||||
}
|
||||
return &ruleState{
|
||||
entries: make([]ruleStateEntry, size),
|
||||
|
@ -69,10 +67,6 @@ func newRuleState(size int) *ruleState {
|
|||
}
|
||||
|
||||
func (s *ruleState) getLast() ruleStateEntry {
|
||||
if s.disabled {
|
||||
return ruleStateEntry{}
|
||||
}
|
||||
|
||||
s.RLock()
|
||||
defer s.RUnlock()
|
||||
return s.entries[s.cur]
|
||||
|
@ -85,10 +79,6 @@ func (s *ruleState) size() int {
|
|||
}
|
||||
|
||||
func (s *ruleState) getAll() []ruleStateEntry {
|
||||
if s.disabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
entries := make([]ruleStateEntry, 0)
|
||||
|
||||
s.RLock()
|
||||
|
@ -111,10 +101,6 @@ func (s *ruleState) getAll() []ruleStateEntry {
|
|||
}
|
||||
|
||||
func (s *ruleState) add(e ruleStateEntry) {
|
||||
if s.disabled {
|
||||
return
|
||||
}
|
||||
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
|
||||
|
|
|
@ -14,13 +14,13 @@ func TestRule_stateDisabled(t *testing.T) {
|
|||
}
|
||||
|
||||
state.add(ruleStateEntry{at: time.Now()})
|
||||
if !e.at.IsZero() {
|
||||
t.Fatalf("expected entry to be zero")
|
||||
}
|
||||
state.add(ruleStateEntry{at: time.Now()})
|
||||
state.add(ruleStateEntry{at: time.Now()})
|
||||
|
||||
if len(state.getAll()) != 0 {
|
||||
if len(state.getAll()) != 1 {
|
||||
// state should store at least one update at any circumstances
|
||||
t.Fatalf("expected for state to have %d entries; got %d",
|
||||
0, len(state.getAll()),
|
||||
1, len(state.getAll()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,9 +40,9 @@
|
|||
for _, g := range groups {
|
||||
for _, r := range g.Rules {
|
||||
if r.LastError != "" {
|
||||
rNotOk[g.Name]++
|
||||
rNotOk[g.ID]++
|
||||
} else {
|
||||
rOk[g.Name]++
|
||||
rOk[g.ID]++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -50,11 +50,11 @@
|
|||
<a class="btn btn-primary" role="button" onclick="collapseAll()">Collapse All</a>
|
||||
<a class="btn btn-primary" role="button" onclick="expandAll()">Expand All</a>
|
||||
{% for _, g := range groups %}
|
||||
<div class="group-heading{% if rNotOk[g.Name] > 0 %} alert-danger{% endif %}" data-bs-target="rules-{%s g.ID %}">
|
||||
<div class="group-heading{% if rNotOk[g.ID] > 0 %} alert-danger{% endif %}" data-bs-target="rules-{%s g.ID %}">
|
||||
<span class="anchor" id="group-{%s g.ID %}"></span>
|
||||
<a href="#group-{%s g.ID %}">{%s g.Name %}{% if g.Type != "prometheus" %} ({%s g.Type %}){% endif %} (every {%f.0 g.Interval %}s)</a>
|
||||
{% if rNotOk[g.Name] > 0 %}<span class="badge bg-danger" title="Number of rules with status Error">{%d rNotOk[g.Name] %}</span> {% endif %}
|
||||
<span class="badge bg-success" title="Number of rules withs status Ok">{%d rOk[g.Name] %}</span>
|
||||
{% if rNotOk[g.ID] > 0 %}<span class="badge bg-danger" title="Number of rules with status Error">{%d rNotOk[g.ID] %}</span> {% endif %}
|
||||
<span class="badge bg-success" title="Number of rules withs status Ok">{%d rOk[g.ID] %}</span>
|
||||
<p class="fs-6 fw-lighter">{%s g.File %}</p>
|
||||
{% if len(g.Params) > 0 %}
|
||||
<div class="fs-6 fw-lighter">Extra params
|
||||
|
@ -427,6 +427,16 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container border-bottom p-2">
|
||||
<div class="row">
|
||||
<div class="col-2">
|
||||
Debug
|
||||
</div>
|
||||
<div class="col">
|
||||
{%v rule.Debug %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="container border-bottom p-2">
|
||||
<div class="row">
|
||||
|
|
|
@ -171,9 +171,9 @@ func StreamListGroups(qw422016 *qt422016.Writer, r *http.Request, groups []APIGr
|
|||
for _, g := range groups {
|
||||
for _, r := range g.Rules {
|
||||
if r.LastError != "" {
|
||||
rNotOk[g.Name]++
|
||||
rNotOk[g.ID]++
|
||||
} else {
|
||||
rOk[g.Name]++
|
||||
rOk[g.ID]++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -189,7 +189,7 @@ func StreamListGroups(qw422016 *qt422016.Writer, r *http.Request, groups []APIGr
|
|||
qw422016.N().S(`
|
||||
<div class="group-heading`)
|
||||
//line app/vmalert/web.qtpl:53
|
||||
if rNotOk[g.Name] > 0 {
|
||||
if rNotOk[g.ID] > 0 {
|
||||
//line app/vmalert/web.qtpl:53
|
||||
qw422016.N().S(` alert-danger`)
|
||||
//line app/vmalert/web.qtpl:53
|
||||
|
@ -230,11 +230,11 @@ func StreamListGroups(qw422016 *qt422016.Writer, r *http.Request, groups []APIGr
|
|||
qw422016.N().S(`s)</a>
|
||||
`)
|
||||
//line app/vmalert/web.qtpl:56
|
||||
if rNotOk[g.Name] > 0 {
|
||||
if rNotOk[g.ID] > 0 {
|
||||
//line app/vmalert/web.qtpl:56
|
||||
qw422016.N().S(`<span class="badge bg-danger" title="Number of rules with status Error">`)
|
||||
//line app/vmalert/web.qtpl:56
|
||||
qw422016.N().D(rNotOk[g.Name])
|
||||
qw422016.N().D(rNotOk[g.ID])
|
||||
//line app/vmalert/web.qtpl:56
|
||||
qw422016.N().S(`</span> `)
|
||||
//line app/vmalert/web.qtpl:56
|
||||
|
@ -243,7 +243,7 @@ func StreamListGroups(qw422016 *qt422016.Writer, r *http.Request, groups []APIGr
|
|||
qw422016.N().S(`
|
||||
<span class="badge bg-success" title="Number of rules withs status Ok">`)
|
||||
//line app/vmalert/web.qtpl:57
|
||||
qw422016.N().D(rOk[g.Name])
|
||||
qw422016.N().D(rOk[g.ID])
|
||||
//line app/vmalert/web.qtpl:57
|
||||
qw422016.N().S(`</span>
|
||||
<p class="fs-6 fw-lighter">`)
|
||||
|
@ -1313,10 +1313,24 @@ func StreamRuleDetails(qw422016 *qt422016.Writer, r *http.Request, rule APIRule)
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container border-bottom p-2">
|
||||
<div class="row">
|
||||
<div class="col-2">
|
||||
Debug
|
||||
</div>
|
||||
<div class="col">
|
||||
`)
|
||||
//line app/vmalert/web.qtpl:436
|
||||
qw422016.E().V(rule.Debug)
|
||||
//line app/vmalert/web.qtpl:436
|
||||
qw422016.N().S(`
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`)
|
||||
//line app/vmalert/web.qtpl:430
|
||||
//line app/vmalert/web.qtpl:440
|
||||
}
|
||||
//line app/vmalert/web.qtpl:430
|
||||
//line app/vmalert/web.qtpl:440
|
||||
qw422016.N().S(`
|
||||
<div class="container border-bottom p-2">
|
||||
<div class="row">
|
||||
|
@ -1325,17 +1339,17 @@ func StreamRuleDetails(qw422016 *qt422016.Writer, r *http.Request, rule APIRule)
|
|||
</div>
|
||||
<div class="col">
|
||||
<a target="_blank" href="`)
|
||||
//line app/vmalert/web.qtpl:437
|
||||
//line app/vmalert/web.qtpl:447
|
||||
qw422016.E().S(prefix)
|
||||
//line app/vmalert/web.qtpl:437
|
||||
//line app/vmalert/web.qtpl:447
|
||||
qw422016.N().S(`groups#group-`)
|
||||
//line app/vmalert/web.qtpl:437
|
||||
//line app/vmalert/web.qtpl:447
|
||||
qw422016.E().S(rule.GroupID)
|
||||
//line app/vmalert/web.qtpl:437
|
||||
//line app/vmalert/web.qtpl:447
|
||||
qw422016.N().S(`">`)
|
||||
//line app/vmalert/web.qtpl:437
|
||||
//line app/vmalert/web.qtpl:447
|
||||
qw422016.E().S(rule.GroupID)
|
||||
//line app/vmalert/web.qtpl:437
|
||||
//line app/vmalert/web.qtpl:447
|
||||
qw422016.N().S(`</a>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1343,13 +1357,13 @@ func StreamRuleDetails(qw422016 *qt422016.Writer, r *http.Request, rule APIRule)
|
|||
|
||||
<br>
|
||||
<div class="display-6 pb-3">Last `)
|
||||
//line app/vmalert/web.qtpl:443
|
||||
//line app/vmalert/web.qtpl:453
|
||||
qw422016.N().D(len(rule.Updates))
|
||||
//line app/vmalert/web.qtpl:443
|
||||
//line app/vmalert/web.qtpl:453
|
||||
qw422016.N().S(`/`)
|
||||
//line app/vmalert/web.qtpl:443
|
||||
//line app/vmalert/web.qtpl:453
|
||||
qw422016.N().D(rule.MaxUpdates)
|
||||
//line app/vmalert/web.qtpl:443
|
||||
//line app/vmalert/web.qtpl:453
|
||||
qw422016.N().S(` updates</span>:</div>
|
||||
<table class="table table-striped table-hover table-sm">
|
||||
<thead>
|
||||
|
@ -1364,201 +1378,201 @@ func StreamRuleDetails(qw422016 *qt422016.Writer, r *http.Request, rule APIRule)
|
|||
<tbody>
|
||||
|
||||
`)
|
||||
//line app/vmalert/web.qtpl:456
|
||||
//line app/vmalert/web.qtpl:466
|
||||
for _, u := range rule.Updates {
|
||||
//line app/vmalert/web.qtpl:456
|
||||
//line app/vmalert/web.qtpl:466
|
||||
qw422016.N().S(`
|
||||
<tr`)
|
||||
//line app/vmalert/web.qtpl:457
|
||||
//line app/vmalert/web.qtpl:467
|
||||
if u.err != nil {
|
||||
//line app/vmalert/web.qtpl:457
|
||||
//line app/vmalert/web.qtpl:467
|
||||
qw422016.N().S(` class="alert-danger"`)
|
||||
//line app/vmalert/web.qtpl:457
|
||||
//line app/vmalert/web.qtpl:467
|
||||
}
|
||||
//line app/vmalert/web.qtpl:457
|
||||
//line app/vmalert/web.qtpl:467
|
||||
qw422016.N().S(`>
|
||||
<td>
|
||||
<span class="badge bg-primary rounded-pill me-3" title="Updated at">`)
|
||||
//line app/vmalert/web.qtpl:459
|
||||
//line app/vmalert/web.qtpl:469
|
||||
qw422016.E().S(u.time.Format(time.RFC3339))
|
||||
//line app/vmalert/web.qtpl:459
|
||||
//line app/vmalert/web.qtpl:469
|
||||
qw422016.N().S(`</span>
|
||||
</td>
|
||||
<td class="text-center" wi>`)
|
||||
//line app/vmalert/web.qtpl:461
|
||||
//line app/vmalert/web.qtpl:471
|
||||
qw422016.N().D(u.samples)
|
||||
//line app/vmalert/web.qtpl:461
|
||||
//line app/vmalert/web.qtpl:471
|
||||
qw422016.N().S(`</td>
|
||||
<td class="text-center">`)
|
||||
//line app/vmalert/web.qtpl:462
|
||||
//line app/vmalert/web.qtpl:472
|
||||
qw422016.N().FPrec(u.duration.Seconds(), 3)
|
||||
//line app/vmalert/web.qtpl:462
|
||||
//line app/vmalert/web.qtpl:472
|
||||
qw422016.N().S(`s</td>
|
||||
<td class="text-center">`)
|
||||
//line app/vmalert/web.qtpl:463
|
||||
//line app/vmalert/web.qtpl:473
|
||||
qw422016.E().S(u.at.Format(time.RFC3339))
|
||||
//line app/vmalert/web.qtpl:463
|
||||
//line app/vmalert/web.qtpl:473
|
||||
qw422016.N().S(`</td>
|
||||
<td>
|
||||
<textarea class="curl-area" rows="1" onclick="this.focus();this.select()">`)
|
||||
//line app/vmalert/web.qtpl:465
|
||||
//line app/vmalert/web.qtpl:475
|
||||
qw422016.E().S(u.curl)
|
||||
//line app/vmalert/web.qtpl:465
|
||||
//line app/vmalert/web.qtpl:475
|
||||
qw422016.N().S(`</textarea>
|
||||
</td>
|
||||
</tr>
|
||||
</li>
|
||||
`)
|
||||
//line app/vmalert/web.qtpl:469
|
||||
//line app/vmalert/web.qtpl:479
|
||||
if u.err != nil {
|
||||
//line app/vmalert/web.qtpl:469
|
||||
//line app/vmalert/web.qtpl:479
|
||||
qw422016.N().S(`
|
||||
<tr`)
|
||||
//line app/vmalert/web.qtpl:470
|
||||
//line app/vmalert/web.qtpl:480
|
||||
if u.err != nil {
|
||||
//line app/vmalert/web.qtpl:470
|
||||
//line app/vmalert/web.qtpl:480
|
||||
qw422016.N().S(` class="alert-danger"`)
|
||||
//line app/vmalert/web.qtpl:470
|
||||
//line app/vmalert/web.qtpl:480
|
||||
}
|
||||
//line app/vmalert/web.qtpl:470
|
||||
//line app/vmalert/web.qtpl:480
|
||||
qw422016.N().S(`>
|
||||
<td colspan="5">
|
||||
<span class="alert-danger">`)
|
||||
//line app/vmalert/web.qtpl:472
|
||||
//line app/vmalert/web.qtpl:482
|
||||
qw422016.E().V(u.err)
|
||||
//line app/vmalert/web.qtpl:472
|
||||
//line app/vmalert/web.qtpl:482
|
||||
qw422016.N().S(`</span>
|
||||
</td>
|
||||
</tr>
|
||||
`)
|
||||
//line app/vmalert/web.qtpl:475
|
||||
//line app/vmalert/web.qtpl:485
|
||||
}
|
||||
//line app/vmalert/web.qtpl:475
|
||||
//line app/vmalert/web.qtpl:485
|
||||
qw422016.N().S(`
|
||||
`)
|
||||
//line app/vmalert/web.qtpl:476
|
||||
//line app/vmalert/web.qtpl:486
|
||||
}
|
||||
//line app/vmalert/web.qtpl:476
|
||||
//line app/vmalert/web.qtpl:486
|
||||
qw422016.N().S(`
|
||||
|
||||
`)
|
||||
//line app/vmalert/web.qtpl:478
|
||||
//line app/vmalert/web.qtpl:488
|
||||
tpl.StreamFooter(qw422016, r)
|
||||
//line app/vmalert/web.qtpl:478
|
||||
//line app/vmalert/web.qtpl:488
|
||||
qw422016.N().S(`
|
||||
`)
|
||||
//line app/vmalert/web.qtpl:479
|
||||
//line app/vmalert/web.qtpl:489
|
||||
}
|
||||
|
||||
//line app/vmalert/web.qtpl:479
|
||||
//line app/vmalert/web.qtpl:489
|
||||
func WriteRuleDetails(qq422016 qtio422016.Writer, r *http.Request, rule APIRule) {
|
||||
//line app/vmalert/web.qtpl:479
|
||||
//line app/vmalert/web.qtpl:489
|
||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||
//line app/vmalert/web.qtpl:479
|
||||
//line app/vmalert/web.qtpl:489
|
||||
StreamRuleDetails(qw422016, r, rule)
|
||||
//line app/vmalert/web.qtpl:479
|
||||
//line app/vmalert/web.qtpl:489
|
||||
qt422016.ReleaseWriter(qw422016)
|
||||
//line app/vmalert/web.qtpl:479
|
||||
//line app/vmalert/web.qtpl:489
|
||||
}
|
||||
|
||||
//line app/vmalert/web.qtpl:479
|
||||
//line app/vmalert/web.qtpl:489
|
||||
func RuleDetails(r *http.Request, rule APIRule) string {
|
||||
//line app/vmalert/web.qtpl:479
|
||||
//line app/vmalert/web.qtpl:489
|
||||
qb422016 := qt422016.AcquireByteBuffer()
|
||||
//line app/vmalert/web.qtpl:479
|
||||
//line app/vmalert/web.qtpl:489
|
||||
WriteRuleDetails(qb422016, r, rule)
|
||||
//line app/vmalert/web.qtpl:479
|
||||
//line app/vmalert/web.qtpl:489
|
||||
qs422016 := string(qb422016.B)
|
||||
//line app/vmalert/web.qtpl:479
|
||||
//line app/vmalert/web.qtpl:489
|
||||
qt422016.ReleaseByteBuffer(qb422016)
|
||||
//line app/vmalert/web.qtpl:479
|
||||
//line app/vmalert/web.qtpl:489
|
||||
return qs422016
|
||||
//line app/vmalert/web.qtpl:479
|
||||
//line app/vmalert/web.qtpl:489
|
||||
}
|
||||
|
||||
//line app/vmalert/web.qtpl:483
|
||||
//line app/vmalert/web.qtpl:493
|
||||
func streambadgeState(qw422016 *qt422016.Writer, state string) {
|
||||
//line app/vmalert/web.qtpl:483
|
||||
//line app/vmalert/web.qtpl:493
|
||||
qw422016.N().S(`
|
||||
`)
|
||||
//line app/vmalert/web.qtpl:485
|
||||
//line app/vmalert/web.qtpl:495
|
||||
badgeClass := "bg-warning text-dark"
|
||||
if state == "firing" {
|
||||
badgeClass = "bg-danger"
|
||||
}
|
||||
|
||||
//line app/vmalert/web.qtpl:489
|
||||
//line app/vmalert/web.qtpl:499
|
||||
qw422016.N().S(`
|
||||
<span class="badge `)
|
||||
//line app/vmalert/web.qtpl:490
|
||||
//line app/vmalert/web.qtpl:500
|
||||
qw422016.E().S(badgeClass)
|
||||
//line app/vmalert/web.qtpl:490
|
||||
//line app/vmalert/web.qtpl:500
|
||||
qw422016.N().S(`">`)
|
||||
//line app/vmalert/web.qtpl:490
|
||||
//line app/vmalert/web.qtpl:500
|
||||
qw422016.E().S(state)
|
||||
//line app/vmalert/web.qtpl:490
|
||||
//line app/vmalert/web.qtpl:500
|
||||
qw422016.N().S(`</span>
|
||||
`)
|
||||
//line app/vmalert/web.qtpl:491
|
||||
//line app/vmalert/web.qtpl:501
|
||||
}
|
||||
|
||||
//line app/vmalert/web.qtpl:491
|
||||
//line app/vmalert/web.qtpl:501
|
||||
func writebadgeState(qq422016 qtio422016.Writer, state string) {
|
||||
//line app/vmalert/web.qtpl:491
|
||||
//line app/vmalert/web.qtpl:501
|
||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||
//line app/vmalert/web.qtpl:491
|
||||
//line app/vmalert/web.qtpl:501
|
||||
streambadgeState(qw422016, state)
|
||||
//line app/vmalert/web.qtpl:491
|
||||
//line app/vmalert/web.qtpl:501
|
||||
qt422016.ReleaseWriter(qw422016)
|
||||
//line app/vmalert/web.qtpl:491
|
||||
//line app/vmalert/web.qtpl:501
|
||||
}
|
||||
|
||||
//line app/vmalert/web.qtpl:491
|
||||
//line app/vmalert/web.qtpl:501
|
||||
func badgeState(state string) string {
|
||||
//line app/vmalert/web.qtpl:491
|
||||
//line app/vmalert/web.qtpl:501
|
||||
qb422016 := qt422016.AcquireByteBuffer()
|
||||
//line app/vmalert/web.qtpl:491
|
||||
//line app/vmalert/web.qtpl:501
|
||||
writebadgeState(qb422016, state)
|
||||
//line app/vmalert/web.qtpl:491
|
||||
//line app/vmalert/web.qtpl:501
|
||||
qs422016 := string(qb422016.B)
|
||||
//line app/vmalert/web.qtpl:491
|
||||
//line app/vmalert/web.qtpl:501
|
||||
qt422016.ReleaseByteBuffer(qb422016)
|
||||
//line app/vmalert/web.qtpl:491
|
||||
//line app/vmalert/web.qtpl:501
|
||||
return qs422016
|
||||
//line app/vmalert/web.qtpl:491
|
||||
//line app/vmalert/web.qtpl:501
|
||||
}
|
||||
|
||||
//line app/vmalert/web.qtpl:493
|
||||
//line app/vmalert/web.qtpl:503
|
||||
func streambadgeRestored(qw422016 *qt422016.Writer) {
|
||||
//line app/vmalert/web.qtpl:493
|
||||
//line app/vmalert/web.qtpl:503
|
||||
qw422016.N().S(`
|
||||
<span class="badge bg-warning text-dark" title="Alert state was restored after the service restart from remote storage">restored</span>
|
||||
`)
|
||||
//line app/vmalert/web.qtpl:495
|
||||
//line app/vmalert/web.qtpl:505
|
||||
}
|
||||
|
||||
//line app/vmalert/web.qtpl:495
|
||||
//line app/vmalert/web.qtpl:505
|
||||
func writebadgeRestored(qq422016 qtio422016.Writer) {
|
||||
//line app/vmalert/web.qtpl:495
|
||||
//line app/vmalert/web.qtpl:505
|
||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||
//line app/vmalert/web.qtpl:495
|
||||
//line app/vmalert/web.qtpl:505
|
||||
streambadgeRestored(qw422016)
|
||||
//line app/vmalert/web.qtpl:495
|
||||
//line app/vmalert/web.qtpl:505
|
||||
qt422016.ReleaseWriter(qw422016)
|
||||
//line app/vmalert/web.qtpl:495
|
||||
//line app/vmalert/web.qtpl:505
|
||||
}
|
||||
|
||||
//line app/vmalert/web.qtpl:495
|
||||
//line app/vmalert/web.qtpl:505
|
||||
func badgeRestored() string {
|
||||
//line app/vmalert/web.qtpl:495
|
||||
//line app/vmalert/web.qtpl:505
|
||||
qb422016 := qt422016.AcquireByteBuffer()
|
||||
//line app/vmalert/web.qtpl:495
|
||||
//line app/vmalert/web.qtpl:505
|
||||
writebadgeRestored(qb422016)
|
||||
//line app/vmalert/web.qtpl:495
|
||||
//line app/vmalert/web.qtpl:505
|
||||
qs422016 := string(qb422016.B)
|
||||
//line app/vmalert/web.qtpl:495
|
||||
//line app/vmalert/web.qtpl:505
|
||||
qt422016.ReleaseByteBuffer(qb422016)
|
||||
//line app/vmalert/web.qtpl:495
|
||||
//line app/vmalert/web.qtpl:505
|
||||
return qs422016
|
||||
//line app/vmalert/web.qtpl:495
|
||||
//line app/vmalert/web.qtpl:505
|
||||
}
|
||||
|
|
|
@ -113,18 +113,20 @@ type APIRule struct {
|
|||
|
||||
// Additional fields
|
||||
|
||||
// Type of the rule: recording or alerting
|
||||
// DatasourceType of the rule: prometheus or graphite
|
||||
DatasourceType string `json:"datasourceType"`
|
||||
LastSamples int `json:"lastSamples"`
|
||||
// ID is a unique Alert's ID within a group
|
||||
ID string `json:"id"`
|
||||
// GroupID is an unique Group's ID
|
||||
GroupID string `json:"group_id"`
|
||||
// Debug shows whether debug mode is enabled
|
||||
Debug bool `json:"debug"`
|
||||
|
||||
// MaxUpdates is the max number of recorded ruleStateEntry objects
|
||||
MaxUpdates int `json:"max_updates_entries"`
|
||||
// Updates contains the ordered list of recorded ruleStateEntry objects
|
||||
Updates []ruleStateEntry `json:"updates"`
|
||||
Updates []ruleStateEntry `json:"-"`
|
||||
}
|
||||
|
||||
// WebLink returns a link to the alert which can be used in UI.
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
{
|
||||
"files": {
|
||||
"main.css": "./static/css/main.3f9cb68f.css",
|
||||
"main.js": "./static/js/main.b1572032.js",
|
||||
"main.css": "./static/css/main.b9c2d13c.css",
|
||||
"main.js": "./static/js/main.40a4969a.js",
|
||||
"static/js/27.c1ccfd29.chunk.js": "./static/js/27.c1ccfd29.chunk.js",
|
||||
"static/media/Lato-Regular.ttf": "./static/media/Lato-Regular.d714fec1633b69a9c2e9.ttf",
|
||||
"static/media/Lato-Bold.ttf": "./static/media/Lato-Bold.32360ba4b57802daa4d6.ttf",
|
||||
"index.html": "./index.html"
|
||||
},
|
||||
"entrypoints": [
|
||||
"static/css/main.3f9cb68f.css",
|
||||
"static/js/main.b1572032.js"
|
||||
"static/css/main.b9c2d13c.css",
|
||||
"static/js/main.40a4969a.js"
|
||||
]
|
||||
}
|
|
@ -1 +1 @@
|
|||
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="./favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="UI for VictoriaMetrics"/><link rel="apple-touch-icon" href="./apple-touch-icon.png"/><link rel="icon" type="image/png" sizes="32x32" href="./favicon-32x32.png"><link rel="manifest" href="./manifest.json"/><title>VM UI</title><script src="./dashboards/index.js" type="module"></script><meta name="twitter:card" content="summary_large_image"><meta name="twitter:image" content="./preview.jpg"><meta name="twitter:title" content="UI for VictoriaMetrics"><meta name="twitter:description" content="Explore and troubleshoot your VictoriaMetrics data"><meta name="twitter:site" content="@VictoriaMetrics"><meta property="og:title" content="Metric explorer for VictoriaMetrics"><meta property="og:description" content="Explore and troubleshoot your VictoriaMetrics data"><meta property="og:image" content="./preview.jpg"><meta property="og:type" content="website"><script defer="defer" src="./static/js/main.b1572032.js"></script><link href="./static/css/main.3f9cb68f.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="./favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"/><meta name="theme-color" content="#000000"/><meta name="description" content="UI for VictoriaMetrics"/><link rel="apple-touch-icon" href="./apple-touch-icon.png"/><link rel="icon" type="image/png" sizes="32x32" href="./favicon-32x32.png"><link rel="manifest" href="./manifest.json"/><title>VM UI</title><script src="./dashboards/index.js" type="module"></script><meta name="twitter:card" content="summary_large_image"><meta name="twitter:image" content="./preview.jpg"><meta name="twitter:title" content="UI for VictoriaMetrics"><meta name="twitter:description" content="Explore and troubleshoot your VictoriaMetrics data"><meta name="twitter:site" content="@VictoriaMetrics"><meta property="og:title" content="Metric explorer for VictoriaMetrics"><meta property="og:description" content="Explore and troubleshoot your VictoriaMetrics data"><meta property="og:image" content="./preview.jpg"><meta property="og:type" content="website"><script defer="defer" src="./static/js/main.40a4969a.js"></script><link href="./static/css/main.b9c2d13c.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
File diff suppressed because one or more lines are too long
1
app/vmselect/vmui/static/css/main.b9c2d13c.css
Normal file
1
app/vmselect/vmui/static/css/main.b9c2d13c.css
Normal file
File diff suppressed because one or more lines are too long
2
app/vmselect/vmui/static/js/main.40a4969a.js
Normal file
2
app/vmselect/vmui/static/js/main.40a4969a.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
|
@ -60,7 +60,7 @@ VMUI can be used to paste into other applications
|
|||
| Name | Default | Description |
|
||||
|:------------------------|:-----------:|--------------------------------------------------------------------------------------:|
|
||||
| serverURL | domain name | Can't be changed from the UI |
|
||||
| inputTenantID | - | If the flag is present, the "Tenant ID" field is displayed |
|
||||
| useTenantID | - | If the flag is present, the "Tenant ID" select is displayed |
|
||||
| headerStyles.background | `#FFFFFF` | Header background color |
|
||||
| headerStyles.color | `#3F51B5` | Header font color |
|
||||
| palette.primary | `#3F51B5` | used to represent primary interface elements for a user |
|
||||
|
@ -74,7 +74,7 @@ VMUI can be used to paste into other applications
|
|||
```json
|
||||
{
|
||||
"serverURL": "http://localhost:8428",
|
||||
"inputTenantID": "true",
|
||||
"useTenantID": true,
|
||||
"headerStyles": {
|
||||
"background": "#FFFFFF",
|
||||
"color": "#538DE8"
|
||||
|
@ -93,7 +93,7 @@ VMUI can be used to paste into other applications
|
|||
|
||||
#### HTML example:
|
||||
```html
|
||||
<div id="root" data-params='{"serverURL":"http://localhost:8428","inputTenantID":"true","headerStyles":{"background":"#FFFFFF","color":"#538DE8"},"palette":{"primary":"#538DE8","secondary":"#F76F8E","error":"#FD151B","warning":"#FFB30F","success":"#7BE622","info":"#0F5BFF"}}'></div>
|
||||
<div id="root" data-params='{"serverURL":"http://localhost:8428","useTenantID":true,"headerStyles":{"background":"#FFFFFF","color":"#538DE8"},"palette":{"primary":"#538DE8","secondary":"#F76F8E","error":"#FD151B","warning":"#FFB30F","success":"#7BE622","info":"#0F5BFF"}}'></div>
|
||||
```
|
||||
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React, { FC, useEffect, useMemo, useRef, useState } from "preact/compat";
|
||||
import uPlot, { Series } from "uplot";
|
||||
import uPlot from "uplot";
|
||||
import { MetricResult } from "../../../api/types";
|
||||
import { formatPrettyNumber } from "../../../utils/uplot/helpers";
|
||||
import dayjs from "dayjs";
|
||||
|
@ -11,12 +11,13 @@ import { CloseIcon, DragIcon } from "../../Main/Icons";
|
|||
import classNames from "classnames";
|
||||
import { MouseEvent as ReactMouseEvent } from "react";
|
||||
import "./style.scss";
|
||||
import { SeriesItem } from "../../../utils/uplot/series";
|
||||
|
||||
export interface ChartTooltipProps {
|
||||
id: string,
|
||||
u: uPlot,
|
||||
metrics: MetricResult[],
|
||||
series: Series[],
|
||||
series: SeriesItem[],
|
||||
yRange: number[];
|
||||
unit?: string,
|
||||
isSticky?: boolean,
|
||||
|
@ -55,15 +56,16 @@ const ChartTooltip: FC<ChartTooltipProps> = ({
|
|||
|
||||
const color = series[seriesIdx]?.stroke+"";
|
||||
|
||||
const calculations = series[seriesIdx]?.calculations || {};
|
||||
|
||||
const groups = new Set(metrics.map(m => m.group));
|
||||
const showQueryNum = groups.size > 1;
|
||||
const group = metrics[seriesIdx-1]?.group || 0;
|
||||
|
||||
const metric = metrics[seriesIdx-1]?.metric || {};
|
||||
const labelNames = Object.keys(metric).filter(x => x != "__name__");
|
||||
const metricName = metric["__name__"] || "value";
|
||||
|
||||
const fields = useMemo(() => {
|
||||
const labelNames = Object.keys(metric).filter(x => x != "__name__");
|
||||
return labelNames.map(key => `${key}=${JSON.stringify(metric[key])}`);
|
||||
}, [metrics, seriesIdx]);
|
||||
|
||||
|
@ -100,10 +102,15 @@ const ChartTooltip: FC<ChartTooltipProps> = ({
|
|||
const overflowX = leftOnChart + tooltipWidth >= width ? tooltipWidth + (2 * margin) : 0;
|
||||
const overflowY = topOnChart + tooltipHeight >= height ? tooltipHeight + (2 * margin) : 0;
|
||||
|
||||
setPosition({
|
||||
const position = {
|
||||
top: topOnChart + tooltipOffset.top + margin - overflowY,
|
||||
left: leftOnChart + tooltipOffset.left + margin - overflowX
|
||||
});
|
||||
};
|
||||
|
||||
if (position.left < 0) position.left = 20;
|
||||
if (position.top < 0) position.top = 20;
|
||||
|
||||
setPosition(position);
|
||||
};
|
||||
|
||||
useEffect(calcPosition, [u, value, dataTime, seriesIdx, tooltipOffset, tooltipRef]);
|
||||
|
@ -169,19 +176,21 @@ const ChartTooltip: FC<ChartTooltipProps> = ({
|
|||
className="vm-chart-tooltip-data__marker"
|
||||
style={{ background: color }}
|
||||
/>
|
||||
<p>
|
||||
{metricName}:
|
||||
<b className="vm-chart-tooltip-data__value">{valueFormat}</b>
|
||||
{unit}
|
||||
</p>
|
||||
</div>
|
||||
{!!fields.length && (
|
||||
<div className="vm-chart-tooltip-info">
|
||||
{fields.map((f, i) => (
|
||||
<div key={`${f}_${i}`}>{f}</div>
|
||||
))}
|
||||
<div>
|
||||
curr:<b>{valueFormat}{unit}</b>, avg:<b>{calculations.avg}</b><br/>
|
||||
min:<b>{calculations.min}</b>, max:<b>{calculations.max}</b>, last:<b>{calculations.last}</b>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="vm-chart-tooltip-info">
|
||||
{metric["__name__"]}
|
||||
{
|
||||
{fields.map((f, i) => (
|
||||
<span key="{i}">
|
||||
{f}{i +1 < fields.length && ","}
|
||||
</span>
|
||||
))}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
), targetPortal);
|
||||
};
|
||||
|
|
|
@ -61,11 +61,6 @@ $chart-tooltip-y: -1 * ($padding-small + $chart-tooltip-half-icon);
|
|||
word-break: break-all;
|
||||
line-height: 12px;
|
||||
|
||||
&__value {
|
||||
padding: 4px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&__marker {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
|
|
|
@ -14,6 +14,7 @@ interface LegendItemProps {
|
|||
const LegendItem: FC<LegendItemProps> = ({ legend, onChange }) => {
|
||||
const [copiedValue, setCopiedValue] = useState("");
|
||||
const freeFormFields = useMemo(() => getFreeFields(legend), [legend]);
|
||||
const calculations = legend.calculations;
|
||||
|
||||
const handleClickFreeField = async (val: string, id: string) => {
|
||||
await navigator.clipboard.writeText(val);
|
||||
|
@ -30,11 +31,11 @@ const LegendItem: FC<LegendItemProps> = ({ legend, onChange }) => {
|
|||
handleClickFreeField(freeField, id);
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-legend-item": true,
|
||||
"vm-legend-row": true,
|
||||
"vm-legend-item_hide": !legend.checked,
|
||||
})}
|
||||
onClick={createHandlerClick(legend)}
|
||||
|
@ -45,30 +46,30 @@ const LegendItem: FC<LegendItemProps> = ({ legend, onChange }) => {
|
|||
/>
|
||||
<div className="vm-legend-item-info">
|
||||
<span className="vm-legend-item-info__label">
|
||||
{legend.freeFormFields["__name__"] || (freeFormFields.length == 0 ? "{}" : "")}
|
||||
</span>
|
||||
{freeFormFields.length > 0 &&
|
||||
<span>
|
||||
{
|
||||
{freeFormFields.map(f => (
|
||||
<Tooltip
|
||||
key={f.id}
|
||||
open={copiedValue === f.id}
|
||||
title={"Copied!"}
|
||||
placement="top-center"
|
||||
{legend.freeFormFields["__name__"]}
|
||||
{
|
||||
{freeFormFields.map((f, i) => (
|
||||
<Tooltip
|
||||
key={f.id}
|
||||
open={copiedValue === f.id}
|
||||
title={"copied!"}
|
||||
placement="top-center"
|
||||
>
|
||||
<span
|
||||
className="vm-legend-item-info__free-fields"
|
||||
key={f.key}
|
||||
onClick={createHandlerCopy(f.freeField, f.id)}
|
||||
title="copy to clipboard"
|
||||
>
|
||||
<span
|
||||
className="vm-legend-item-info__free-fields"
|
||||
key={f.key}
|
||||
onClick={createHandlerCopy(f.freeField, f.id)}
|
||||
>
|
||||
{f.freeField}
|
||||
</span>
|
||||
</Tooltip>
|
||||
))}
|
||||
}
|
||||
</span>
|
||||
}
|
||||
{f.freeField}{i + 1 < freeFormFields.length && ","}
|
||||
</span>
|
||||
</Tooltip>
|
||||
))}
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
<div className="vm-legend-item-values">
|
||||
avg:{calculations.avg}, min:{calculations.min}, max:{calculations.max}, last:{calculations.last}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -6,10 +6,11 @@
|
|||
grid-gap: $padding-small;
|
||||
align-items: start;
|
||||
justify-content: start;
|
||||
padding: $padding-small $padding-large $padding-small $padding-small;
|
||||
padding: $padding-small;
|
||||
background-color: $color-background-block;
|
||||
cursor: pointer;
|
||||
transition: 0.2s ease;
|
||||
margin-bottom: $padding-small;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
|
@ -30,22 +31,26 @@
|
|||
|
||||
&-info {
|
||||
font-weight: normal;
|
||||
word-break: break-all;
|
||||
|
||||
&__label {
|
||||
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
&__free-fields {
|
||||
padding: 3px;
|
||||
padding: 2px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
&:not(:last-child):after {
|
||||
content: ",";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-values {
|
||||
grid-column: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $padding-small;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,12 +9,13 @@
|
|||
|
||||
&-group {
|
||||
min-width: 23%;
|
||||
width: 100%;
|
||||
margin: 0 $padding-global $padding-global 0;
|
||||
|
||||
&-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 $padding-small $padding-small;
|
||||
padding: $padding-small;
|
||||
margin-bottom: 1px;
|
||||
border-bottom: $border-divider;
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ import classNames from "classnames";
|
|||
import ChartTooltip, { ChartTooltipProps } from "../ChartTooltip/ChartTooltip";
|
||||
import dayjs from "dayjs";
|
||||
import { useAppState } from "../../../state/common/StateContext";
|
||||
import { SeriesItem } from "../../../utils/uplot/series";
|
||||
|
||||
export interface LineChartProps {
|
||||
metrics: MetricResult[];
|
||||
|
@ -55,6 +56,7 @@ const LineChart: FC<LineChartProps> = ({
|
|||
const [xRange, setXRange] = useState({ min: period.start, max: period.end });
|
||||
const [yRange, setYRange] = useState([0, 1]);
|
||||
const [uPlotInst, setUPlotInst] = useState<uPlot>();
|
||||
const [startTouchDistance, setStartTouchDistance] = useState(0);
|
||||
const layoutSize = useResize(container);
|
||||
|
||||
const [showTooltip, setShowTooltip] = useState(false);
|
||||
|
@ -84,6 +86,7 @@ const LineChart: FC<LineChartProps> = ({
|
|||
left: parseFloat(u.over.style.left),
|
||||
top: parseFloat(u.over.style.top)
|
||||
});
|
||||
|
||||
u.over.addEventListener("mousedown", e => {
|
||||
const { ctrlKey, metaKey, button } = e;
|
||||
const leftClick = button === 0;
|
||||
|
@ -94,6 +97,10 @@ const LineChart: FC<LineChartProps> = ({
|
|||
}
|
||||
});
|
||||
|
||||
u.over.addEventListener("touchstart", e => {
|
||||
dragChart({ u, e, setPanning, setPlotScale, factor });
|
||||
});
|
||||
|
||||
u.over.addEventListener("wheel", e => {
|
||||
if (!e.ctrlKey && !e.metaKey) return;
|
||||
e.preventDefault();
|
||||
|
@ -235,6 +242,47 @@ const LineChart: FC<LineChartProps> = ({
|
|||
};
|
||||
}, [xRange]);
|
||||
|
||||
const handleTouchStart = (e: TouchEvent) => {
|
||||
if (e.touches.length !== 2) return;
|
||||
e.preventDefault();
|
||||
|
||||
const dx = e.touches[0].clientX - e.touches[1].clientX;
|
||||
const dy = e.touches[0].clientY - e.touches[1].clientY;
|
||||
setStartTouchDistance(Math.sqrt(dx * dx + dy * dy));
|
||||
};
|
||||
|
||||
const handleTouchMove = (e: TouchEvent) => {
|
||||
if (e.touches.length !== 2 || !uPlotInst) return;
|
||||
e.preventDefault();
|
||||
|
||||
const dx = e.touches[0].clientX - e.touches[1].clientX;
|
||||
const dy = e.touches[0].clientY - e.touches[1].clientY;
|
||||
const endTouchDistance = Math.sqrt(dx * dx + dy * dy);
|
||||
const diffDistance = startTouchDistance - endTouchDistance;
|
||||
|
||||
const max = (uPlotInst.scales.x.max || xRange.max);
|
||||
const min = (uPlotInst.scales.x.min || xRange.min);
|
||||
const dur = max - min;
|
||||
const dir = (diffDistance > 0 ? -1 : 1);
|
||||
|
||||
const zoomFactor = dur / 50 * dir;
|
||||
uPlotInst.batch(() => setPlotScale({
|
||||
u: uPlotInst,
|
||||
min: min + zoomFactor,
|
||||
max: max - zoomFactor
|
||||
}));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("touchmove", handleTouchMove);
|
||||
window.addEventListener("touchstart", handleTouchStart);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("touchmove", handleTouchMove);
|
||||
window.removeEventListener("touchstart", handleTouchStart);
|
||||
};
|
||||
}, [uPlotInst, startTouchDistance]);
|
||||
|
||||
useEffect(() => updateChart(typeChartUpdate.data), [data]);
|
||||
useEffect(() => updateChart(typeChartUpdate.xRange), [xRange]);
|
||||
useEffect(() => updateChart(typeChartUpdate.yRange), [yaxis]);
|
||||
|
@ -256,6 +304,10 @@ const LineChart: FC<LineChartProps> = ({
|
|||
"vm-line-chart": true,
|
||||
"vm-line-chart_panning": isPanning
|
||||
})}
|
||||
style={{
|
||||
minWidth: `${layoutSize.width || 400}px`,
|
||||
minHeight: `${height || 500}px`
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="vm-line-chart__u-plot"
|
||||
|
@ -265,7 +317,7 @@ const LineChart: FC<LineChartProps> = ({
|
|||
<ChartTooltip
|
||||
unit={unit}
|
||||
u={uPlotInst}
|
||||
series={series}
|
||||
series={series as SeriesItem[]}
|
||||
metrics={metrics}
|
||||
yRange={yRange}
|
||||
tooltipIdx={tooltipIdx}
|
||||
|
|
|
@ -14,10 +14,12 @@ import classNames from "classnames";
|
|||
import Timezones from "./Timezones/Timezones";
|
||||
import { useTimeDispatch, useTimeState } from "../../../state/time/TimeStateContext";
|
||||
import ThemeControl from "../ThemeControl/ThemeControl";
|
||||
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||
|
||||
const title = "Settings";
|
||||
|
||||
const GlobalSettings: FC = () => {
|
||||
const GlobalSettings: FC<{showTitle?: boolean}> = ({ showTitle }) => {
|
||||
const { isMobile } = useDeviceDetect();
|
||||
|
||||
const appModeEnable = getAppModeEnable();
|
||||
const { serverUrl: stateServerUrl } = useAppState();
|
||||
|
@ -49,7 +51,10 @@ const GlobalSettings: FC = () => {
|
|||
}, [stateServerUrl]);
|
||||
|
||||
return <>
|
||||
<Tooltip title={title}>
|
||||
<Tooltip
|
||||
open={showTitle === true ? false : undefined}
|
||||
title={title}
|
||||
>
|
||||
<Button
|
||||
className={classNames({
|
||||
"vm-header-button": !appModeEnable
|
||||
|
@ -58,14 +63,21 @@ const GlobalSettings: FC = () => {
|
|||
color="primary"
|
||||
startIcon={<SettingsIcon/>}
|
||||
onClick={handleOpen}
|
||||
/>
|
||||
>
|
||||
{showTitle && title}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
{open && (
|
||||
<Modal
|
||||
title={title}
|
||||
onClose={handleClose}
|
||||
>
|
||||
<div className="vm-server-configurator">
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-server-configurator": true,
|
||||
"vm-server-configurator_mobile": isMobile
|
||||
})}
|
||||
>
|
||||
{!appModeEnable && (
|
||||
<div className="vm-server-configurator__input">
|
||||
<ServerConfigurator
|
||||
|
@ -88,9 +100,11 @@ const GlobalSettings: FC = () => {
|
|||
onChange={setTimezone}
|
||||
/>
|
||||
</div>
|
||||
<div className="vm-server-configurator__input">
|
||||
<ThemeControl/>
|
||||
</div>
|
||||
{!appModeEnable && (
|
||||
<div className="vm-server-configurator__input">
|
||||
<ThemeControl/>
|
||||
</div>
|
||||
)}
|
||||
<div className="vm-server-configurator__footer">
|
||||
<Button
|
||||
variant="outlined"
|
||||
|
|
|
@ -70,15 +70,16 @@ const LimitsConfigurator: FC<ServerConfiguratorProps> = ({ limits, onChange , on
|
|||
</div>
|
||||
<div className="vm-limits-configurator__inputs">
|
||||
{fields.map(f => (
|
||||
<TextField
|
||||
key={f.type}
|
||||
label={f.label}
|
||||
value={limits[f.type]}
|
||||
error={error[f.type]}
|
||||
onChange={createChangeHandler(f.type)}
|
||||
onEnter={onEnter}
|
||||
type="number"
|
||||
/>
|
||||
<div key={f.type}>
|
||||
<TextField
|
||||
label={f.label}
|
||||
value={limits[f.type]}
|
||||
error={error[f.type]}
|
||||
onChange={createChangeHandler(f.type)}
|
||||
onEnter={onEnter}
|
||||
type="number"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -12,10 +12,14 @@
|
|||
}
|
||||
|
||||
&__inputs {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: $padding-global;
|
||||
|
||||
div {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React, { FC, useState, useRef, useEffect, useMemo } from "preact/compat";
|
||||
import { useAppDispatch, useAppState } from "../../../../state/common/StateContext";
|
||||
import { useTimeDispatch } from "../../../../state/time/TimeStateContext";
|
||||
import { ArrowDownIcon, StorageIcons } from "../../../Main/Icons";
|
||||
import { ArrowDownIcon, StorageIcon } from "../../../Main/Icons";
|
||||
import Button from "../../../Main/Button/Button";
|
||||
import "./style.scss";
|
||||
import { replaceTenantId } from "../../../../utils/default-server-url";
|
||||
|
@ -9,17 +9,32 @@ import classNames from "classnames";
|
|||
import Popper from "../../../Main/Popper/Popper";
|
||||
import { getAppModeEnable } from "../../../../utils/app-mode";
|
||||
import Tooltip from "../../../Main/Tooltip/Tooltip";
|
||||
import useDeviceDetect from "../../../../hooks/useDeviceDetect";
|
||||
import TextField from "../../../Main/TextField/TextField";
|
||||
|
||||
const TenantsConfiguration: FC<{accountIds: string[]}> = ({ accountIds }) => {
|
||||
const appModeEnable = getAppModeEnable();
|
||||
const { isMobile } = useDeviceDetect();
|
||||
|
||||
const { tenantId: tenantIdState, serverUrl } = useAppState();
|
||||
const dispatch = useAppDispatch();
|
||||
const timeDispatch = useTimeDispatch();
|
||||
|
||||
const [search, setSearch] = useState("");
|
||||
const [openOptions, setOpenOptions] = useState(false);
|
||||
const optionsButtonRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const accountIdsFiltered = useMemo(() => {
|
||||
if (!search) return accountIds;
|
||||
try {
|
||||
const regexp = new RegExp(search, "i");
|
||||
const found = accountIds.filter((item) => regexp.test(item));
|
||||
return found.sort((a,b) => (a.match(regexp)?.index || 0) - (b.match(regexp)?.index || 0));
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}, [search, accountIds]);
|
||||
|
||||
const getTenantIdFromUrl = (url: string) => {
|
||||
const regexp = /(\/select\/)(\d+|\d.+)(\/)(.+)/;
|
||||
return (url.match(regexp) || [])[2];
|
||||
|
@ -71,8 +86,8 @@ const TenantsConfiguration: FC<{accountIds: string[]}> = ({ accountIds }) => {
|
|||
variant="contained"
|
||||
color="primary"
|
||||
fullWidth
|
||||
startIcon={<StorageIcons/>}
|
||||
endIcon={(
|
||||
startIcon={<StorageIcon/>}
|
||||
endIcon={!isMobile ? (
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-execution-controls-buttons__arrow": true,
|
||||
|
@ -81,22 +96,29 @@ const TenantsConfiguration: FC<{accountIds: string[]}> = ({ accountIds }) => {
|
|||
>
|
||||
<ArrowDownIcon/>
|
||||
</div>
|
||||
)}
|
||||
) : undefined}
|
||||
onClick={toggleOpenOptions}
|
||||
>
|
||||
{tenantIdState}
|
||||
{!isMobile && tenantIdState}
|
||||
</Button>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Popper
|
||||
open={openOptions}
|
||||
placement="bottom-left"
|
||||
placement="bottom-right"
|
||||
onClose={handleCloseOptions}
|
||||
buttonRef={optionsButtonRef}
|
||||
fullWidth
|
||||
>
|
||||
<div className="vm-list">
|
||||
{accountIds.map(id => (
|
||||
<div className="vm-list vm-tenant-input-list">
|
||||
<div className="vm-tenant-input-list__search">
|
||||
<TextField
|
||||
autofocus
|
||||
label="Search"
|
||||
value={search}
|
||||
onChange={setSearch}
|
||||
/>
|
||||
</div>
|
||||
{accountIdsFiltered.map(id => (
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-list-item": true,
|
||||
|
|
|
@ -2,8 +2,10 @@ import { useAppState } from "../../../../../state/common/StateContext";
|
|||
import { useEffect, useMemo, useState } from "preact/compat";
|
||||
import { ErrorTypes } from "../../../../../types";
|
||||
import { getAccountIds } from "../../../../../api/accountId";
|
||||
import { getAppModeParams } from "../../../../../utils/app-mode";
|
||||
|
||||
export const useFetchAccountIds = () => {
|
||||
const { useTenantID } = getAppModeParams();
|
||||
const { serverUrl } = useAppState();
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
@ -13,13 +15,14 @@ export const useFetchAccountIds = () => {
|
|||
const fetchUrl = useMemo(() => getAccountIds(serverUrl), [serverUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!useTenantID) return;
|
||||
const fetchData = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await fetch(fetchUrl);
|
||||
const resp = await response.json();
|
||||
const data = (resp.data || []) as string[];
|
||||
setAccountIds(data);
|
||||
setAccountIds(data.sort((a, b) => a.localeCompare(b)));
|
||||
|
||||
if (response.ok) {
|
||||
setError(undefined);
|
||||
|
|
|
@ -2,4 +2,18 @@
|
|||
|
||||
.vm-tenant-input {
|
||||
position: relative;
|
||||
|
||||
&-list {
|
||||
max-height: 300px;
|
||||
overflow: auto;
|
||||
overscroll-behavior: none;
|
||||
border-radius: $border-radius-medium;
|
||||
|
||||
&__search {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
padding: $padding-small;
|
||||
background-color: $color-background-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -91,6 +91,7 @@ const Timezones: FC<TimezonesProps> = ({ timezoneState, onChange }) => {
|
|||
buttonRef={targetRef}
|
||||
placement="bottom-left"
|
||||
onClose={handleCloseList}
|
||||
fullWidth
|
||||
>
|
||||
<div className="vm-timezones-list">
|
||||
<div className="vm-timezones-list-header">
|
||||
|
|
|
@ -46,7 +46,6 @@
|
|||
}
|
||||
|
||||
&-list {
|
||||
min-width: 600px;
|
||||
max-height: 200px;
|
||||
background-color: $color-background-block;
|
||||
border-radius: $border-radius-medium;
|
||||
|
|
|
@ -1,12 +1,25 @@
|
|||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-server-configurator {
|
||||
display: grid;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: $padding-medium;
|
||||
width: 600px;
|
||||
|
||||
&_mobile {
|
||||
grid-auto-rows: min-content;
|
||||
align-items: flex-start;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__input {
|
||||
width: 100%;
|
||||
|
||||
&_server {
|
||||
display: grid;
|
||||
|
@ -34,4 +47,10 @@
|
|||
margin-left: auto;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
&_mobile &__footer {
|
||||
align-items: flex-end;
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -111,7 +111,12 @@ const StepConfigurator: FC = () => {
|
|||
startIcon={<TimelineIcon/>}
|
||||
onClick={toggleOpenOptions}
|
||||
>
|
||||
STEP {customStep}
|
||||
<p>
|
||||
STEP
|
||||
<p className="vm-step-control__value">
|
||||
{customStep}
|
||||
</p>
|
||||
</p>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Popper
|
||||
|
|
|
@ -8,6 +8,15 @@
|
|||
text-transform: none;
|
||||
}
|
||||
|
||||
&__value {
|
||||
display: inline;
|
||||
margin-left: 3px;
|
||||
|
||||
@media (max-width: 500px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&-popper {
|
||||
display: grid;
|
||||
gap: $padding-small;
|
||||
|
|
|
@ -3,9 +3,12 @@ import "./style.scss";
|
|||
import { useAppDispatch, useAppState } from "../../../state/common/StateContext";
|
||||
import { Theme } from "../../../types";
|
||||
import Toggle from "../../Main/Toggle/Toggle";
|
||||
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||
import classNames from "classnames";
|
||||
|
||||
const options = Object.values(Theme).map(value => ({ title: value, value }));
|
||||
const ThemeControl = () => {
|
||||
const { isMobile } = useDeviceDetect();
|
||||
const { theme } = useAppState();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
|
@ -14,11 +17,19 @@ const ThemeControl = () => {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="vm-theme-control">
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-theme-control": true,
|
||||
"vm-theme-control_mobile": isMobile
|
||||
})}
|
||||
>
|
||||
<div className="vm-server-configurator__title">
|
||||
Theme preferences
|
||||
</div>
|
||||
<div className="vm-theme-control__toggle">
|
||||
<div
|
||||
className="vm-theme-control__toggle"
|
||||
key={`${isMobile}`}
|
||||
>
|
||||
<Toggle
|
||||
options={options}
|
||||
value={theme}
|
||||
|
|
|
@ -7,4 +7,8 @@
|
|||
min-width: 300px;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
&_mobile &__toggle {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import Popper from "../../../Main/Popper/Popper";
|
|||
import "./style.scss";
|
||||
import classNames from "classnames";
|
||||
import Tooltip from "../../../Main/Tooltip/Tooltip";
|
||||
import useResize from "../../../../hooks/useResize";
|
||||
|
||||
interface AutoRefreshOption {
|
||||
seconds: number
|
||||
|
@ -29,6 +30,7 @@ const delayOptions: AutoRefreshOption[] = [
|
|||
];
|
||||
|
||||
export const ExecutionControls: FC = () => {
|
||||
const windowSize = useResize(document.body);
|
||||
|
||||
const dispatch = useTimeDispatch();
|
||||
const appModeEnable = getAppModeEnable();
|
||||
|
@ -83,17 +85,20 @@ export const ExecutionControls: FC = () => {
|
|||
<div
|
||||
className={classNames({
|
||||
"vm-execution-controls-buttons": true,
|
||||
"vm-header-button": !appModeEnable
|
||||
"vm-header-button": !appModeEnable,
|
||||
"vm-execution-controls-buttons_short": windowSize.width <= 360
|
||||
})}
|
||||
>
|
||||
<Tooltip title="Refresh dashboard">
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleUpdate}
|
||||
startIcon={<RefreshIcon/>}
|
||||
/>
|
||||
</Tooltip>
|
||||
{windowSize.width > 360 && (
|
||||
<Tooltip title="Refresh dashboard">
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleUpdate}
|
||||
startIcon={<RefreshIcon/>}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip title="Auto-refresh control">
|
||||
<div ref={optionsButtonRef}>
|
||||
<Button
|
||||
|
|
|
@ -9,6 +9,10 @@
|
|||
border-radius: calc($button-radius + 1px);
|
||||
min-width: 107px;
|
||||
|
||||
&_short {
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
&__arrow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
@ -5,6 +5,11 @@
|
|||
grid-template-columns: repeat(2, 230px);
|
||||
padding: $padding-global 0;
|
||||
|
||||
@media (max-width: 500px) {
|
||||
grid-template-columns: 1fr;
|
||||
min-width: 250px;
|
||||
}
|
||||
|
||||
&-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -12,6 +17,12 @@
|
|||
border-right: $border-divider;
|
||||
padding: 0 $padding-global;
|
||||
|
||||
@media (max-width: 500px) {
|
||||
border-right: none;
|
||||
border-bottom: $border-divider;
|
||||
padding-bottom: $padding-global;
|
||||
}
|
||||
|
||||
&-inputs {
|
||||
flex-grow: 1;
|
||||
display: grid;
|
||||
|
|
|
@ -5,16 +5,18 @@
|
|||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: $padding-small calc($padding-small + 10px);
|
||||
gap: $padding-global calc($padding-small + 10px);
|
||||
|
||||
&__job {
|
||||
flex-grow: 0.5;
|
||||
min-width: 200px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
&__instance {
|
||||
flex-grow: 2;
|
||||
}
|
||||
|
||||
&__size {
|
||||
flex-grow: 1;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
&-metrics {
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
.vm-footer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: $padding-medium;
|
||||
|
@ -21,6 +22,11 @@
|
|||
|
||||
&__website {
|
||||
margin-right: $padding-global;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
margin-right: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&__link {
|
||||
|
@ -30,5 +36,10 @@
|
|||
&__copyright {
|
||||
text-align: right;
|
||||
flex-grow: 1;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,8 +17,13 @@ import { useAppState } from "../../../state/common/StateContext";
|
|||
import HeaderNav from "./HeaderNav/HeaderNav";
|
||||
import TenantsConfiguration from "../../Configurators/GlobalSettings/TenantsConfiguration/TenantsConfiguration";
|
||||
import { useFetchAccountIds } from "../../Configurators/GlobalSettings/TenantsConfiguration/hooks/useFetchAccountIds";
|
||||
import useResize from "../../../hooks/useResize";
|
||||
import SidebarHeader from "./SidebarNav/SidebarHeader";
|
||||
|
||||
const Header: FC = () => {
|
||||
const windowSize = useResize(document.body);
|
||||
const displaySidebar = useMemo(() => window.innerWidth < 1000, [windowSize]);
|
||||
|
||||
const { isDarkTheme } = useAppState();
|
||||
const appModeEnable = getAppModeEnable();
|
||||
const { accountIds } = useFetchAccountIds();
|
||||
|
@ -58,27 +63,37 @@ const Header: FC = () => {
|
|||
})}
|
||||
style={{ background, color }}
|
||||
>
|
||||
{!appModeEnable && (
|
||||
<div
|
||||
className="vm-header-logo"
|
||||
onClick={onClickLogo}
|
||||
style={{ color }}
|
||||
>
|
||||
<LogoFullIcon/>
|
||||
</div>
|
||||
{displaySidebar ? (
|
||||
<SidebarHeader
|
||||
background={background}
|
||||
color={color}
|
||||
onClickLogo={onClickLogo}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{!appModeEnable && (
|
||||
<div
|
||||
className="vm-header-logo"
|
||||
onClick={onClickLogo}
|
||||
style={{ color }}
|
||||
>
|
||||
<LogoFullIcon/>
|
||||
</div>
|
||||
)}
|
||||
<HeaderNav
|
||||
color={color}
|
||||
background={background}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<HeaderNav
|
||||
color={color}
|
||||
background={background}
|
||||
/>
|
||||
<div className="vm-header__settings">
|
||||
{headerSetup?.tenant && <TenantsConfiguration accountIds={accountIds}/>}
|
||||
{headerSetup?.stepControl && <StepConfigurator/>}
|
||||
{headerSetup?.timeSelector && <TimeSelector/>}
|
||||
{headerSetup?.cardinalityDatePicker && <CardinalityDatePicker/>}
|
||||
{headerSetup?.executionControls && <ExecutionControls/>}
|
||||
<GlobalSettings/>
|
||||
<ShortcutKeys/>
|
||||
{!displaySidebar && <GlobalSettings/>}
|
||||
{!displaySidebar && <ShortcutKeys/>}
|
||||
</div>
|
||||
</header>;
|
||||
};
|
||||
|
|
|
@ -7,13 +7,15 @@ import { useEffect } from "react";
|
|||
import "./style.scss";
|
||||
import NavItem from "./NavItem";
|
||||
import NavSubItem from "./NavSubItem";
|
||||
import classNames from "classnames";
|
||||
|
||||
interface HeaderNavProps {
|
||||
color: string
|
||||
background: string
|
||||
direction?: "row" | "column"
|
||||
}
|
||||
|
||||
const HeaderNav: FC<HeaderNavProps> = ({ color, background }) => {
|
||||
const HeaderNav: FC<HeaderNavProps> = ({ color, background, direction }) => {
|
||||
const appModeEnable = getAppModeEnable();
|
||||
const { dashboardsSettings } = useDashboardsState();
|
||||
const { pathname } = useLocation();
|
||||
|
@ -59,7 +61,12 @@ const HeaderNav: FC<HeaderNavProps> = ({ color, background }) => {
|
|||
|
||||
|
||||
return (
|
||||
<nav className="vm-header-nav">
|
||||
<nav
|
||||
className={classNames({
|
||||
"vm-header-nav": true,
|
||||
[`vm-header-nav_${direction}`]: direction
|
||||
})}
|
||||
>
|
||||
{menu.map(m => (
|
||||
m.submenu
|
||||
? (
|
||||
|
@ -70,6 +77,7 @@ const HeaderNav: FC<HeaderNavProps> = ({ color, background }) => {
|
|||
submenu={m.submenu}
|
||||
color={color}
|
||||
background={background}
|
||||
direction={direction}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
|
|
|
@ -12,6 +12,7 @@ interface NavItemProps {
|
|||
submenu: {label: string | undefined, value: string}[],
|
||||
color?: string
|
||||
background?: string
|
||||
direction?: "row" | "column"
|
||||
}
|
||||
|
||||
const NavSubItem: FC<NavItemProps> = ({
|
||||
|
@ -19,7 +20,8 @@ const NavSubItem: FC<NavItemProps> = ({
|
|||
label,
|
||||
color,
|
||||
background,
|
||||
submenu
|
||||
submenu,
|
||||
direction
|
||||
}) => {
|
||||
const { pathname } = useLocation();
|
||||
|
||||
|
@ -50,6 +52,21 @@ const NavSubItem: FC<NavItemProps> = ({
|
|||
handleCloseSubmenu();
|
||||
}, [pathname]);
|
||||
|
||||
if (direction === "column") {
|
||||
return (
|
||||
<>
|
||||
{submenu.map(sm => (
|
||||
<NavItem
|
||||
key={sm.value}
|
||||
activeMenu={activeMenu}
|
||||
value={sm.value}
|
||||
label={sm.label || ""}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames({
|
||||
|
@ -85,6 +102,7 @@ const NavSubItem: FC<NavItemProps> = ({
|
|||
activeMenu={activeMenu}
|
||||
value={sm.value}
|
||||
label={sm.label || ""}
|
||||
color={color}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
@ -8,6 +8,20 @@
|
|||
font-size: $font-size-small;
|
||||
font-weight: bold;
|
||||
|
||||
&_column {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: $padding-small;
|
||||
}
|
||||
|
||||
&_column &-item {
|
||||
padding: $padding-global 0;
|
||||
|
||||
&_sub {
|
||||
justify-content: stretch;
|
||||
}
|
||||
}
|
||||
|
||||
&-item {
|
||||
position: relative;
|
||||
padding: $padding-global $padding-small;
|
||||
|
|
|
@ -0,0 +1,85 @@
|
|||
import React, { FC, useEffect, useRef, useState } from "preact/compat";
|
||||
import GlobalSettings from "../../../Configurators/GlobalSettings/GlobalSettings";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import ShortcutKeys from "../../../Main/ShortcutKeys/ShortcutKeys";
|
||||
import { LogoFullIcon } from "../../../Main/Icons";
|
||||
import classNames from "classnames";
|
||||
import HeaderNav from "../HeaderNav/HeaderNav";
|
||||
import useClickOutside from "../../../../hooks/useClickOutside";
|
||||
import MenuBurger from "../../../Main/MenuBurger/MenuBurger";
|
||||
import useDeviceDetect from "../../../../hooks/useDeviceDetect";
|
||||
import "./style.scss";
|
||||
|
||||
interface SidebarHeaderProps {
|
||||
background: string
|
||||
color: string
|
||||
onClickLogo: () => void
|
||||
}
|
||||
|
||||
const SidebarHeader: FC<SidebarHeaderProps> = ({
|
||||
background,
|
||||
color,
|
||||
onClickLogo,
|
||||
}) => {
|
||||
const { pathname } = useLocation();
|
||||
const { isMobile } = useDeviceDetect();
|
||||
|
||||
const sidebarRef = useRef<HTMLDivElement>(null);
|
||||
const [openMenu, setOpenMenu] = useState(false);
|
||||
|
||||
const handleToggleMenu = () => {
|
||||
setOpenMenu(prev => !prev);
|
||||
};
|
||||
|
||||
const handleCloseMenu = () => {
|
||||
setOpenMenu(false);
|
||||
};
|
||||
|
||||
useEffect(handleCloseMenu, [pathname]);
|
||||
|
||||
useClickOutside(sidebarRef, handleCloseMenu);
|
||||
|
||||
return <div
|
||||
className="vm-header-sidebar"
|
||||
ref={sidebarRef}
|
||||
>
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-header-sidebar-button": true,
|
||||
"vm-header-sidebar-button_open": openMenu
|
||||
})}
|
||||
>
|
||||
<MenuBurger
|
||||
open={openMenu}
|
||||
onClick={handleToggleMenu}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-header-sidebar-menu": true,
|
||||
"vm-header-sidebar-menu_open": openMenu
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className="vm-header-sidebar-menu__logo"
|
||||
onClick={onClickLogo}
|
||||
style={{ color }}
|
||||
>
|
||||
<LogoFullIcon/>
|
||||
</div>
|
||||
<div>
|
||||
<HeaderNav
|
||||
color={color}
|
||||
background={background}
|
||||
direction="column"
|
||||
/>
|
||||
</div>
|
||||
<div className="vm-header-sidebar-menu-settings">
|
||||
<GlobalSettings showTitle={true}/>
|
||||
{!isMobile && <ShortcutKeys showTitle={true}/>}
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
|
||||
export default SidebarHeader;
|
|
@ -0,0 +1,58 @@
|
|||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-header-sidebar {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
color: inherit;
|
||||
background-color: inherit;
|
||||
|
||||
&-button {
|
||||
position: absolute;
|
||||
left: $padding-global;
|
||||
top: $padding-global;
|
||||
transition: left 300ms cubic-bezier(0.280, 0.840, 0.420, 1);
|
||||
|
||||
&_open {
|
||||
position: fixed;
|
||||
left: calc(182px - $padding-global);
|
||||
z-index: 102;
|
||||
}
|
||||
}
|
||||
|
||||
&-menu {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: grid;
|
||||
gap: $padding-global;
|
||||
padding: $padding-global;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
width: 200px;
|
||||
height: 100%;
|
||||
background-color: inherit;
|
||||
z-index: 101;
|
||||
transform-origin: left;
|
||||
transform: translateX(-100%);
|
||||
transition: transform 300ms cubic-bezier(0.280, 0.840, 0.420, 1);
|
||||
box-shadow: $box-shadow-popper;
|
||||
|
||||
&_open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
&__logo {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
cursor: pointer;
|
||||
width: 65px;
|
||||
}
|
||||
|
||||
&-settings {
|
||||
display: grid;
|
||||
align-items: center;
|
||||
gap: $padding-small;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -7,12 +7,20 @@
|
|||
justify-content: flex-start;
|
||||
padding: $padding-small $padding-medium;
|
||||
gap: 0 $padding-large;
|
||||
min-height: 51px;
|
||||
z-index: 99;
|
||||
|
||||
&_app {
|
||||
padding: $padding-small 0;
|
||||
}
|
||||
|
||||
@media (max-width: 1000px) {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
gap: $padding-small;
|
||||
padding: $padding-small;
|
||||
}
|
||||
|
||||
&_dark {
|
||||
.vm-header-button,
|
||||
button:before,
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
.vm-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: calc(100vh - var(--scrollbar-height));
|
||||
min-height: calc(($vh * 100) - var(--scrollbar-height));
|
||||
|
||||
&-body {
|
||||
flex-grow: 1;
|
||||
|
@ -11,6 +11,10 @@
|
|||
padding: $padding-medium;
|
||||
background-color: $color-background-body;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&_app {
|
||||
padding: $padding-small 0;
|
||||
background-color: transparent;
|
||||
|
|
|
@ -390,7 +390,7 @@ export const QuestionIcon = () => (
|
|||
|
||||
);
|
||||
|
||||
export const StorageIcons = () => (
|
||||
export const StorageIcon = () => (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
|
@ -400,3 +400,14 @@ export const StorageIcons = () => (
|
|||
></path>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const MenuIcon = () => (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
d="M4 18h16c.55 0 1-.45 1-1s-.45-1-1-1H4c-.55 0-1 .45-1 1s.45 1 1 1zm0-5h16c.55 0 1-.45 1-1s-.45-1-1-1H4c-.55 0-1 .45-1 1s.45 1 1 1zM3 7c0 .55.45 1 1 1h16c.55 0 1-.45 1-1s-.45-1-1-1H4c-.55 0-1 .45-1 1z"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
import React from "preact/compat";
|
||||
import classNames from "classnames";
|
||||
import "./style.scss";
|
||||
|
||||
const MenuBurger = ({ open, onClick }: {open: boolean, onClick: () => void}) => (
|
||||
<button
|
||||
className={classNames({
|
||||
"vm-menu-burger": true,
|
||||
"vm-menu-burger_opened": open
|
||||
})}
|
||||
onClick={onClick}
|
||||
>
|
||||
<span></span>
|
||||
</button>
|
||||
);
|
||||
|
||||
export default MenuBurger;
|
133
app/vmui/packages/vmui/src/components/Main/MenuBurger/style.scss
Normal file
133
app/vmui/packages/vmui/src/components/Main/MenuBurger/style.scss
Normal file
|
@ -0,0 +1,133 @@
|
|||
@use "src/styles/variables" as *;
|
||||
|
||||
$width-line: 2px;
|
||||
|
||||
.vm-menu-burger {
|
||||
position: relative;
|
||||
border: none;
|
||||
background: none;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
padding: 0;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
transform-style: preserve-3d;
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -6px;
|
||||
top: -6px;
|
||||
width: calc(100% + 12px);
|
||||
height: calc(100% + 12px);
|
||||
background-color: rgba($color-black, 0.1);
|
||||
border-radius: 50%;
|
||||
transform: scale(0) translateZ(-2px);
|
||||
transition: transform 140ms ease-in-out;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&:after {
|
||||
transform: scale(1) translateZ(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
display: block;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
border-top: $width-line solid #fff;
|
||||
transition: transform 0.3s ease, border-color 0.3s ease;
|
||||
|
||||
&,
|
||||
&:before,
|
||||
&:after {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: $width-line;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
&:before,
|
||||
&:after {
|
||||
content: '';
|
||||
top: 0;
|
||||
background: $color-white;
|
||||
animation-duration: 0.6s;
|
||||
animation-timing-function: cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
&:before {
|
||||
animation-name: topLineBurger;
|
||||
}
|
||||
|
||||
&:after {
|
||||
animation-name: bottomLineBurger;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
&_opened span {
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
&_opened span:before {
|
||||
animation-name: topLineCross;
|
||||
}
|
||||
|
||||
&_opened span:after {
|
||||
animation-name: bottomLineCross;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes topLineCross {
|
||||
0% {
|
||||
transform: translateY(-7px);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(0px);
|
||||
}
|
||||
100% {
|
||||
width: 60%;
|
||||
transform: translateY(-2px) translateX(30%) rotate(45deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bottomLineCross {
|
||||
0% {
|
||||
transform: translateY(3px);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(0px);
|
||||
}
|
||||
100% {
|
||||
width: 60%;
|
||||
transform: translateY(-2px) translateX(30%) rotate(-45deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes topLineBurger {
|
||||
0% {
|
||||
transform: translateY(0px) rotate(45deg);
|
||||
}
|
||||
50% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(-7px) rotate(0deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bottomLineBurger {
|
||||
0% {
|
||||
transform: translateY(0px) rotate(-45deg);
|
||||
}
|
||||
50% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(3px) rotate(0deg);
|
||||
}
|
||||
}
|
|
@ -4,6 +4,8 @@ import { CloseIcon } from "../Icons";
|
|||
import Button from "../Button/Button";
|
||||
import { ReactNode, MouseEvent } from "react";
|
||||
import "./style.scss";
|
||||
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||
import classNames from "classnames";
|
||||
|
||||
interface ModalProps {
|
||||
title?: string
|
||||
|
@ -12,6 +14,7 @@ interface ModalProps {
|
|||
}
|
||||
|
||||
const Modal: FC<ModalProps> = ({ title, children, onClose }) => {
|
||||
const { isMobile } = useDeviceDetect();
|
||||
|
||||
const handleKeyUp = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") onClose();
|
||||
|
@ -22,16 +25,21 @@ const Modal: FC<ModalProps> = ({ title, children, onClose }) => {
|
|||
};
|
||||
|
||||
useEffect(() => {
|
||||
document.body.style.overflow = "hidden";
|
||||
window.addEventListener("keyup", handleKeyUp);
|
||||
|
||||
return () => {
|
||||
document.body.style.overflow = "auto";
|
||||
window.removeEventListener("keyup", handleKeyUp);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return ReactDOM.createPortal((
|
||||
<div
|
||||
className="vm-modal"
|
||||
className={classNames({
|
||||
"vm-modal": true,
|
||||
"vm-modal_mobile": isMobile
|
||||
})}
|
||||
onMouseDown={onClose}
|
||||
>
|
||||
<div className="vm-modal-content">
|
||||
|
|
|
@ -14,12 +14,28 @@ $padding-modal: 22px;
|
|||
justify-content: center;
|
||||
background: rgba($color-black, 0.55);
|
||||
|
||||
&_mobile &-content {
|
||||
min-height: calc($vh * 100);
|
||||
max-height: calc($vh * 100);
|
||||
width: 100vw;
|
||||
border-radius: 0;
|
||||
|
||||
&-body {
|
||||
display: grid;
|
||||
align-items: flex-start;
|
||||
min-height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&-content {
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr;
|
||||
align-items: flex-start;
|
||||
padding: $padding-modal;
|
||||
background: $color-background-block;
|
||||
box-shadow: 0 0 24px rgba($color-black, 0.07);
|
||||
border-radius: $border-radius-small;
|
||||
max-height: 90vh;
|
||||
max-height: calc($vh * 90);
|
||||
overflow: auto;
|
||||
|
||||
&-header {
|
||||
|
@ -44,6 +60,8 @@ $padding-modal: 22px;
|
|||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
&-body {}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -99,12 +99,20 @@ const Popper: FC<PopperProps> = ({
|
|||
if (isOverflowLeft) position.left = buttonPos.left + offsetLeft;
|
||||
|
||||
if (fullWidth) position.width = `${buttonPos.width}px`;
|
||||
if (position.top < 0) position.top = 20;
|
||||
|
||||
return position;
|
||||
},[buttonRef, placement, isOpen, children, fullWidth]);
|
||||
|
||||
if (clickOutside) useClickOutside(popperRef, () => setIsOpen(false), buttonRef);
|
||||
|
||||
useEffect(() => {
|
||||
if (!popperRef.current || !isOpen) return;
|
||||
const { right, width } = popperRef.current.getBoundingClientRect();
|
||||
if (right > window.innerWidth) popperRef.current.style.left = `${window.innerWidth - 20 -width}px`;
|
||||
}, [isOpen, popperRef]);
|
||||
|
||||
|
||||
const popperClasses = classNames({
|
||||
"vm-popper": true,
|
||||
"vm-popper_open": isOpen,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React, { FC, useState } from "preact/compat";
|
||||
import { isMacOs } from "../../../utils/detect-os";
|
||||
import { isMacOs } from "../../../utils/detect-device";
|
||||
import { getAppModeEnable } from "../../../utils/app-mode";
|
||||
import Button from "../Button/Button";
|
||||
import { KeyboardIcon } from "../Icons";
|
||||
|
@ -69,7 +69,9 @@ const keyList = [
|
|||
}
|
||||
];
|
||||
|
||||
const ShortcutKeys: FC = () => {
|
||||
const title = "Shortcut keys";
|
||||
|
||||
const ShortcutKeys: FC<{showTitle?: boolean}> = ({ showTitle }) => {
|
||||
const [openList, setOpenList] = useState(false);
|
||||
const appModeEnable = getAppModeEnable();
|
||||
|
||||
|
@ -83,7 +85,8 @@ const ShortcutKeys: FC = () => {
|
|||
|
||||
return <>
|
||||
<Tooltip
|
||||
title="Shortcut keys"
|
||||
open={showTitle === true ? false : undefined}
|
||||
title={title}
|
||||
placement="bottom-center"
|
||||
>
|
||||
<Button
|
||||
|
@ -92,7 +95,9 @@ const ShortcutKeys: FC = () => {
|
|||
color="primary"
|
||||
startIcon={<KeyboardIcon/>}
|
||||
onClick={handleOpen}
|
||||
/>
|
||||
>
|
||||
{showTitle && title}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
{openList && (
|
||||
|
|
|
@ -3,6 +3,10 @@
|
|||
.vm-shortcuts {
|
||||
min-width: 400px;
|
||||
|
||||
@media (max-width: 500px) {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
&-section {
|
||||
margin-bottom: $padding-medium;
|
||||
|
||||
|
@ -17,12 +21,20 @@
|
|||
display: grid;
|
||||
gap: $padding-global;
|
||||
|
||||
@media (max-width: 500px) {
|
||||
gap: $padding-medium;
|
||||
}
|
||||
|
||||
&-item {
|
||||
display: grid;
|
||||
grid-template-columns: 210px 1fr;
|
||||
align-items: center;
|
||||
gap: $padding-small;
|
||||
|
||||
@media (max-width: 500px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
&__key {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { FC, useEffect, useState } from "preact/compat";
|
||||
import { getContrastColor } from "../../../utils/color";
|
||||
import { getCssVariable, isSystemDark, setCssVariable } from "../../../utils/theme";
|
||||
import { AppParams, getAppModeParams } from "../../../utils/app-mode";
|
||||
import { AppParams, getAppModeEnable, getAppModeParams } from "../../../utils/app-mode";
|
||||
import { getFromStorage } from "../../../utils/storage";
|
||||
import { darkPalette, lightPalette } from "../../../constants/palette";
|
||||
import { Theme } from "../../../types";
|
||||
|
@ -23,6 +23,7 @@ const colorVariables = [
|
|||
|
||||
export const ThemeProvider: FC<ThemeProviderProps> = ({ onLoaded }) => {
|
||||
|
||||
const appModeEnable = getAppModeEnable();
|
||||
const { palette: paletteAppMode = {} } = getAppModeParams();
|
||||
const { theme } = useAppState();
|
||||
const isDarkTheme = useSystemTheme();
|
||||
|
@ -39,6 +40,7 @@ export const ThemeProvider: FC<ThemeProviderProps> = ({ onLoaded }) => {
|
|||
const { clientWidth, clientHeight } = document.documentElement;
|
||||
setCssVariable("scrollbar-width", `${innerWidth - clientWidth}px`);
|
||||
setCssVariable("scrollbar-height", `${innerHeight - clientHeight}px`);
|
||||
setCssVariable("vh", `${innerHeight * 0.01}px`);
|
||||
};
|
||||
|
||||
const setContrastText = () => {
|
||||
|
@ -70,6 +72,8 @@ export const ThemeProvider: FC<ThemeProviderProps> = ({ onLoaded }) => {
|
|||
setCssVariable(variable, value);
|
||||
});
|
||||
setContrastText();
|
||||
|
||||
if (appModeEnable) setAppModePalette();
|
||||
};
|
||||
|
||||
const updatePalette = () => {
|
||||
|
@ -85,13 +89,18 @@ export const ThemeProvider: FC<ThemeProviderProps> = ({ onLoaded }) => {
|
|||
};
|
||||
|
||||
useEffect(() => {
|
||||
setAppModePalette();
|
||||
setScrollbarSize();
|
||||
setTheme();
|
||||
}, [palette]);
|
||||
|
||||
useEffect(updatePalette, [theme, isDarkTheme]);
|
||||
|
||||
useEffect(() => {
|
||||
if (appModeEnable) {
|
||||
dispatch({ type: "SET_THEME", payload: Theme.light });
|
||||
}
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ import ReactDOM from "react-dom";
|
|||
import "./style.scss";
|
||||
import { ReactNode } from "react";
|
||||
import { ExoticComponent } from "react";
|
||||
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||
|
||||
interface TooltipProps {
|
||||
children: ReactNode
|
||||
|
@ -19,6 +20,7 @@ const Tooltip: FC<TooltipProps> = ({
|
|||
placement = "bottom-center",
|
||||
offset = { top: 6, left: 0 }
|
||||
}) => {
|
||||
const { isMobile } = useDeviceDetect();
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [popperSize, setPopperSize] = useState({ width: 0, height: 0 });
|
||||
|
@ -121,7 +123,7 @@ const Tooltip: FC<TooltipProps> = ({
|
|||
{children}
|
||||
</Fragment>
|
||||
|
||||
{isOpen && ReactDOM.createPortal((
|
||||
{!isMobile && isOpen && ReactDOM.createPortal((
|
||||
<div
|
||||
className="vm-tooltip"
|
||||
ref={popperRef}
|
||||
|
|
|
@ -12,6 +12,7 @@ import { getAvgFromArray, getMaxFromArray, getMinFromArray } from "../../../util
|
|||
import classNames from "classnames";
|
||||
import { useTimeState } from "../../../state/time/TimeStateContext";
|
||||
import "./style.scss";
|
||||
import { promValueToNumber } from "../../../utils/metric";
|
||||
|
||||
export interface GraphViewProps {
|
||||
data?: MetricResult[];
|
||||
|
@ -28,21 +29,6 @@ export interface GraphViewProps {
|
|||
height?: number
|
||||
}
|
||||
|
||||
const promValueToNumber = (s: string): number => {
|
||||
// See https://prometheus.io/docs/prometheus/latest/querying/api/#expression-query-result-formats
|
||||
switch (s) {
|
||||
case "NaN":
|
||||
return NaN;
|
||||
case "Inf":
|
||||
case "+Inf":
|
||||
return Infinity;
|
||||
case "-Inf":
|
||||
return -Infinity;
|
||||
default:
|
||||
return parseFloat(s);
|
||||
}
|
||||
};
|
||||
|
||||
const GraphView: FC<GraphViewProps> = ({
|
||||
data = [],
|
||||
period,
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
@use "src/styles/variables" as *;
|
||||
$all-paddings: $padding-medium * 4;
|
||||
|
||||
.vm-graph-view {
|
||||
width: 100%;
|
||||
|
||||
&_full-width {
|
||||
width: calc(100vw - $all-paddings - var(--scrollbar-width));
|
||||
width: calc(100vw - ($padding-medium * 4) - var(--scrollbar-width));
|
||||
|
||||
@media (max-width: 768px) {
|
||||
width: calc(100vw - ($padding-medium * 2) - var(--scrollbar-width));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
16
app/vmui/packages/vmui/src/hooks/useDeviceDetect.ts
Normal file
16
app/vmui/packages/vmui/src/hooks/useDeviceDetect.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { isMobileAgent } from "../utils/detect-device";
|
||||
import useResize from "./useResize";
|
||||
|
||||
export default function useDeviceDetect() {
|
||||
const windowSize = useResize(document.body);
|
||||
const [isMobile, setMobile] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const mobileAgent = isMobileAgent();
|
||||
const smallWidth = window.innerWidth < 500;
|
||||
setMobile(mobileAgent || smallWidth);
|
||||
}, [windowSize]);
|
||||
|
||||
return { isMobile };
|
||||
}
|
|
@ -155,8 +155,11 @@ export const useFetchQuery = ({
|
|||
},
|
||||
[serverUrl, period, displayType, customStep, hideQuery]);
|
||||
|
||||
const [prevUrl, setPrevUrl] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible || !fetchUrl?.length) return;
|
||||
const isLazyPredefined = (fetchUrl === prevUrl && !!predefinedQuery);
|
||||
if (!visible || !fetchUrl?.length || isLazyPredefined) return;
|
||||
setIsLoading(true);
|
||||
const expr = predefinedQuery ?? query;
|
||||
throttledFetchData({
|
||||
|
@ -168,6 +171,7 @@ export const useFetchQuery = ({
|
|||
showAllSeries,
|
||||
hideQuery,
|
||||
});
|
||||
setPrevUrl(fetchUrl);
|
||||
}, [fetchUrl, visible, stateSeriesLimits, showAllSeries]);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -14,7 +14,7 @@ const useResize = (node: HTMLElement | null): {width: number, height: number} =>
|
|||
return () => {
|
||||
if (node) observer.unobserve(node);
|
||||
};
|
||||
}, []);
|
||||
}, [node]);
|
||||
return windowSize;
|
||||
};
|
||||
|
||||
|
|
|
@ -118,24 +118,26 @@ const CardinalityConfigurator: FC<CardinalityConfiguratorProps> = ({
|
|||
at <b>{date}</b>{match && <span> for series selector <b>{match}</b></span>}.
|
||||
Show top {topN} entries per table.
|
||||
</div>
|
||||
<a
|
||||
className="vm-link vm-link_with-icon"
|
||||
target="_blank"
|
||||
href="https://docs.victoriametrics.com/#cardinality-explorer"
|
||||
rel="help noreferrer"
|
||||
>
|
||||
<WikiIcon/>
|
||||
Documentation
|
||||
</a>
|
||||
<a
|
||||
className="vm-link vm-link_with-icon"
|
||||
target="_blank"
|
||||
href="https://victoriametrics.com/blog/cardinality-explorer/"
|
||||
rel="help noreferrer"
|
||||
>
|
||||
<QuestionIcon/>
|
||||
Example of using
|
||||
</a>
|
||||
<div className="vm-cardinality-configurator-bottom__docs">
|
||||
<a
|
||||
className="vm-link vm-link_with-icon"
|
||||
target="_blank"
|
||||
href="https://docs.victoriametrics.com/#cardinality-explorer"
|
||||
rel="help noreferrer"
|
||||
>
|
||||
<WikiIcon/>
|
||||
Documentation
|
||||
</a>
|
||||
<a
|
||||
className="vm-link vm-link_with-icon"
|
||||
target="_blank"
|
||||
href="https://victoriametrics.com/blog/cardinality-explorer/"
|
||||
rel="help noreferrer"
|
||||
>
|
||||
<QuestionIcon/>
|
||||
Example of using
|
||||
</a>
|
||||
</div>
|
||||
<Button
|
||||
startIcon={<PlayIcon/>}
|
||||
onClick={onRunQuery}
|
||||
|
|
|
@ -12,6 +12,10 @@
|
|||
gap: 0 $padding-medium;
|
||||
|
||||
&__query {
|
||||
flex-grow: 8;
|
||||
}
|
||||
|
||||
&__item {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
@ -19,13 +23,21 @@
|
|||
&-additional {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: $padding-small;
|
||||
}
|
||||
|
||||
&-bottom {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: $padding-global;
|
||||
|
||||
&__docs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $padding-global;
|
||||
}
|
||||
|
||||
&__info {
|
||||
flex-grow: 1;
|
||||
font-size: $font-size;
|
||||
|
@ -34,5 +46,9 @@
|
|||
a {
|
||||
color: $color-text-secondary;
|
||||
}
|
||||
|
||||
button {
|
||||
margin: 0 0 0 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -65,7 +65,10 @@ const MetricsContent: FC<MetricsProperties> = ({
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div ref={chartContainer}>
|
||||
<div
|
||||
ref={chartContainer}
|
||||
className="vm-metrics-content__table"
|
||||
>
|
||||
{activeTab === 0 && (
|
||||
<EnhancedTable
|
||||
rows={rows}
|
||||
|
|
|
@ -2,6 +2,20 @@
|
|||
|
||||
.vm-metrics-content {
|
||||
&-header {
|
||||
margin: -$padding-medium 0-$padding-medium $padding-medium;
|
||||
margin: -$padding-medium 0-$padding-medium 0;
|
||||
}
|
||||
|
||||
&__table {
|
||||
padding-top: $padding-medium;
|
||||
width: calc(100vw - ($padding-medium * 4) - var(--scrollbar-width));
|
||||
overflow: auto;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
width: calc(100vw - ($padding-medium * 2) - var(--scrollbar-width));
|
||||
}
|
||||
|
||||
.vm-table-cell_header {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,14 +28,27 @@
|
|||
|
||||
&-settings {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: $padding-medium;
|
||||
|
||||
@media (max-width: 500px) {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
&__buttons {
|
||||
flex-grow: 1;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, auto);
|
||||
gap: $padding-small;
|
||||
justify-content: flex-end;
|
||||
|
||||
@media (max-width: 500px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,6 +40,10 @@
|
|||
gap: $padding-global;
|
||||
padding: 0;
|
||||
|
||||
@media (max-width: 1000px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
&-panel {
|
||||
position: relative;
|
||||
border-radius: $border-radius-medium;
|
||||
|
|
|
@ -32,7 +32,7 @@ const PredefinedPanel: FC<PredefinedPanelsProps> = ({
|
|||
const dispatch = useTimeDispatch();
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [visible, setVisible] = useState(true);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [yaxis, setYaxis] = useState<YaxisState>({
|
||||
limits: {
|
||||
enable: false,
|
||||
|
@ -73,7 +73,7 @@ const PredefinedPanel: FC<PredefinedPanelsProps> = ({
|
|||
return () => {
|
||||
if (containerRef.current) observer.unobserve(containerRef.current);
|
||||
};
|
||||
}, []);
|
||||
}, [containerRef]);
|
||||
|
||||
if (!validExpr) return (
|
||||
<Alert variant="error">
|
||||
|
|
|
@ -37,5 +37,6 @@
|
|||
|
||||
&-body {
|
||||
padding: $padding-small $padding-small*2;
|
||||
min-height: 500px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,7 +26,8 @@ export const useFetchDashboards = (): {
|
|||
const fetchLocalDashboards = async () => {
|
||||
const filenames = window.__VMUI_PREDEFINED_DASHBOARDS__;
|
||||
if (!filenames?.length) return [];
|
||||
return await Promise.all(filenames.map(async f => importModule(f)));
|
||||
const dashboards = await Promise.all(filenames.map(async f => importModule(f)));
|
||||
setDashboards((prevDash) => [...dashboards, ...prevDash]);
|
||||
};
|
||||
|
||||
const fetchRemoteDashboards = async () => {
|
||||
|
@ -45,19 +46,20 @@ export const useFetchDashboards = (): {
|
|||
}
|
||||
setIsLoading(false);
|
||||
} else {
|
||||
await fetchLocalDashboards();
|
||||
setError(resp.error);
|
||||
setIsLoading(false);
|
||||
}
|
||||
} catch (e) {
|
||||
setIsLoading(false);
|
||||
if (e instanceof Error) setError(`${e.name}: ${e.message}`);
|
||||
await fetchLocalDashboards();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (appModeEnable) return;
|
||||
setDashboards([]);
|
||||
fetchLocalDashboards().then(d => d.length && setDashboards((prevDash) => [...d, ...prevDash]));
|
||||
fetchRemoteDashboards();
|
||||
}, [serverUrl]);
|
||||
|
||||
|
|
|
@ -5,6 +5,10 @@
|
|||
gap: $padding-global;
|
||||
align-items: flex-start;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: $padding-medium 0;
|
||||
}
|
||||
|
||||
&-tabs.vm-block {
|
||||
padding: $padding-global;
|
||||
}
|
||||
|
|
|
@ -40,7 +40,7 @@ const TopQueryPanel: FC<TopQueryPanelProps> = ({ rows, title, columns, defaultOr
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="vm-top-queries-panel__table">
|
||||
{activeTab === 0 && (
|
||||
<TopQueryTable
|
||||
rows={rows}
|
||||
|
|
|
@ -2,6 +2,20 @@
|
|||
|
||||
.vm-top-queries-panel {
|
||||
&-header {
|
||||
margin: -$padding-medium 0-$padding-medium $padding-medium;
|
||||
margin: -$padding-medium 0-$padding-medium 0;
|
||||
}
|
||||
|
||||
&__table {
|
||||
padding-top: $padding-medium;
|
||||
width: calc(100vw - ($padding-medium * 4) - var(--scrollbar-width));
|
||||
overflow: auto;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
width: calc(100vw - ($padding-medium * 2) - var(--scrollbar-width));
|
||||
}
|
||||
|
||||
.vm-table-cell_header {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,7 +40,7 @@ const Index: FC = () => {
|
|||
const getQueryStatsTitle = (key: keyof TopQueryStats) => {
|
||||
if (!data) return key;
|
||||
const value = data[key];
|
||||
if (typeof value === "number") return formatPrettyNumber(value);
|
||||
if (typeof value === "number") return formatPrettyNumber(value, value, value);
|
||||
return value || key;
|
||||
};
|
||||
|
||||
|
@ -71,23 +71,27 @@ const Index: FC = () => {
|
|||
{loading && <Spinner containerStyles={{ height: "500px" }}/>}
|
||||
|
||||
<div className="vm-top-queries-controls vm-block">
|
||||
<div className="vm-top-queries-controls__fields">
|
||||
<TextField
|
||||
label="Max lifetime"
|
||||
value={maxLifetime}
|
||||
error={errorMaxLife}
|
||||
helperText={`For example ${exampleDuration}`}
|
||||
onChange={onMaxLifetimeChange}
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
<TextField
|
||||
label="Number of returned queries"
|
||||
type="number"
|
||||
value={topN || ""}
|
||||
error={errorTopN}
|
||||
onChange={onTopNChange}
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
<div className="vm-top-queries-controls-fields">
|
||||
<div className="vm-top-queries-controls-fields__item">
|
||||
<TextField
|
||||
label="Max lifetime"
|
||||
value={maxLifetime}
|
||||
error={errorMaxLife}
|
||||
helperText={`For example ${exampleDuration}`}
|
||||
onChange={onMaxLifetimeChange}
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
</div>
|
||||
<div className="vm-top-queries-controls-fields__item">
|
||||
<TextField
|
||||
label="Number of returned queries"
|
||||
type="number"
|
||||
value={topN || ""}
|
||||
error={errorTopN}
|
||||
onChange={onTopNChange}
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="vm-top-queries-controls-bottom">
|
||||
<div className="vm-top-queries-controls-bottom__info">
|
||||
|
|
|
@ -9,10 +9,16 @@
|
|||
display: grid;
|
||||
gap: $padding-small;
|
||||
|
||||
&__fields {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
&-fields {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: $padding-medium;
|
||||
|
||||
&__item {
|
||||
flex-grow: 1;
|
||||
min-width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
&-bottom {
|
||||
|
|
|
@ -7,6 +7,7 @@ import { ErrorTypes } from "../../../types";
|
|||
import classNames from "classnames";
|
||||
import { useSnack } from "../../../contexts/Snackbar";
|
||||
import { CopyIcon, RestartIcon } from "../../../components/Main/Icons";
|
||||
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||
|
||||
interface JsonFormProps {
|
||||
defaultJson?: string
|
||||
|
@ -28,6 +29,7 @@ const JsonForm: FC<JsonFormProps> = ({
|
|||
onUpload,
|
||||
}) => {
|
||||
const { showInfoMessage } = useSnack();
|
||||
const { isMobile } = useDeviceDetect();
|
||||
|
||||
const [json, setJson] = useState(defaultJson);
|
||||
const [title, setTitle] = useState(defaultTile);
|
||||
|
@ -77,7 +79,8 @@ const JsonForm: FC<JsonFormProps> = ({
|
|||
<div
|
||||
className={classNames({
|
||||
"vm-json-form": true,
|
||||
"vm-json-form_one-field": !displayTitle
|
||||
"vm-json-form_one-field": !displayTitle,
|
||||
"vm-json-form_mobile": isMobile
|
||||
})}
|
||||
>
|
||||
{displayTitle && (
|
||||
|
|
|
@ -2,15 +2,21 @@
|
|||
|
||||
.vm-json-form {
|
||||
display: grid;
|
||||
grid-template-rows: auto calc(70vh - 78px - ($padding-medium*3)) auto;
|
||||
grid-template-rows: auto calc(($vh * 70) - 78px - ($padding-medium*3)) auto;
|
||||
gap: $padding-global;
|
||||
width: 70vw;
|
||||
max-width: 1000px;
|
||||
max-height: 900px;
|
||||
overflow: hidden;
|
||||
|
||||
&_mobile {
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
}
|
||||
|
||||
&_one-field {
|
||||
grid-template-rows: calc(70vh - 78px - ($padding-medium*3)) auto;
|
||||
grid-template-rows: calc(($vh * 70) - 78px - ($padding-medium*3)) auto;
|
||||
}
|
||||
|
||||
.vm-text-field_textarea {
|
||||
|
@ -29,6 +35,14 @@
|
|||
justify-content: space-between;
|
||||
gap: $padding-small;
|
||||
|
||||
@media (max-width: 500px) {
|
||||
flex-direction: column;
|
||||
|
||||
button {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&__controls {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
|
@ -36,10 +50,22 @@
|
|||
justify-content: flex-start;
|
||||
gap: $padding-small;
|
||||
|
||||
@media (max-width: 500px) {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&_right {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 90px);
|
||||
justify-content: flex-end;
|
||||
|
||||
@media (max-width: 500px) {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,9 +3,12 @@
|
|||
.vm-trace-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: $padding-global;
|
||||
min-height: 100%;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: $padding-medium 0;
|
||||
}
|
||||
|
||||
&-controls {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
|
@ -21,6 +24,11 @@
|
|||
gap: $padding-global;
|
||||
margin-bottom: $padding-medium;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
grid-template-columns: 1fr;
|
||||
padding: 0 $padding-medium;
|
||||
}
|
||||
|
||||
&-errors {
|
||||
display: grid;
|
||||
align-items: flex-start;
|
||||
|
@ -28,6 +36,10 @@
|
|||
grid-template-columns: 1fr;
|
||||
gap: $padding-medium;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
&-item {
|
||||
position: relative;
|
||||
display: grid;
|
||||
|
|
|
@ -16,9 +16,6 @@
|
|||
}
|
||||
|
||||
&_header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
&_selected {
|
||||
|
|
|
@ -13,12 +13,13 @@ html, body, #root {
|
|||
}
|
||||
|
||||
body {
|
||||
overflow: scroll;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
* {
|
||||
font: inherit;
|
||||
cursor: inherit;
|
||||
touch-action: pan-x pan-y;
|
||||
}
|
||||
|
||||
code {
|
||||
|
|
|
@ -61,3 +61,4 @@ $box-shadow: var(--box-shadow);
|
|||
$box-shadow-popper: var(--box-shadow-popper);
|
||||
|
||||
$color-hover-black: var(--color-hover-black);
|
||||
$vh: var(--vh);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
export interface AppParams {
|
||||
serverURL?: string
|
||||
inputTenantID?: boolean
|
||||
useTenantID?: boolean
|
||||
headerStyles?: {
|
||||
background?: string
|
||||
color?: string
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue