mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2026-05-17 00:26:36 +03:00
vmui: replace VMAlert proxy with Alerting tab in VMUI (#8989)
### Describe Your Changes Rules page header + content <img width="1235" height="520" alt="image" src="https://github.com/user-attachments/assets/bb0c5818-c44a-46e6-bc47-e6718be34016" /> Expanded rule without alert <img width="1418" alt="image" src="https://github.com/user-attachments/assets/ae0b265f-24fe-4549-8913-b1be8e7c2862" /> Expanded rule with alert <img width="1418" alt="image" src="https://github.com/user-attachments/assets/8a138403-0712-4de2-bfa5-467da3a979dd" /> Notifiers page <img width="1419" alt="image" src="https://github.com/user-attachments/assets/557c2831-e960-44ec-9b93-f1ebfeb1fbb0" /> fixes https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8330 fixes https://github.com/VictoriaMetrics/VictoriaMetrics/issues/6091 fixes https://github.com/VictoriaMetrics/VictoriaLogs/issues/90 VMUI: - Added added `Alerting -> Rules` and `Alerting -> Notifiers` pages for VictoriaMetrics - Support includeAll option in Select component VMAlert: - added `/api/v1/group`useful to get information about certain group - added `lastError` for `/api/v1/notifiers` for each target to see information about failed notifiers ### Checklist The following checks are **mandatory**: - [ ] My change adheres to [VictoriaMetrics contributing guidelines](https://docs.victoriametrics.com/victoriametrics/contributing/). --------- Signed-off-by: hagen1778 <roman@victoriametrics.com> Co-authored-by: hagen1778 <roman@victoriametrics.com>
This commit is contained in:
@@ -29,6 +29,18 @@ type manager struct {
|
||||
groups map[uint64]*rule.Group
|
||||
}
|
||||
|
||||
// groupAPI generates apiGroup object from group by its ID(hash)
|
||||
func (m *manager) groupAPI(gID uint64) (*apiGroup, error) {
|
||||
m.groupsMu.RLock()
|
||||
defer m.groupsMu.RUnlock()
|
||||
|
||||
g, ok := m.groups[gID]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("can't find group with id %d", gID)
|
||||
}
|
||||
return groupToAPI(g), nil
|
||||
}
|
||||
|
||||
// ruleAPI generates apiRule object from alert by its ID(hash)
|
||||
func (m *manager) ruleAPI(gID, rID uint64) (apiRule, error) {
|
||||
m.groupsMu.RLock()
|
||||
|
||||
@@ -22,10 +22,11 @@ import (
|
||||
// AlertManager represents integration provider with Prometheus alert manager
|
||||
// https://github.com/prometheus/alertmanager
|
||||
type AlertManager struct {
|
||||
addr *url.URL
|
||||
argFunc AlertURLGenerator
|
||||
client *http.Client
|
||||
timeout time.Duration
|
||||
addr *url.URL
|
||||
argFunc AlertURLGenerator
|
||||
client *http.Client
|
||||
timeout time.Duration
|
||||
lastError string
|
||||
|
||||
authCfg *promauth.Config
|
||||
// stores already parsed RelabelConfigs object
|
||||
@@ -71,6 +72,10 @@ func (am AlertManager) Addr() string {
|
||||
return am.addr.Redacted()
|
||||
}
|
||||
|
||||
func (am *AlertManager) LastError() string {
|
||||
return am.lastError
|
||||
}
|
||||
|
||||
// Send an alert or resolve message
|
||||
func (am *AlertManager) Send(ctx context.Context, alerts []Alert, headers map[string]string) error {
|
||||
am.metrics.alertsSent.Add(len(alerts))
|
||||
@@ -79,6 +84,9 @@ func (am *AlertManager) Send(ctx context.Context, alerts []Alert, headers map[st
|
||||
am.metrics.alertsSendDuration.UpdateDuration(startTime)
|
||||
if err != nil {
|
||||
am.metrics.alertsSendErrors.Add(len(alerts))
|
||||
am.lastError = err.Error()
|
||||
} else {
|
||||
am.lastError = ""
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -18,6 +18,11 @@ type FakeNotifier struct {
|
||||
// Close does nothing
|
||||
func (*FakeNotifier) Close() {}
|
||||
|
||||
// LastError returns last error message
|
||||
func (*FakeNotifier) LastError() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Addr returns ""
|
||||
func (*FakeNotifier) Addr() string { return "" }
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@ type Notifier interface {
|
||||
Send(ctx context.Context, alerts []Alert, notifierHeaders map[string]string) error
|
||||
// Addr returns address where alerts are sent.
|
||||
Addr() string
|
||||
// LastError returns error, that occured during last attempt to send data
|
||||
LastError() string
|
||||
// Close is a destructor for the Notifier
|
||||
Close()
|
||||
}
|
||||
|
||||
@@ -25,6 +25,11 @@ func (bh *blackHoleNotifier) Close() {
|
||||
bh.metrics.close()
|
||||
}
|
||||
|
||||
// LastError return last notifier's error
|
||||
func (bh *blackHoleNotifier) LastError() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// newBlackHoleNotifier creates a new blackHoleNotifier
|
||||
func newBlackHoleNotifier() *blackHoleNotifier {
|
||||
address := "blackhole"
|
||||
|
||||
@@ -30,6 +30,8 @@ var (
|
||||
{"api/v1/alerts", "list all active alerts"},
|
||||
{"api/v1/notifiers", "list all notifiers"},
|
||||
{fmt.Sprintf("api/v1/alert?%s=<int>&%s=<int>", paramGroupID, paramAlertID), "get alert status by group and alert ID"},
|
||||
{fmt.Sprintf("api/v1/rule?%s=<int>&%s=<int>", paramGroupID, paramRuleID), "get rule status by group and rule ID"},
|
||||
{fmt.Sprintf("api/v1/group?%s=<int>", paramGroupID), "get group status by group ID"},
|
||||
}
|
||||
systemLinks = [][2]string{
|
||||
{"vmalert/groups", "UI"},
|
||||
@@ -195,6 +197,20 @@ func (rh *requestHandler) handler(w http.ResponseWriter, r *http.Request) bool {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(data)
|
||||
return true
|
||||
case "/vmalert/api/v1/group", "/api/v1/group":
|
||||
group, err := rh.getGroup(r)
|
||||
if err != nil {
|
||||
httpserver.Errorf(w, r, "%s", err)
|
||||
return true
|
||||
}
|
||||
data, err := json.Marshal(group)
|
||||
if err != nil {
|
||||
httpserver.Errorf(w, r, "failed to marshal group: %s", err)
|
||||
return true
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(data)
|
||||
return true
|
||||
case "/-/reload":
|
||||
if !httpserver.CheckAuthFlag(w, r, reloadAuthKey) {
|
||||
return true
|
||||
@@ -209,6 +225,18 @@ func (rh *requestHandler) handler(w http.ResponseWriter, r *http.Request) bool {
|
||||
}
|
||||
}
|
||||
|
||||
func (rh *requestHandler) getGroup(r *http.Request) (*apiGroup, error) {
|
||||
groupID, err := strconv.ParseUint(r.FormValue(paramGroupID), 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read %q param: %w", paramGroupID, err)
|
||||
}
|
||||
obj, err := rh.m.groupAPI(groupID)
|
||||
if err != nil {
|
||||
return nil, errResponse(err, http.StatusNotFound)
|
||||
}
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
func (rh *requestHandler) getRule(r *http.Request) (apiRule, error) {
|
||||
groupID, err := strconv.ParseUint(r.FormValue(paramGroupID), 10, 64)
|
||||
if err != nil {
|
||||
@@ -337,12 +365,12 @@ func (rh *requestHandler) groups(rf *rulesFilter) []*apiGroup {
|
||||
rule.Alerts = nil
|
||||
}
|
||||
if rule.LastError != "" {
|
||||
g.Unhealthy++
|
||||
g.unhealthy++
|
||||
} else {
|
||||
g.Healthy++
|
||||
g.healthy++
|
||||
}
|
||||
if isNoMatch(rule) {
|
||||
g.NoMatch++
|
||||
g.noMatch++
|
||||
}
|
||||
filteredRules = append(filteredRules, rule)
|
||||
}
|
||||
@@ -459,8 +487,9 @@ func (rh *requestHandler) listNotifiers() ([]byte, error) {
|
||||
}
|
||||
for _, target := range protoTargets {
|
||||
notifier.Targets = append(notifier.Targets, &apiTarget{
|
||||
Address: target.Addr(),
|
||||
Labels: target.Labels.ToMap(),
|
||||
Address: target.Addr(),
|
||||
Labels: target.Labels.ToMap(),
|
||||
LastError: target.LastError(),
|
||||
})
|
||||
}
|
||||
lr.Data.Notifiers = append(lr.Data.Notifiers, notifier)
|
||||
|
||||
@@ -113,7 +113,7 @@
|
||||
{%= Controls(prefix, currentIcon, currentText, icons, filters, true) %}
|
||||
{% if len(groups) > 0 %}
|
||||
{% for _, g := range groups %}
|
||||
<div id="group-{%s g.ID %}" class="d-flex w-100 border-0 flex-column group-items{% if g.Unhealthy > 0 %} alert-danger{% endif %}">
|
||||
<div id="group-{%s g.ID %}" class="d-flex w-100 border-0 flex-column group-items{% if g.unhealthy > 0 %} alert-danger{% endif %}">
|
||||
<span class="d-flex justify-content-between">
|
||||
<a href="#group-{%s g.ID %}">{%s g.Name %}{% if g.Type != "prometheus" %} ({%s g.Type %}){% endif %} (every {%f.0 g.Interval %}s) #</a>
|
||||
<span
|
||||
@@ -123,9 +123,9 @@
|
||||
data-bs-target="#sub-{%s g.ID %}"
|
||||
>
|
||||
<span class="d-flex gap-2">
|
||||
{% if g.Unhealthy > 0 %}<span class="badge bg-danger" title="Number of rules with status Error">{%d g.Unhealthy %}</span> {% endif %}
|
||||
{% if g.NoMatch > 0 %}<span class="badge bg-warning" title="Number of rules with status NoMatch">{%d g.NoMatch %}</span> {% endif %}
|
||||
<span class="badge bg-success" title="Number of rules with status Ok">{%d g.Healthy %}</span>
|
||||
{% if g.unhealthy > 0 %}<span class="badge bg-danger" title="Number of rules with status Error">{%d g.unhealthy %}</span> {% endif %}
|
||||
{% if g.noMatch > 0 %}<span class="badge bg-warning" title="Number of rules with status NoMatch">{%d g.noMatch %}</span> {% endif %}
|
||||
<span class="badge bg-success" title="Number of rules with status Ok">{%d g.healthy %}</span>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
@@ -363,7 +363,7 @@ func StreamListGroups(qw422016 *qt422016.Writer, r *http.Request, groups []*apiG
|
||||
//line app/vmalert/web.qtpl:116
|
||||
qw422016.N().S(`" class="d-flex w-100 border-0 flex-column group-items`)
|
||||
//line app/vmalert/web.qtpl:116
|
||||
if g.Unhealthy > 0 {
|
||||
if g.unhealthy > 0 {
|
||||
//line app/vmalert/web.qtpl:116
|
||||
qw422016.N().S(` alert-danger`)
|
||||
//line app/vmalert/web.qtpl:116
|
||||
@@ -407,11 +407,11 @@ func StreamListGroups(qw422016 *qt422016.Writer, r *http.Request, groups []*apiG
|
||||
<span class="d-flex gap-2">
|
||||
`)
|
||||
//line app/vmalert/web.qtpl:126
|
||||
if g.Unhealthy > 0 {
|
||||
if g.unhealthy > 0 {
|
||||
//line app/vmalert/web.qtpl:126
|
||||
qw422016.N().S(`<span class="badge bg-danger" title="Number of rules with status Error">`)
|
||||
//line app/vmalert/web.qtpl:126
|
||||
qw422016.N().D(g.Unhealthy)
|
||||
qw422016.N().D(g.unhealthy)
|
||||
//line app/vmalert/web.qtpl:126
|
||||
qw422016.N().S(`</span> `)
|
||||
//line app/vmalert/web.qtpl:126
|
||||
@@ -420,11 +420,11 @@ func StreamListGroups(qw422016 *qt422016.Writer, r *http.Request, groups []*apiG
|
||||
qw422016.N().S(`
|
||||
`)
|
||||
//line app/vmalert/web.qtpl:127
|
||||
if g.NoMatch > 0 {
|
||||
if g.noMatch > 0 {
|
||||
//line app/vmalert/web.qtpl:127
|
||||
qw422016.N().S(`<span class="badge bg-warning" title="Number of rules with status NoMatch">`)
|
||||
//line app/vmalert/web.qtpl:127
|
||||
qw422016.N().D(g.NoMatch)
|
||||
qw422016.N().D(g.noMatch)
|
||||
//line app/vmalert/web.qtpl:127
|
||||
qw422016.N().S(`</span> `)
|
||||
//line app/vmalert/web.qtpl:127
|
||||
@@ -433,7 +433,7 @@ func StreamListGroups(qw422016 *qt422016.Writer, r *http.Request, groups []*apiG
|
||||
qw422016.N().S(`
|
||||
<span class="badge bg-success" title="Number of rules with status Ok">`)
|
||||
//line app/vmalert/web.qtpl:128
|
||||
qw422016.N().D(g.Healthy)
|
||||
qw422016.N().D(g.healthy)
|
||||
//line app/vmalert/web.qtpl:128
|
||||
qw422016.N().S(`</span>
|
||||
</span>
|
||||
|
||||
@@ -25,6 +25,7 @@ func TestHandler(t *testing.T) {
|
||||
m := &manager{groups: map[uint64]*rule.Group{}}
|
||||
var ar *rule.AlertingRule
|
||||
var rr *rule.RecordingRule
|
||||
var groupIDs []uint64
|
||||
for _, dsType := range []string{"prometheus", "", "graphite"} {
|
||||
g := rule.NewGroup(config.Group{
|
||||
Name: "group",
|
||||
@@ -45,7 +46,9 @@ func TestHandler(t *testing.T) {
|
||||
ar = g.Rules[0].(*rule.AlertingRule)
|
||||
rr = g.Rules[1].(*rule.RecordingRule)
|
||||
g.ExecOnce(context.Background(), func() []notifier.Notifier { return nil }, nil, time.Time{})
|
||||
m.groups[g.CreateID()] = g
|
||||
id := g.CreateID()
|
||||
m.groups[id] = g
|
||||
groupIDs = append(groupIDs, id)
|
||||
}
|
||||
rh := &requestHandler{m: m}
|
||||
|
||||
@@ -188,6 +191,21 @@ func TestHandler(t *testing.T) {
|
||||
t.Fatalf("expected %+v to have state updates field not empty", gotRuleWithUpdates.StateUpdates)
|
||||
}
|
||||
})
|
||||
t.Run("/api/v1/group?groupID", func(t *testing.T) {
|
||||
id := groupIDs[0]
|
||||
g := m.groups[id]
|
||||
expGroup := groupToAPI(g)
|
||||
gotGroup := apiGroup{}
|
||||
getResp(t, ts.URL+"/"+expGroup.APILink(), &gotGroup, 200)
|
||||
if expGroup.ID != gotGroup.ID {
|
||||
t.Fatalf("expected to get Group %q; got %q instead", expGroup.ID, gotGroup.ID)
|
||||
}
|
||||
gotGroup = apiGroup{}
|
||||
getResp(t, ts.URL+"/vmalert/"+expGroup.APILink(), &gotGroup, 200)
|
||||
if expGroup.ID != gotGroup.ID {
|
||||
t.Fatalf("expected to get Group %q; got %q instead", expGroup.ID, gotGroup.ID)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("/api/v1/rules&filters", func(t *testing.T) {
|
||||
check := func(url string, statusCode, expGroups, expRules int) {
|
||||
|
||||
@@ -28,6 +28,8 @@ type apiNotifier struct {
|
||||
type apiTarget struct {
|
||||
Address string `json:"address"`
|
||||
Labels map[string]string `json:"labels"`
|
||||
// LastError contains the error faced while sending to notifier.
|
||||
LastError string `json:"lastError"`
|
||||
}
|
||||
|
||||
// apiAlert represents a notifier.AlertingRule state
|
||||
@@ -109,11 +111,16 @@ type apiGroup struct {
|
||||
// EvalDelay will adjust the `time` parameter of rule evaluation requests to compensate intentional query delay from datasource.
|
||||
EvalDelay float64 `json:"eval_delay,omitempty"`
|
||||
// Unhealthy unhealthy rules count
|
||||
Unhealthy int
|
||||
unhealthy int
|
||||
// Healthy passing rules count
|
||||
Healthy int
|
||||
healthy int
|
||||
// NoMatch not matching rules count
|
||||
NoMatch int
|
||||
noMatch int
|
||||
}
|
||||
|
||||
// APILink returns a link to the group's JSON representation.
|
||||
func (ag *apiGroup) APILink() string {
|
||||
return fmt.Sprintf("api/v1/group?%s=%s", paramGroupID, ag.ID)
|
||||
}
|
||||
|
||||
// groupAlerts represents a group of alerts for WEB view
|
||||
|
||||
1
app/vmselect/vmui/assets/index-B7vIex3g.css
Normal file
1
app/vmselect/vmui/assets/index-B7vIex3g.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
205
app/vmselect/vmui/assets/index-SqjehVXD.js
Normal file
205
app/vmselect/vmui/assets/index-SqjehVXD.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -36,10 +36,10 @@
|
||||
<meta property="og:title" content="UI for VictoriaMetrics">
|
||||
<meta property="og:url" content="https://victoriametrics.com/">
|
||||
<meta property="og:description" content="Explore and troubleshoot your VictoriaMetrics data">
|
||||
<script type="module" crossorigin src="./assets/index-Ck5nH8JI.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="./assets/vendor-BVRvRxZ2.js">
|
||||
<script type="module" crossorigin src="./assets/index-SqjehVXD.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="./assets/vendor-DBOs1yKE.js">
|
||||
<link rel="stylesheet" crossorigin href="./assets/vendor-D1GxaB_c.css">
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-BHg4iVVe.css">
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-B7vIex3g.css">
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:20-alpine3.19
|
||||
FROM node:22-alpine3.22
|
||||
|
||||
# Sets a custom location for the npm cache, preventing access errors in system directories
|
||||
ENV NPM_CONFIG_CACHE=/build/.npm
|
||||
|
||||
489
app/vmui/packages/vmui/package-lock.json
generated
489
app/vmui/packages/vmui/package-lock.json
generated
@@ -1177,7 +1177,7 @@
|
||||
"version": "0.3.12",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz",
|
||||
"integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||
@@ -1188,24 +1188,36 @@
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/source-map": {
|
||||
"version": "0.3.10",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.10.tgz",
|
||||
"integrity": "sha512-0pPkgz9dY+bijgistcTTJ5mR+ocqRXLuhXHYdzoMmmoJ2C9S46RCm2GMUbatPEUK9Yjy26IrAy8D/M00lLkv+Q==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
"@jridgewell/trace-mapping": "^0.3.25"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz",
|
||||
"integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.29",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz",
|
||||
"integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.1.0",
|
||||
@@ -1250,6 +1262,316 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz",
|
||||
"integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"detect-libc": "^1.0.3",
|
||||
"is-glob": "^4.0.3",
|
||||
"micromatch": "^4.0.5",
|
||||
"node-addon-api": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@parcel/watcher-android-arm64": "2.5.1",
|
||||
"@parcel/watcher-darwin-arm64": "2.5.1",
|
||||
"@parcel/watcher-darwin-x64": "2.5.1",
|
||||
"@parcel/watcher-freebsd-x64": "2.5.1",
|
||||
"@parcel/watcher-linux-arm-glibc": "2.5.1",
|
||||
"@parcel/watcher-linux-arm-musl": "2.5.1",
|
||||
"@parcel/watcher-linux-arm64-glibc": "2.5.1",
|
||||
"@parcel/watcher-linux-arm64-musl": "2.5.1",
|
||||
"@parcel/watcher-linux-x64-glibc": "2.5.1",
|
||||
"@parcel/watcher-linux-x64-musl": "2.5.1",
|
||||
"@parcel/watcher-win32-arm64": "2.5.1",
|
||||
"@parcel/watcher-win32-ia32": "2.5.1",
|
||||
"@parcel/watcher-win32-x64": "2.5.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-android-arm64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz",
|
||||
"integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-darwin-arm64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz",
|
||||
"integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-darwin-x64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz",
|
||||
"integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-freebsd-x64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz",
|
||||
"integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-arm-glibc": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz",
|
||||
"integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-arm-musl": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz",
|
||||
"integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-arm64-glibc": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz",
|
||||
"integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-arm64-musl": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz",
|
||||
"integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-x64-glibc": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz",
|
||||
"integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-x64-musl": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz",
|
||||
"integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-win32-arm64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz",
|
||||
"integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-win32-ia32": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz",
|
||||
"integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-win32-x64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz",
|
||||
"integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@preact/preset-vite": {
|
||||
"version": "2.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@preact/preset-vite/-/preset-vite-2.10.2.tgz",
|
||||
@@ -1750,9 +2072,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "24.0.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.12.tgz",
|
||||
"integrity": "sha512-LtOrbvDf5ndC9Xi+4QZjVL0woFymF/xSTKZKPgrrl7H7XoeDvnD+E2IclKVDyaK9UM756W/3BXqSU+JEHopA9g==",
|
||||
"version": "24.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz",
|
||||
"integrity": "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2221,7 +2543,7 @@
|
||||
"version": "8.15.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
@@ -2523,7 +2845,7 @@
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fill-range": "^7.1.1"
|
||||
@@ -2572,6 +2894,14 @@
|
||||
"devOptional": true,
|
||||
"license": "MIT/X11"
|
||||
},
|
||||
"node_modules/buffer-from": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/cac": {
|
||||
"version": "6.7.14",
|
||||
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
|
||||
@@ -2702,6 +3032,23 @@
|
||||
"node": ">= 16"
|
||||
}
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
||||
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"readdirp": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.16.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/classnames": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
|
||||
@@ -2750,6 +3097,14 @@
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
@@ -3060,6 +3415,20 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
|
||||
"integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"detect-libc": "bin/detect-libc.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/doctrine": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
|
||||
@@ -3808,7 +4177,7 @@
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
@@ -4522,7 +4891,7 @@
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -4577,7 +4946,7 @@
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-extglob": "^2.1.1"
|
||||
@@ -4616,7 +4985,7 @@
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.12.0"
|
||||
@@ -5119,7 +5488,7 @@
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"braces": "^3.0.3",
|
||||
@@ -5184,6 +5553,14 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-addon-api": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
|
||||
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/node-html-parser": {
|
||||
"version": "6.1.13",
|
||||
"resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-6.1.13.tgz",
|
||||
@@ -5512,7 +5889,7 @@
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
@@ -5754,6 +6131,21 @@
|
||||
"react-dom": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||||
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 14.18.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "individual",
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/redent": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
|
||||
@@ -6054,6 +6446,28 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sass": {
|
||||
"version": "1.89.2",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.89.2.tgz",
|
||||
"integrity": "sha512-xCmtksBKd/jdJ9Bt9p7nPKiuqrlBMBuuGkQlkhZjjQk3Ty48lv93k5Dq6OPkKt4XwxDJ7tvlfrTa1MPA9bf+QA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"chokidar": "^4.0.0",
|
||||
"immutable": "^5.0.2",
|
||||
"source-map-js": ">=0.6.2 <2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"sass": "sass.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@parcel/watcher": "^2.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/sass-embedded": {
|
||||
"version": "1.89.2",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.89.2.tgz",
|
||||
@@ -6583,6 +6997,29 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-support": {
|
||||
"version": "0.5.21",
|
||||
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
|
||||
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"buffer-from": "^1.0.0",
|
||||
"source-map": "^0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-support/node_modules/source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"license": "BSD-3-Clause",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/stack-trace": {
|
||||
"version": "1.0.0-pre2",
|
||||
"resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-1.0.0-pre2.tgz",
|
||||
@@ -6849,6 +7286,26 @@
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/terser": {
|
||||
"version": "5.43.1",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.43.1.tgz",
|
||||
"integrity": "sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==",
|
||||
"license": "BSD-2-Clause",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/source-map": "^0.3.3",
|
||||
"acorn": "^8.14.0",
|
||||
"commander": "^2.20.0",
|
||||
"source-map-support": "~0.5.20"
|
||||
},
|
||||
"bin": {
|
||||
"terser": "bin/terser"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/tinybench": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
||||
@@ -6959,7 +7416,7 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-number": "^7.0.0"
|
||||
|
||||
@@ -18,84 +18,96 @@ import QueryAnalyzer from "./pages/QueryAnalyzer";
|
||||
import DownsamplingFilters from "./pages/DownsamplingFilters";
|
||||
import RetentionFilters from "./pages/RetentionFilters";
|
||||
import RawQueryPage from "./pages/RawQueryPage";
|
||||
import ExploreRules from "./pages/ExploreAlerts/ExploreRules";
|
||||
import ExploreNotifiers from "./pages/ExploreAlerts/ExploreNotifiers";
|
||||
|
||||
const App: FC = () => {
|
||||
const [loadedTheme, setLoadedTheme] = useState(false);
|
||||
|
||||
return <>
|
||||
<HashRouter>
|
||||
<AppContextProvider>
|
||||
<>
|
||||
<ThemeProvider onLoaded={setLoadedTheme}/>
|
||||
{loadedTheme && (
|
||||
<Routes>
|
||||
<Route
|
||||
path={"/"}
|
||||
element={<MainLayout/>}
|
||||
>
|
||||
return (
|
||||
<>
|
||||
<HashRouter>
|
||||
<AppContextProvider>
|
||||
<>
|
||||
<ThemeProvider onLoaded={setLoadedTheme} />
|
||||
{loadedTheme && (
|
||||
<Routes>
|
||||
<Route
|
||||
path={router.home}
|
||||
element={<CustomPanel/>}
|
||||
/>
|
||||
<Route
|
||||
path={router.rawQuery}
|
||||
element={<RawQueryPage/>}
|
||||
/>
|
||||
<Route
|
||||
path={router.metrics}
|
||||
element={<ExploreMetrics/>}
|
||||
/>
|
||||
<Route
|
||||
path={router.cardinality}
|
||||
element={<CardinalityPanel/>}
|
||||
/>
|
||||
<Route
|
||||
path={router.topQueries}
|
||||
element={<TopQueries/>}
|
||||
/>
|
||||
<Route
|
||||
path={router.trace}
|
||||
element={<TracePage/>}
|
||||
/>
|
||||
<Route
|
||||
path={router.queryAnalyzer}
|
||||
element={<QueryAnalyzer/>}
|
||||
/>
|
||||
<Route
|
||||
path={router.dashboards}
|
||||
element={<DashboardsLayout/>}
|
||||
/>
|
||||
<Route
|
||||
path={router.withTemplate}
|
||||
element={<WithTemplate/>}
|
||||
/>
|
||||
<Route
|
||||
path={router.relabel}
|
||||
element={<Relabel/>}
|
||||
/>
|
||||
<Route
|
||||
path={router.activeQueries}
|
||||
element={<ActiveQueries/>}
|
||||
/>
|
||||
<Route
|
||||
path={router.icons}
|
||||
element={<PreviewIcons/>}
|
||||
/>
|
||||
<Route
|
||||
path={router.downsamplingDebug}
|
||||
element={<DownsamplingFilters/>}
|
||||
/>
|
||||
<Route
|
||||
path={router.retentionDebug}
|
||||
element={<RetentionFilters/>}
|
||||
/>
|
||||
</Route>
|
||||
</Routes>
|
||||
)}
|
||||
</>
|
||||
</AppContextProvider>
|
||||
</HashRouter>
|
||||
</>;
|
||||
path={"/"}
|
||||
element={<MainLayout />}
|
||||
>
|
||||
<Route
|
||||
path={router.home}
|
||||
element={<CustomPanel />}
|
||||
/>
|
||||
<Route
|
||||
path={router.rawQuery}
|
||||
element={<RawQueryPage />}
|
||||
/>
|
||||
<Route
|
||||
path={router.metrics}
|
||||
element={<ExploreMetrics />}
|
||||
/>
|
||||
<Route
|
||||
path={router.cardinality}
|
||||
element={<CardinalityPanel />}
|
||||
/>
|
||||
<Route
|
||||
path={router.topQueries}
|
||||
element={<TopQueries />}
|
||||
/>
|
||||
<Route
|
||||
path={router.trace}
|
||||
element={<TracePage />}
|
||||
/>
|
||||
<Route
|
||||
path={router.queryAnalyzer}
|
||||
element={<QueryAnalyzer />}
|
||||
/>
|
||||
<Route
|
||||
path={router.dashboards}
|
||||
element={<DashboardsLayout />}
|
||||
/>
|
||||
<Route
|
||||
path={router.withTemplate}
|
||||
element={<WithTemplate />}
|
||||
/>
|
||||
<Route
|
||||
path={router.relabel}
|
||||
element={<Relabel />}
|
||||
/>
|
||||
<Route
|
||||
path={router.activeQueries}
|
||||
element={<ActiveQueries />}
|
||||
/>
|
||||
<Route
|
||||
path={router.icons}
|
||||
element={<PreviewIcons />}
|
||||
/>
|
||||
<Route
|
||||
path={router.downsamplingDebug}
|
||||
element={<DownsamplingFilters />}
|
||||
/>
|
||||
<Route
|
||||
path={router.retentionDebug}
|
||||
element={<RetentionFilters />}
|
||||
/>
|
||||
<Route
|
||||
path={router.rules}
|
||||
element={<ExploreRules />}
|
||||
/>
|
||||
<Route
|
||||
path={router.notifiers}
|
||||
element={<ExploreNotifiers />}
|
||||
/>
|
||||
</Route>
|
||||
</Routes>
|
||||
)}
|
||||
</>
|
||||
</AppContextProvider>
|
||||
</HashRouter>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export const getAccountIds = (server: string) =>
|
||||
`${server.replace(/^(.+)(\/select.+)/, "$1")}/admin/tenants`;
|
||||
import { getUrlWithoutTenant } from "../utils/tenants";
|
||||
export const getAccountIds = (server: string) => `${getUrlWithoutTenant(server)}/admin/tenants`;
|
||||
|
||||
23
app/vmui/packages/vmui/src/api/explore-alerts.ts
Normal file
23
app/vmui/packages/vmui/src/api/explore-alerts.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
export const getGroupsUrl = (server: string): string => {
|
||||
return `${server}/vmalert/api/v1/rules?datasource_type=prometheus`;
|
||||
};
|
||||
|
||||
export const getItemUrl = (
|
||||
server: string,
|
||||
groupId: string,
|
||||
id: string,
|
||||
mode: string,
|
||||
): string => {
|
||||
return `${server}/vmalert/api/v1/${mode}?group_id=${groupId}&${mode}_id=${id}`;
|
||||
};
|
||||
|
||||
export const getGroupUrl = (
|
||||
server: string,
|
||||
id: string,
|
||||
): string => {
|
||||
return `${server}/vmalert/api/v1/group?group_id=${id}`;
|
||||
};
|
||||
|
||||
export const getNotifiersUrl = (server: string): string => {
|
||||
return `${server}/vmalert/api/v1/notifiers`;
|
||||
};
|
||||
@@ -30,7 +30,13 @@ const delayOptions: AutoRefreshOption[] = [
|
||||
{ seconds: 7200, title: "2h" }
|
||||
];
|
||||
|
||||
export const ExecutionControls: FC = () => {
|
||||
interface ExecutionControlsProps {
|
||||
tooltip: string;
|
||||
useAutorefresh?: boolean;
|
||||
closeModal: () => void;
|
||||
}
|
||||
|
||||
export const ExecutionControls: FC<ExecutionControlsProps> = ({ tooltip, useAutorefresh, closeModal }) => {
|
||||
const { isMobile } = useDeviceDetect();
|
||||
|
||||
const dispatch = useTimeDispatch();
|
||||
@@ -56,6 +62,9 @@ export const ExecutionControls: FC = () => {
|
||||
|
||||
const handleUpdate = () => {
|
||||
dispatch({ type: "RUN_QUERY" });
|
||||
if (!useAutorefresh && isMobile) {
|
||||
closeModal();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -77,91 +86,118 @@ export const ExecutionControls: FC = () => {
|
||||
handleChange(d);
|
||||
};
|
||||
|
||||
return <>
|
||||
<div className="vm-execution-controls">
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-execution-controls-buttons": true,
|
||||
"vm-execution-controls-buttons_mobile": isMobile,
|
||||
"vm-header-button": !appModeEnable,
|
||||
})}
|
||||
>
|
||||
{!isMobile && (
|
||||
<Tooltip title="Refresh dashboard">
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleUpdate}
|
||||
startIcon={<RefreshIcon/>}
|
||||
ariaLabel="refresh dashboard"
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
{isMobile ? (
|
||||
<div
|
||||
className="vm-mobile-option"
|
||||
onClick={toggleOpenOptions}
|
||||
>
|
||||
<span className="vm-mobile-option__icon"><RestartIcon/></span>
|
||||
<div className="vm-mobile-option-text">
|
||||
<span className="vm-mobile-option-text__label">Auto-refresh</span>
|
||||
<span className="vm-mobile-option-text__value">{selectedDelay.title}</span>
|
||||
</div>
|
||||
<span className="vm-mobile-option__arrow"><ArrowDownIcon/></span>
|
||||
</div>
|
||||
) : (
|
||||
<Tooltip title="Auto-refresh control">
|
||||
<div ref={optionsButtonRef}>
|
||||
return (
|
||||
<>
|
||||
<div className="vm-execution-controls">
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-execution-controls-buttons": true,
|
||||
"vm-execution-controls-buttons_mobile": isMobile,
|
||||
"vm-header-button": !appModeEnable,
|
||||
"vm-autorefresh": useAutorefresh,
|
||||
})}
|
||||
>
|
||||
{useAutorefresh ? (
|
||||
isMobile ? (
|
||||
<div
|
||||
className="vm-mobile-option"
|
||||
onClick={toggleOpenOptions}
|
||||
>
|
||||
<span className="vm-mobile-option__icon"><RestartIcon/></span>
|
||||
<div className="vm-mobile-option-text">
|
||||
<span className="vm-mobile-option-text__label">Auto-refresh</span>
|
||||
<span className="vm-mobile-option-text__value">{selectedDelay.title}</span>
|
||||
</div>
|
||||
<span className="vm-mobile-option__arrow"><ArrowDownIcon/></span>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<Tooltip title={tooltip}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleUpdate}
|
||||
startIcon={<RefreshIcon/>}
|
||||
ariaLabel={tooltip}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="Auto-refresh control">
|
||||
<div ref={optionsButtonRef}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
fullWidth
|
||||
endIcon={(
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-execution-controls-buttons__arrow": true,
|
||||
"vm-execution-controls-buttons__arrow_open": openOptions,
|
||||
})}
|
||||
>
|
||||
<ArrowDownIcon/>
|
||||
</div>
|
||||
)}
|
||||
onClick={toggleOpenOptions}
|
||||
>
|
||||
{selectedDelay.title}
|
||||
</Button>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</>
|
||||
)
|
||||
) : (
|
||||
isMobile ? (
|
||||
<div
|
||||
className="vm-mobile-option"
|
||||
onClick={handleUpdate}
|
||||
>
|
||||
<span className="vm-mobile-option__icon"><RestartIcon/></span>
|
||||
<div className="vm-mobile-option-text">
|
||||
<span className="vm-mobile-option-text__label">Refresh</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
fullWidth
|
||||
endIcon={(
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-execution-controls-buttons__arrow": true,
|
||||
"vm-execution-controls-buttons__arrow_open": openOptions,
|
||||
})}
|
||||
>
|
||||
<ArrowDownIcon/>
|
||||
</div>
|
||||
)}
|
||||
onClick={toggleOpenOptions}
|
||||
>
|
||||
{selectedDelay.title}
|
||||
</Button>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
onClick={handleUpdate}
|
||||
startIcon={<RefreshIcon/>}
|
||||
ariaLabel={tooltip}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Popper
|
||||
open={openOptions}
|
||||
placement="bottom-right"
|
||||
onClose={handleCloseOptions}
|
||||
buttonRef={optionsButtonRef}
|
||||
title={isMobile ? "Auto-refresh duration" : undefined}
|
||||
>
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-execution-controls-list": true,
|
||||
"vm-execution-controls-list_mobile": isMobile,
|
||||
})}
|
||||
>
|
||||
{delayOptions.map(d => (
|
||||
{useAutorefresh && (
|
||||
<Popper
|
||||
open={openOptions}
|
||||
placement="bottom-right"
|
||||
onClose={handleCloseOptions}
|
||||
buttonRef={optionsButtonRef}
|
||||
title={isMobile ? "Auto-refresh duration" : undefined}
|
||||
>
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-list-item": true,
|
||||
"vm-list-item_mobile": isMobile,
|
||||
"vm-list-item_active": d.seconds === selectedDelay.seconds
|
||||
"vm-execution-controls-list": true,
|
||||
"vm-execution-controls-list_mobile": isMobile,
|
||||
})}
|
||||
key={d.seconds}
|
||||
onClick={createHandlerChange(d)}
|
||||
>
|
||||
{d.title}
|
||||
{delayOptions.map(d => (
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-list-item": true,
|
||||
"vm-list-item_mobile": isMobile,
|
||||
"vm-list-item_active": d.seconds === selectedDelay.seconds
|
||||
})}
|
||||
key={d.seconds}
|
||||
onClick={createHandlerChange(d)}
|
||||
>
|
||||
{d.title}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Popper>
|
||||
</>;
|
||||
</Popper>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7,7 +7,10 @@
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
border-radius: calc($button-radius + 1px);
|
||||
min-width: 107px;
|
||||
|
||||
:is(.vm-autorefresh) {
|
||||
min-width: 107px;
|
||||
}
|
||||
|
||||
&_mobile {
|
||||
flex-direction: column;
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import "./style.scss";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
export type BadgeColor = "firing" | "inactive" | "pending" | "no-match" | "unhealthy" | "ok" | "passive";
|
||||
|
||||
interface BadgeItem {
|
||||
value?: number | string;
|
||||
color: BadgeColor;
|
||||
}
|
||||
|
||||
interface BadgesProps {
|
||||
items: Record<string, BadgeItem>;
|
||||
align?: "center" | "start" | "end";
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
const Badges = ({ items, children, align = "start" }: BadgesProps) => {
|
||||
return (
|
||||
<div
|
||||
className="vm-badges"
|
||||
style={{ "justify-content": align }}
|
||||
>
|
||||
{Object.entries(items).map(([name, props]) => (
|
||||
<span
|
||||
key={name}
|
||||
className={`vm-badge ${props.color}`}
|
||||
>{props.value ? `${name}: ${props.value}` : name}</span>
|
||||
))}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Badges;
|
||||
@@ -0,0 +1,69 @@
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
$badge-colors: (
|
||||
"firing": $color-error,
|
||||
"inactive": $color-success,
|
||||
"pending": $color-warning,
|
||||
"no-match": $color-notice,
|
||||
"unhealthy": $color-broken,
|
||||
"ok": $color-info,
|
||||
"passive": $color-passive,
|
||||
"all": $color-passive,
|
||||
);
|
||||
|
||||
.vm-badges {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: $padding-small;
|
||||
&.align-center {
|
||||
justify-content: center;
|
||||
}
|
||||
.vm-badge {
|
||||
padding: 0 $padding-tiny;
|
||||
width: fit-content;
|
||||
@each $class, $color in $badge-colors {
|
||||
&.#{$class} {
|
||||
border: 1px solid $color;
|
||||
color: $color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.vm-badge-base {
|
||||
font-weight: 400;
|
||||
border-radius: $border-radius-small;
|
||||
}
|
||||
|
||||
.vm-badge-menu-item {
|
||||
@extend .vm-badge-base;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
line-height: 22px;
|
||||
@each $class, $color in $badge-colors {
|
||||
&.#{$class} {
|
||||
border-right: $border-radius-small solid $color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.vm-badge-item {
|
||||
@extend .vm-badge-base;
|
||||
@each $class, $color in $badge-colors {
|
||||
&.#{$class} {
|
||||
border-left: $border-radius-small solid $color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.vm-badge {
|
||||
@extend .vm-badge-base;
|
||||
background-color: transparent;
|
||||
padding: 0 $padding-tiny;
|
||||
line-height: 22px;
|
||||
max-width: 300px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import "./style.scss";
|
||||
import { Alert as APIAlert } from "../../../types";
|
||||
import { createSearchParams } from "react-router-dom";
|
||||
import Button from "../../Main/Button/Button";
|
||||
import Badges from "../Badges";
|
||||
import {
|
||||
SearchIcon,
|
||||
} from "../../Main/Icons";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
interface BaseAlertProps {
|
||||
item: APIAlert;
|
||||
}
|
||||
|
||||
const BaseAlert = ({ item }: BaseAlertProps) => {
|
||||
const query = item?.expression;
|
||||
|
||||
const openQueryLink = () => {
|
||||
const params = {
|
||||
"g0.expr": query,
|
||||
"g0.end_time": ""
|
||||
};
|
||||
window.open(`#/?${createSearchParams(params).toString()}`, "_blank", "noopener noreferrer");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="vm-explore-alerts-alert-item">
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
style={{ "text-align": "end" }}
|
||||
colSpan={2}
|
||||
>
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
color="gray"
|
||||
startIcon={<SearchIcon />}
|
||||
onClick={openQueryLink}
|
||||
>
|
||||
<span className="vm-button-text">Run query</span>
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="vm-col-md">Query</td>
|
||||
<td>
|
||||
<pre>
|
||||
<code className="language-promql">{query}</code>
|
||||
</pre>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="vm-col-md">Active at</td>
|
||||
<td>{dayjs(item.activeAt).format("DD MMM YYYY HH:mm:ss")}</td>
|
||||
</tr>
|
||||
{!!Object.keys(item?.labels || {}).length && (
|
||||
<tr>
|
||||
<td className="vm-col-md">Labels</td>
|
||||
<td>
|
||||
<Badges
|
||||
items={Object.fromEntries(Object.entries(item.labels).map(([name, value]) => [name, {
|
||||
color: "passive",
|
||||
value: value,
|
||||
}]))}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
{!!Object.keys(item.annotations || {}).length && (
|
||||
<>
|
||||
<span className="title">Annotations</span>
|
||||
<table>
|
||||
<tbody>
|
||||
{Object.entries(item.annotations || {}).map(([name, value]) => (
|
||||
<tr key={name}>
|
||||
<td className="vm-col-md">{name}</td>
|
||||
<td>{value}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BaseAlert;
|
||||
@@ -0,0 +1,74 @@
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-modal {
|
||||
.vm-explore-alerts-alert-item {
|
||||
table {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.vm-explore-alerts-alert-item {
|
||||
row-gap: $padding-global;
|
||||
margin-right: $padding-global;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.title {
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
a:hover > pre {
|
||||
background-color: $color-background-badge;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
background-color: $color-background-hover;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
pre {
|
||||
background-color: $color-background-badge;
|
||||
padding: 0 $padding-global;
|
||||
border-radius: $border-radius-small;
|
||||
word-break: break-word;
|
||||
white-space: pre-wrap;
|
||||
.keyword,
|
||||
.function,
|
||||
.attr-name,
|
||||
.range-duration {
|
||||
color: $color-keyword;
|
||||
}
|
||||
}
|
||||
|
||||
.vm-col-sm {
|
||||
width: 10%;
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.vm-col-md {
|
||||
width: 15%;
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
td, th {
|
||||
line-height: 30px;
|
||||
padding: 4px $padding-small;
|
||||
}
|
||||
th {
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
padding: 0 $padding-small;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import "./style.scss";
|
||||
import { Group as APIGroup } from "../../../types";
|
||||
import dayjs from "dayjs";
|
||||
import { formatDuration } from "../helpers";
|
||||
import Badges from "../Badges";
|
||||
|
||||
interface BaseGroupProps {
|
||||
group: APIGroup;
|
||||
}
|
||||
|
||||
const BaseGroup = ({ group }: BaseGroupProps) => {
|
||||
return (
|
||||
<div className="vm-explore-alerts-group">
|
||||
<div></div>
|
||||
<table>
|
||||
<tbody>
|
||||
{!!group.interval && (
|
||||
<tr>
|
||||
<td className="vm-col-md">Interval</td>
|
||||
<td>{formatDuration(group.interval)}</td>
|
||||
</tr>
|
||||
)}
|
||||
{!!group.lastEvaluation && (
|
||||
<tr>
|
||||
<td className="vm-col-md">Last evaluation</td>
|
||||
<td>{dayjs(group.lastEvaluation).format("DD MMM YYYY HH:mm:ss")}</td>
|
||||
</tr>
|
||||
)}
|
||||
{!!group.eval_offset && (
|
||||
<tr>
|
||||
<td className="vm-col-md">Eval offset</td>
|
||||
<td>{formatDuration(group.eval_offset)}</td>
|
||||
</tr>
|
||||
)}
|
||||
{!!group.eval_delay && (
|
||||
<tr>
|
||||
<td className="vm-col-md">Eval delay</td>
|
||||
<td>{formatDuration(group.eval_delay)}</td>
|
||||
</tr>
|
||||
)}
|
||||
{!!group.file && (
|
||||
<tr>
|
||||
<td className="vm-col-md">File</td>
|
||||
<td>{group.file}</td>
|
||||
</tr>
|
||||
)}
|
||||
{!!group.concurrency && (
|
||||
<tr>
|
||||
<td className="vm-col-md">Concurrency</td>
|
||||
<td>{group.concurrency}</td>
|
||||
</tr>
|
||||
)}
|
||||
{!!group?.labels?.length && (
|
||||
<tr>
|
||||
<td className="vm-col-md">Labels</td>
|
||||
<td>
|
||||
<Badges
|
||||
items={Object.fromEntries(Object.entries(group.labels).map(([name, value]) => [name, {
|
||||
color: "passive",
|
||||
value: value,
|
||||
}]))}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{!!group?.params?.length && (
|
||||
<tr>
|
||||
<td className="vm-col-md">Params</td>
|
||||
<td>
|
||||
<Badges
|
||||
items={Object.fromEntries(group.params.map(value => [value, {
|
||||
color: "passive",
|
||||
}]))}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{!!group?.headers?.length && (
|
||||
<tr>
|
||||
<td className="vm-col-md">Headers</td>
|
||||
<td>
|
||||
<Badges
|
||||
items={Object.fromEntries(group.headers.map(value => [value, {
|
||||
color: "passive",
|
||||
}]))}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{!!group?.notifier_headers?.length && (
|
||||
<tr>
|
||||
<td className="vm-col-md">Notifier headers</td>
|
||||
<td>
|
||||
<Badges
|
||||
items={Object.fromEntries(group.notifier_headers.map(value => [value, {
|
||||
color: "passive",
|
||||
}]))}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BaseGroup;
|
||||
@@ -0,0 +1,78 @@
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-modal {
|
||||
.vm-explore-alerts-group {
|
||||
table {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.vm-explore-alerts-group {
|
||||
row-gap: $padding-global;
|
||||
margin-right: $padding-global;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.title {
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
pre {
|
||||
position: relative;
|
||||
background-color: $color-background-badge;
|
||||
padding: 0 $padding-global;
|
||||
border-radius: $border-radius-small;
|
||||
word-break: break-word;
|
||||
white-space: pre-wrap;
|
||||
.keyword,
|
||||
.function,
|
||||
.attr-name,
|
||||
.range-duration {
|
||||
color: $color-keyword;
|
||||
}
|
||||
div {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
column-gap: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.vm-col-sm {
|
||||
width: 10%;
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.vm-col-md {
|
||||
width: 15%;
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
tr.hoverable {
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background-color: $color-background-hover;
|
||||
}
|
||||
}
|
||||
td, th {
|
||||
line-height: 30px;
|
||||
padding: 4px $padding-small;
|
||||
}
|
||||
th {
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
padding: 0 $padding-small;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
import "./style.scss";
|
||||
import { Rule as APIRule } from "../../../types";
|
||||
import { useNavigate, createSearchParams } from "react-router-dom";
|
||||
import { SearchIcon, DetailsIcon } from "../../Main/Icons";
|
||||
import Button from "../../Main/Button/Button";
|
||||
import Alert from "../../Main/Alert/Alert";
|
||||
import Badges, { BadgeColor } from "../Badges";
|
||||
import dayjs from "dayjs";
|
||||
import { formatDuration } from "../helpers";
|
||||
|
||||
interface BaseRuleProps {
|
||||
item: APIRule;
|
||||
}
|
||||
|
||||
const BaseRule = ({ item }: BaseRuleProps) => {
|
||||
const query = item?.query;
|
||||
const navigate = useNavigate();
|
||||
const openAlertLink = (id: string) => {
|
||||
return () => {
|
||||
navigate({
|
||||
pathname: "/rules",
|
||||
search: `group_id=${item.group_id}&alert_id=${id}`,
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
const openQueryLink = () => {
|
||||
const params = {
|
||||
"g0.expr": query,
|
||||
"g0.end_time": ""
|
||||
};
|
||||
window.open(`#/?${createSearchParams(params).toString()}`, "_blank", "noopener noreferrer");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="vm-explore-alerts-rule-item">
|
||||
<div></div>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
style={{ "text-align": "end" }}
|
||||
colSpan={2}
|
||||
>
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
color="gray"
|
||||
startIcon={<SearchIcon />}
|
||||
onClick={openQueryLink}
|
||||
>
|
||||
<span className="vm-button-text">Run query</span>
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="vm-col-md">Query</td>
|
||||
<td>
|
||||
<pre>
|
||||
<code className="language-promql">{query}</code>
|
||||
</pre>
|
||||
</td>
|
||||
</tr>
|
||||
{!!item.duration && (
|
||||
<tr>
|
||||
<td className="vm-col-md">For</td>
|
||||
<td>{formatDuration(item.duration)}</td>
|
||||
</tr>
|
||||
)}
|
||||
{!!item.lastEvaluation && (
|
||||
<tr>
|
||||
<td className="vm-col-md">Last evaluation</td>
|
||||
<td>{dayjs(item.lastEvaluation).format("DD MMM YYYY HH:mm:ss")}</td>
|
||||
</tr>
|
||||
)}
|
||||
{!!item.lastError && item.health !== "ok" && (
|
||||
<tr>
|
||||
<td className="vm-col-md">Last error</td>
|
||||
<td>
|
||||
<Alert variant="error">{item.lastError}</Alert>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{!!Object.keys(item?.labels || {}).length && (
|
||||
<tr>
|
||||
<td className="vm-col-md">Labels</td>
|
||||
<td>
|
||||
<Badges
|
||||
items={Object.fromEntries(Object.entries(item.labels).map(([name, value]) => [name, {
|
||||
color: "passive",
|
||||
value: value,
|
||||
}]))}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
{!!Object.keys(item?.annotations || {}).length && (
|
||||
<>
|
||||
<span className="title">Annotations</span>
|
||||
<table className="fixed">
|
||||
<tbody>
|
||||
{Object.entries(item.annotations || {}).map(([name, value]) => (
|
||||
<tr key={name}>
|
||||
<td className="vm-col-md">{name}</td>
|
||||
<td>{value}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
)}
|
||||
{!!item?.updates?.length && (
|
||||
<>
|
||||
<span className="title">{`Last updates ${item.updates.length}/${item.max_updates_entries}`}</span>
|
||||
<table className="fixed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="vm-col-md">Updated at</th>
|
||||
<th className="vm-col-md">Series returned</th>
|
||||
<th className="vm-col-md">Series fetched</th>
|
||||
<th className="vm-col-md">Duration</th>
|
||||
<th className="vm-col-md">Executed at</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{item.updates.map((update) => (
|
||||
<tr
|
||||
key={update.at}
|
||||
>
|
||||
<td className="vm-col-md">{dayjs(update.time).format("DD MMM YYYY HH:mm:ss")}</td>
|
||||
<td className="vm-col-md">{update.samples}</td>
|
||||
<td className="vm-col-md">{update.series_fetched}</td>
|
||||
<td className="vm-col-md">{formatDuration(update.duration / 1e9)}</td>
|
||||
<td className="vm-col-md">{dayjs(update.at).format("DD MMM YYYY HH:mm:ss")}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
)}
|
||||
{!!item?.alerts?.length && (
|
||||
<>
|
||||
<span className="title">Alerts</span>
|
||||
<table className="fixed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="vm-col-sm">Active since</th>
|
||||
<th className="vm-col-sm">State</th>
|
||||
<th className="vm-col-sm">Value</th>
|
||||
<th>Labels</th>
|
||||
<th className="vm-col-hidden"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{item.alerts.map((alert) => (
|
||||
<tr
|
||||
id={`alert-${alert.id}`}
|
||||
key={alert.id}
|
||||
>
|
||||
<td className="vm-col-sm">
|
||||
{dayjs(alert.activeAt).format("DD MMM YYYY HH:mm:ss")}
|
||||
</td>
|
||||
<td className="vm-col-sm">
|
||||
<Badges
|
||||
items={{ [alert.state]: { color: alert.state as BadgeColor } }}
|
||||
/>
|
||||
</td>
|
||||
<td className="vm-col-sm">
|
||||
<Badges
|
||||
items={{ [alert.value]: { color: "passive" } }}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<Badges
|
||||
align="center"
|
||||
items={Object.fromEntries(Object.entries(alert.labels || {}).map(([name, value]) => [name, {
|
||||
color: "passive",
|
||||
value: value,
|
||||
}]))}
|
||||
/>
|
||||
</td>
|
||||
<td className="vm-col-hidden">
|
||||
<Button
|
||||
className="vm-button-borderless"
|
||||
size="small"
|
||||
variant="outlined"
|
||||
color="gray"
|
||||
startIcon={<DetailsIcon />}
|
||||
onClick={openAlertLink(alert.id)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BaseRule;
|
||||
@@ -0,0 +1,88 @@
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-modal {
|
||||
.vm-explore-alerts-rule-item {
|
||||
table {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.vm-explore-alerts-rule-item {
|
||||
row-gap: $padding-global;
|
||||
margin-right: $padding-global;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.title {
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
pre {
|
||||
position: relative;
|
||||
background-color: $color-background-badge;
|
||||
padding: 0 $padding-global;
|
||||
border-radius: $border-radius-small;
|
||||
word-break: break-word;
|
||||
white-space: pre-wrap;
|
||||
.keyword,
|
||||
.function,
|
||||
.attr-name,
|
||||
.range-duration {
|
||||
color: $color-keyword;
|
||||
}
|
||||
div {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
column-gap: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.vm-col-hidden {
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
.vm-button {
|
||||
color: $color-passive;
|
||||
border: 1px solid var(--color-passive);
|
||||
}
|
||||
|
||||
.vm-col-sm {
|
||||
width: 10%;
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.vm-col-md {
|
||||
width: 15%;
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
table {
|
||||
&.fixed {
|
||||
table-layout: fixed;
|
||||
}
|
||||
width: 100%;
|
||||
td, th {
|
||||
line-height: 30px;
|
||||
padding: 4px $padding-small;
|
||||
vertical-align: middle;
|
||||
}
|
||||
td.align-center {
|
||||
text-align: center
|
||||
}
|
||||
th {
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
padding: 0 $padding-small;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { FC } from "preact/compat";
|
||||
import "./style.scss";
|
||||
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Group as APIGroup } from "../../../types";
|
||||
import { DetailsIcon } from "../../Main/Icons";
|
||||
import Button from "../../Main/Button/Button";
|
||||
import Badges, { BadgeColor } from "../Badges";
|
||||
import classNames from "classnames";
|
||||
interface GroupHeaderControlsProps {
|
||||
group: APIGroup;
|
||||
}
|
||||
|
||||
const GroupHeaderHeader: FC<GroupHeaderControlsProps> = ({ group }) => {
|
||||
const { isMobile } = useDeviceDetect();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const openGroupModal = async () => {
|
||||
navigate({
|
||||
pathname: "/rules",
|
||||
search: `group_id=${group.id}`,
|
||||
});
|
||||
};
|
||||
|
||||
const headerClasses = classNames({
|
||||
"vm-explore-alerts-group-header": true,
|
||||
"vm-explore-alerts-group-header_mobile": isMobile,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={headerClasses}>
|
||||
<div className="vm-explore-alerts-group-header__desc">
|
||||
<div className="vm-explore-alerts-group-header__name">{group.name}</div>
|
||||
{!isMobile && (
|
||||
<div className="vm-explore-alerts-group-header__file">{group.file}</div>
|
||||
)}
|
||||
</div>
|
||||
<Badges
|
||||
items={Object.fromEntries(Object.entries(group.states || {}).map(([name, value]) => [name.toLowerCase(), {
|
||||
color: name.toLowerCase().replace(" ", "-") as BadgeColor,
|
||||
value: value,
|
||||
}]))}
|
||||
>
|
||||
<Button
|
||||
className="vm-button-borderless"
|
||||
size="small"
|
||||
color="gray"
|
||||
variant="outlined"
|
||||
startIcon={<DetailsIcon />}
|
||||
onClick={openGroupModal}
|
||||
/>
|
||||
</Badges>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GroupHeaderHeader;
|
||||
@@ -0,0 +1,60 @@
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-explore-alerts-group-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: $padding-tiny 0 $padding-tiny $padding-global;
|
||||
justify-content: space-between;
|
||||
|
||||
.vm-button_small {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.vm-button-borderless {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
@media(max-width: 768px) {
|
||||
.vm-button-text {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&_mobile {
|
||||
.vm-button-text {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__desc {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $padding-tiny;
|
||||
}
|
||||
|
||||
&__index {
|
||||
color: $color-text-secondary;
|
||||
font-size: $font-size-small;
|
||||
}
|
||||
|
||||
&__name {
|
||||
flex-grow: 1;
|
||||
font-weight: bold;
|
||||
max-width: 100%;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
line-height: 130%;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
&__file {
|
||||
color: $color-text-disabled;
|
||||
}
|
||||
|
||||
code {
|
||||
padding: 0.2em 0.4em;
|
||||
font-size: 85%;
|
||||
background-color: $color-hover-black;
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import { FC } from "preact/compat";
|
||||
import "./style.scss";
|
||||
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||
import useCopyToClipboard from "../../../hooks/useCopyToClipboard";
|
||||
import { useAppState } from "../../../state/common/StateContext";
|
||||
import Tooltip from "../../Main/Tooltip/Tooltip";
|
||||
import classNames from "classnames";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import Badges, { BadgeColor } from "../Badges";
|
||||
import {
|
||||
LinkIcon,
|
||||
GroupIcon,
|
||||
AlertIcon,
|
||||
AlertingRuleIcon,
|
||||
RecordingRuleIcon,
|
||||
DetailsIcon,
|
||||
} from "../../Main/Icons";
|
||||
import Button from "../../Main/Button/Button";
|
||||
|
||||
interface ItemHeaderControlsProps {
|
||||
entity: string;
|
||||
type?: string;
|
||||
groupId: string;
|
||||
states?: Record<string, number>;
|
||||
id?: string;
|
||||
name: string;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
const ItemHeader: FC<ItemHeaderControlsProps> = ({ name, id, groupId, entity, type, states, onClose }) => {
|
||||
const { isMobile } = useDeviceDetect();
|
||||
const { serverUrl } = useAppState();
|
||||
const navigate = useNavigate();
|
||||
const copyToClipboard = useCopyToClipboard();
|
||||
|
||||
const openItemLink = () => {
|
||||
navigate({
|
||||
pathname: "/rules",
|
||||
search: `group_id=${groupId}&${entity}_id=${id}`,
|
||||
});
|
||||
};
|
||||
|
||||
const copyLink = async () => {
|
||||
let link = `${serverUrl}/vmui/#/rules?group_id=${groupId}`;
|
||||
if (type) link = `${link}&${entity}_id=${id}`;
|
||||
await copyToClipboard(link, `Link to ${entity} has been copied`);
|
||||
};
|
||||
|
||||
const headerClasses = classNames({
|
||||
"vm-explore-alerts-item-header": true,
|
||||
"vm-explore-alerts-item-header_mobile": isMobile,
|
||||
});
|
||||
|
||||
const renderIcon = () => {
|
||||
switch(entity) {
|
||||
case "alert":
|
||||
return (
|
||||
<Tooltip title="Alert">
|
||||
<AlertIcon />
|
||||
</Tooltip>
|
||||
);
|
||||
case "group":
|
||||
return (
|
||||
<Tooltip title="Group">
|
||||
<GroupIcon />
|
||||
</Tooltip>
|
||||
);
|
||||
default:
|
||||
switch(type) {
|
||||
case "alerting":
|
||||
return (
|
||||
<Tooltip title="Alerting rule">
|
||||
<AlertingRuleIcon />
|
||||
</Tooltip>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Tooltip title="Recording rule">
|
||||
<RecordingRuleIcon />
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={headerClasses}
|
||||
id={`rule-${id}`}
|
||||
>
|
||||
<div className="vm-explore-alerts-item-header__title">
|
||||
{renderIcon()}
|
||||
<div className="vm-explore-alerts-item-header__name">{name}</div>
|
||||
</div>
|
||||
<Badges
|
||||
items={Object.fromEntries(Object.entries(states || {}).map(([name, value]) => [name, {
|
||||
color: name.toLowerCase().replace(" ", "-") as BadgeColor,
|
||||
value: value == 1 ? 0 : value,
|
||||
}]))}
|
||||
>
|
||||
{onClose ? (
|
||||
<Button
|
||||
className="vm-back-button"
|
||||
size="small"
|
||||
variant="outlined"
|
||||
color="gray"
|
||||
startIcon={<LinkIcon />}
|
||||
onClick={copyLink}
|
||||
>
|
||||
<span className="vm-button-text">Copy Link</span>
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
className="vm-button-borderless"
|
||||
size="small"
|
||||
variant="outlined"
|
||||
color="gray"
|
||||
startIcon={<DetailsIcon />}
|
||||
onClick={openItemLink}
|
||||
/>
|
||||
)}
|
||||
</Badges>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ItemHeader;
|
||||
@@ -0,0 +1,70 @@
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-explore-alerts-item-header {
|
||||
display: flex;
|
||||
grid-template-columns: auto 1fr auto auto;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: $padding-global;
|
||||
|
||||
.vm-button_small {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
@media(max-width: 768px) {
|
||||
.vm-button-text {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.vm-button-borderless {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.vm-back-button {
|
||||
svg {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
|
||||
&_mobile {
|
||||
grid-template-columns: 1fr auto;
|
||||
.vm-button-text {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__index {
|
||||
color: $color-text-secondary;
|
||||
font-size: $font-size-small;
|
||||
}
|
||||
|
||||
&__name {
|
||||
font-weight: bold;
|
||||
max-width: 100%;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
line-height: 130%;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
&__title {
|
||||
display: flex;
|
||||
column-gap: $padding-global;
|
||||
svg {
|
||||
fill: $color-text-disabled;
|
||||
width: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
&__file {
|
||||
color: $color-text-disabled;
|
||||
}
|
||||
|
||||
code {
|
||||
padding: 0.2em 0.4em;
|
||||
font-size: 85%;
|
||||
background-color: $color-hover-black;
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { FC } from "preact/compat";
|
||||
import "./style.scss";
|
||||
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||
import { Notifier } from "../../../types";
|
||||
import classNames from "classnames";
|
||||
|
||||
interface NotifierHeaderControlsProps {
|
||||
notifier: Notifier;
|
||||
}
|
||||
|
||||
const NotifierHeaderHeader: FC<NotifierHeaderControlsProps> = ({
|
||||
notifier,
|
||||
}) => {
|
||||
const { isMobile } = useDeviceDetect();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-explore-alerts-notifier-header": true,
|
||||
"vm-explore-alerts-notifier-header_mobile": isMobile,
|
||||
})}
|
||||
>
|
||||
<div className="vm-explore-alerts-notifier-header__name">
|
||||
{notifier.kind}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotifierHeaderHeader;
|
||||
@@ -0,0 +1,40 @@
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-explore-alerts-notifier-header {
|
||||
display: flex;
|
||||
grid-template-columns: auto 1fr auto auto;
|
||||
align-items: center;
|
||||
padding: $padding-global;
|
||||
justify-content: space-between;
|
||||
gap: $padding-global;
|
||||
|
||||
&_mobile {
|
||||
grid-template-columns: 1fr auto;
|
||||
padding: $padding-small $padding-global;
|
||||
}
|
||||
|
||||
&__index {
|
||||
color: $color-text-secondary;
|
||||
font-size: $font-size-small;
|
||||
}
|
||||
|
||||
&__name {
|
||||
flex-grow: 1;
|
||||
font-weight: bold;
|
||||
max-width: 100%;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
line-height: 130%;
|
||||
}
|
||||
|
||||
&__file {
|
||||
color: $color-text-disabled;
|
||||
}
|
||||
|
||||
code {
|
||||
padding: 0.2em 0.4em;
|
||||
font-size: 85%;
|
||||
background-color: $color-hover-black;
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { FC } from "preact/compat";
|
||||
import Select from "../../Main/Select/Select";
|
||||
import { SearchIcon } from "../../Main/Icons";
|
||||
import TextField from "../../Main/TextField/TextField";
|
||||
import "./style.scss";
|
||||
import classNames from "classnames";
|
||||
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||
|
||||
interface NotifiersHeaderProps {
|
||||
kinds: string[];
|
||||
allKinds: string[];
|
||||
onChangeKinds: (input: string) => void;
|
||||
onChangeSearch: (input: string) => void;
|
||||
}
|
||||
|
||||
const NotifiersHeader: FC<NotifiersHeaderProps> = ({
|
||||
kinds,
|
||||
allKinds,
|
||||
onChangeKinds,
|
||||
onChangeSearch,
|
||||
}) => {
|
||||
const { isMobile } = useDeviceDetect();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-explore-alerts-header": true,
|
||||
"vm-explore-alerts-header_mobile": isMobile,
|
||||
"vm-block": true,
|
||||
"vm-block_mobile": isMobile,
|
||||
})}
|
||||
>
|
||||
<div className="vm-explore-alerts-header__rule_type">
|
||||
<Select
|
||||
value={kinds}
|
||||
list={allKinds}
|
||||
label="Notifier type"
|
||||
placeholder="Please select notifier type"
|
||||
onChange={onChangeKinds}
|
||||
autofocus={!!kinds.length && !isMobile}
|
||||
includeAll
|
||||
searchable
|
||||
/>
|
||||
</div>
|
||||
<div className="vm-explore-alerts-header-search">
|
||||
<TextField
|
||||
label="Search"
|
||||
placeholder="Filter by kind, address or labels"
|
||||
startIcon={<SearchIcon />}
|
||||
onChange={onChangeSearch}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotifiersHeader;
|
||||
@@ -0,0 +1,65 @@
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-explore-alerts-header {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: $padding-global calc($padding-small + 10px);
|
||||
width: 100%;
|
||||
|
||||
&_mobile {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
&__rule_type {
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
&__state {
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
&-description {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
align-items: flex-start;
|
||||
gap: $padding-small;
|
||||
|
||||
ul {
|
||||
list-style-position: inside;
|
||||
}
|
||||
|
||||
button {
|
||||
color: inherit;
|
||||
min-height: 29px;
|
||||
}
|
||||
|
||||
code {
|
||||
margin: 0 3px;
|
||||
}
|
||||
}
|
||||
|
||||
&-search {
|
||||
flex-grow: 1;
|
||||
.vm-text-field__input {
|
||||
padding: 11px 28px;
|
||||
}
|
||||
.vm-text-field__icon-start {
|
||||
height: 42px;
|
||||
}
|
||||
}
|
||||
|
||||
&__clear-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { FC } from "preact/compat";
|
||||
import ItemHeader from "../ItemHeader";
|
||||
import Accordion from "../../Main/Accordion/Accordion";
|
||||
import "./style.scss";
|
||||
import { Rule as APIRule } from "../../../types";
|
||||
import BaseRule from "../BaseRule";
|
||||
|
||||
interface RuleProps {
|
||||
states: Record<string, number>;
|
||||
rule: APIRule;
|
||||
}
|
||||
|
||||
const Rule: FC<RuleProps> = ({ states, rule }) => {
|
||||
const state = Object.keys(states).length > 0 ? Object.keys(states)[0] : "ok";
|
||||
return (
|
||||
<div className={`vm-explore-alerts-rule vm-badge-item ${state.replace(" ", "-")}`}>
|
||||
<Accordion
|
||||
key={`rule-${rule.id}`}
|
||||
title={<ItemHeader
|
||||
entity="rule"
|
||||
type={rule.type}
|
||||
groupId={rule.group_id}
|
||||
states={states}
|
||||
id={rule.id}
|
||||
name={rule.name}
|
||||
/>}
|
||||
>
|
||||
<BaseRule item={rule} />
|
||||
</Accordion>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Rule;
|
||||
@@ -0,0 +1,18 @@
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-explore-alerts-rule {
|
||||
padding: $padding-tiny;
|
||||
padding-right: 0;
|
||||
display: flex;
|
||||
row-gap: $padding-tiny;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
|
||||
&:has(>details[open]) {
|
||||
background-color: $color-background-item;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $color-background-item;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { FC, useMemo } from "preact/compat";
|
||||
import Select from "../../Main/Select/Select";
|
||||
import { SearchIcon } from "../../Main/Icons";
|
||||
import TextField from "../../Main/TextField/TextField";
|
||||
import "./style.scss";
|
||||
import classNames from "classnames";
|
||||
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||
|
||||
interface RulesHeaderProps {
|
||||
types: string[];
|
||||
allTypes: string[];
|
||||
allStates: string[];
|
||||
states: string[];
|
||||
onChangeTypes: (input: string) => void;
|
||||
onChangeStates: (input: string) => void;
|
||||
onChangeSearch: (input: string) => void;
|
||||
}
|
||||
|
||||
const RulesHeader: FC<RulesHeaderProps> = ({
|
||||
types,
|
||||
allTypes,
|
||||
allStates,
|
||||
states,
|
||||
onChangeTypes,
|
||||
onChangeStates,
|
||||
onChangeSearch,
|
||||
}) => {
|
||||
const noStateText = useMemo(
|
||||
() => (types.length ? "" : "No states. Please select rule states"),
|
||||
[types],
|
||||
);
|
||||
const { isMobile } = useDeviceDetect();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-explore-alerts-header": true,
|
||||
"vm-explore-alerts-header_mobile": isMobile,
|
||||
"vm-block": true,
|
||||
"vm-block_mobile": isMobile,
|
||||
})}
|
||||
>
|
||||
<div className="vm-explore-alerts-header__rule_type">
|
||||
<Select
|
||||
value={types}
|
||||
list={allTypes}
|
||||
label="Rules type"
|
||||
placeholder="Please select rule type"
|
||||
onChange={onChangeTypes}
|
||||
autofocus={!!types.length && !isMobile}
|
||||
includeAll
|
||||
searchable
|
||||
/>
|
||||
</div>
|
||||
<div className="vm-explore-alerts-header__state">
|
||||
<Select
|
||||
itemClassName="vm-badge-menu-item"
|
||||
value={states}
|
||||
list={allStates}
|
||||
label="State"
|
||||
placeholder="Please rule state"
|
||||
onChange={onChangeStates}
|
||||
noOptionsText={noStateText}
|
||||
includeAll
|
||||
searchable
|
||||
/>
|
||||
</div>
|
||||
<div className="vm-explore-alerts-header-search">
|
||||
<TextField
|
||||
label="Search"
|
||||
placeholder="Filter by rule, name or labels"
|
||||
startIcon={<SearchIcon />}
|
||||
onChange={onChangeSearch}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default RulesHeader;
|
||||
@@ -0,0 +1,65 @@
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-explore-alerts-header {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: $padding-global calc($padding-small + 10px);
|
||||
width: 100%;
|
||||
|
||||
&_mobile {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
&__rule_type {
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
&__state {
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
&-description {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
align-items: flex-start;
|
||||
gap: $padding-small;
|
||||
|
||||
ul {
|
||||
list-style-position: inside;
|
||||
}
|
||||
|
||||
button {
|
||||
color: inherit;
|
||||
min-height: 29px;
|
||||
}
|
||||
|
||||
code {
|
||||
margin: 0 3px;
|
||||
}
|
||||
}
|
||||
|
||||
&-search {
|
||||
flex-grow: 1;
|
||||
.vm-text-field__input {
|
||||
padding: 11px 28px;
|
||||
}
|
||||
.vm-text-field__icon-start {
|
||||
height: 42px;
|
||||
}
|
||||
}
|
||||
|
||||
&__clear-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { FC } from "preact/compat";
|
||||
import "./style.scss";
|
||||
import { Target as APITarget } from "../../../types";
|
||||
import Alert from "../../Main/Alert/Alert";
|
||||
import Accordion from "../../Main/Accordion/Accordion";
|
||||
import Badges from "../Badges";
|
||||
|
||||
interface TargetProps {
|
||||
target: APITarget;
|
||||
}
|
||||
|
||||
const Target: FC<TargetProps> = ({ target }) => {
|
||||
const state = target?.lastError ? "unhealthy" : "ok";
|
||||
return (
|
||||
<div className={`vm-explore-alerts-target vm-badge-item ${state.replace(" ", "-")}`}>
|
||||
{(!!target?.labels?.length || !!target?.lastError) ? (
|
||||
<Accordion
|
||||
key={`target-${target.address}`}
|
||||
title={(
|
||||
<div className="vm-explore-alerts-target-header__name">{target.address}</div>
|
||||
)}
|
||||
>
|
||||
<div className="vm-explore-alerts-target-item">
|
||||
<table>
|
||||
<tbody>
|
||||
{!!target?.labels?.length && (
|
||||
<tr>
|
||||
<td className="vm-col-md">Labels</td>
|
||||
<td>
|
||||
<Badges
|
||||
items={Object.fromEntries(Object.entries(target.labels).map(([name, value]) => [name, {
|
||||
value: value,
|
||||
color: "passive",
|
||||
}]))}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{!!target.lastError && (
|
||||
<tr>
|
||||
<td className="vm-col-md">Last error</td>
|
||||
<td>
|
||||
<Alert variant="error">{target.lastError}</Alert>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Accordion>
|
||||
) : (
|
||||
<span>{target.address}</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Target;
|
||||
@@ -0,0 +1,48 @@
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-explore-alerts-target {
|
||||
row-gap: $padding-global;
|
||||
margin-right: $padding-global;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.vm-col-md {
|
||||
width: 40%;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
td {
|
||||
vertical-align: middle;
|
||||
padding: $padding-global $padding-small;
|
||||
}
|
||||
th {
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
padding: 0 $padding-small;
|
||||
}
|
||||
}
|
||||
|
||||
padding: $padding-tiny;
|
||||
padding-right: 0;
|
||||
display: flex;
|
||||
row-gap: $padding-tiny;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
border-radius: $border-radius-small;
|
||||
|
||||
&:has(>details[open]) {
|
||||
background-color: $color-background-item;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $color-background-item;
|
||||
}
|
||||
|
||||
.vm-explore-alerts-item-header__name {
|
||||
line-height: 22px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import dayjs from "dayjs";
|
||||
|
||||
export const formatDuration = (raw: number) => {
|
||||
const duration = dayjs.duration(Math.round(raw * 1000));
|
||||
const fmt = [];
|
||||
if (duration.get("day")) fmt.push("D[d]");
|
||||
if (duration.get("hour")) fmt.push("H[h]");
|
||||
if (duration.get("minute")) fmt.push("m[m]");
|
||||
if (duration.get("millisecond")) {
|
||||
fmt.push("s.SSS[s]");
|
||||
} else if (!fmt.length || duration.get("second")) {
|
||||
fmt.push("s[s]");
|
||||
}
|
||||
return duration.format(fmt.join(" "));
|
||||
};
|
||||
@@ -1,9 +1,11 @@
|
||||
import { FC, useState, useEffect } from "preact/compat";
|
||||
import { JSX } from "preact";
|
||||
import { ArrowDownIcon } from "../Icons";
|
||||
import "./style.scss";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
interface AccordionProps {
|
||||
id?: string
|
||||
title: ReactNode
|
||||
children: ReactNode
|
||||
defaultExpanded?: boolean
|
||||
@@ -14,21 +16,24 @@ const Accordion: FC<AccordionProps> = ({
|
||||
defaultExpanded = false,
|
||||
onChange,
|
||||
title,
|
||||
children
|
||||
children,
|
||||
id,
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(defaultExpanded);
|
||||
|
||||
const toggleOpen = () => {
|
||||
const toggleOpen = (event: JSX.TargetedMouseEvent<HTMLElement>) => {
|
||||
const selection = window.getSelection();
|
||||
if (selection && selection.toString()) {
|
||||
if ((event.target as HTMLElement).closest("button")) {
|
||||
event.preventDefault();
|
||||
return; // If the text is selected, cancel the execution of toggle.
|
||||
}
|
||||
|
||||
setIsOpen((prev) => {
|
||||
const newState = !prev;
|
||||
onChange && onChange(newState);
|
||||
return newState;
|
||||
});
|
||||
if (selection && selection.toString()) {
|
||||
event.preventDefault();
|
||||
return; // If the text is selected, cancel the execution of toggle.
|
||||
}
|
||||
const details = event.currentTarget.parentElement as HTMLDetailsElement;
|
||||
onChange && onChange(details.open);
|
||||
setIsOpen(details.open);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -37,23 +42,23 @@ const Accordion: FC<AccordionProps> = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<header
|
||||
className={`vm-accordion-header ${isOpen && "vm-accordion-header_open"}`}
|
||||
onClick={toggleOpen}
|
||||
<details
|
||||
className="vm-accordion-section"
|
||||
key="content"
|
||||
open={isOpen}
|
||||
id={id}
|
||||
>
|
||||
{title}
|
||||
<div className={`vm-accordion-header__arrow ${isOpen && "vm-accordion-header__arrow_open"}`}>
|
||||
<ArrowDownIcon />
|
||||
</div>
|
||||
</header>
|
||||
{isOpen && (
|
||||
<section
|
||||
className="vm-accordion-section"
|
||||
key="content"
|
||||
<summary
|
||||
className="vm-accordion-header"
|
||||
onClick={toggleOpen}
|
||||
>
|
||||
{children}
|
||||
</section>
|
||||
)}
|
||||
{title}
|
||||
<div className="vm-accordion-header__arrow">
|
||||
<ArrowDownIcon />
|
||||
</div>
|
||||
</summary>
|
||||
{children}
|
||||
</details>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -17,10 +17,6 @@
|
||||
transform: rotate(0);
|
||||
transition: transform 200ms ease-in-out;
|
||||
|
||||
&_open {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 14px;
|
||||
height: auto;
|
||||
@@ -28,6 +24,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
.vm-accordion-section[open] > summary {
|
||||
& > .vm-accordion-header {
|
||||
&__arrow {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.accordion-section {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -1,41 +1,27 @@
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-alert {
|
||||
position: relative;
|
||||
z-index: 20;
|
||||
position: sticky;
|
||||
top: $padding-global;
|
||||
display: grid;
|
||||
grid-template-columns: 20px 1fr;
|
||||
align-items: center;
|
||||
gap: $padding-small;
|
||||
padding: $padding-global;
|
||||
background-color: $color-background-block;
|
||||
border-radius: $border-radius-medium;
|
||||
box-shadow: $box-shadow;
|
||||
font-size: $font-size;
|
||||
font-weight: normal;
|
||||
color: $color-text;
|
||||
line-height: 1.5;
|
||||
opacity: 0.8;
|
||||
|
||||
&_mobile {
|
||||
align-items: flex-start;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
&:after {
|
||||
position: absolute;
|
||||
content: '';
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: $border-radius-medium;
|
||||
z-index: 1;
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
&_mobile:after {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
&__icon,
|
||||
&__content {
|
||||
position: relative;
|
||||
@@ -48,54 +34,53 @@
|
||||
justify-content: center;
|
||||
align-self: flex-start;
|
||||
min-height: 24px;
|
||||
filter: brightness(0.6);
|
||||
}
|
||||
|
||||
&__content {
|
||||
filter: brightness(0.6);
|
||||
white-space: pre-line;
|
||||
text-wrap: balance;
|
||||
overflow-wrap: anywhere;
|
||||
filter: brightness(0.6);
|
||||
}
|
||||
|
||||
&_success {
|
||||
color: $color-success;
|
||||
|
||||
&:after {
|
||||
background-color: $color-success;
|
||||
}
|
||||
background-color: $color-background-success;
|
||||
}
|
||||
|
||||
&_error {
|
||||
color: $color-error;
|
||||
|
||||
&:after {
|
||||
background-color: $color-error;
|
||||
}
|
||||
background-color: $color-background-error;
|
||||
}
|
||||
|
||||
&_info {
|
||||
color: $color-info;
|
||||
|
||||
&:after {
|
||||
background-color: $color-info;
|
||||
}
|
||||
background-color: $color-background-info;
|
||||
}
|
||||
|
||||
&_warning {
|
||||
color: $color-warning;
|
||||
|
||||
&:after {
|
||||
background-color: $color-warning;
|
||||
}
|
||||
background-color: $color-background-warning;
|
||||
}
|
||||
|
||||
&_dark {
|
||||
&:after {
|
||||
opacity: 0.1;
|
||||
}
|
||||
}
|
||||
|
||||
&_dark &__content {
|
||||
&_dark &__content, &_dark &__icon {
|
||||
filter: none;
|
||||
}
|
||||
|
||||
&_dark:is(&_success) {
|
||||
border: 0.5px solid $color-success;
|
||||
}
|
||||
|
||||
&_dark:is(&_error) {
|
||||
border: 0.5px solid $color-error;
|
||||
}
|
||||
|
||||
&_dark:is(&_info) {
|
||||
border: 0.5px solid $color-info;
|
||||
}
|
||||
|
||||
&_dark:is(&_warning) {
|
||||
border: 0.5px solid $color-warning;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ export interface AutocompleteOptions {
|
||||
}
|
||||
|
||||
interface AutocompleteProps {
|
||||
itemClassName?: string
|
||||
value: string
|
||||
options: AutocompleteOptions[]
|
||||
anchor: React.RefObject<HTMLElement>
|
||||
@@ -41,6 +42,7 @@ enum FocusType {
|
||||
|
||||
const Autocomplete: FC<AutocompleteProps> = ({
|
||||
value,
|
||||
itemClassName,
|
||||
options,
|
||||
anchor,
|
||||
disabled,
|
||||
@@ -212,7 +214,9 @@ const Autocomplete: FC<AutocompleteProps> = ({
|
||||
>
|
||||
{selected?.includes(option.value) && <DoneIcon/>}
|
||||
<>{option.icon}</>
|
||||
<span>{option.value}</span>
|
||||
<div className={`vm-list-item-inner ${itemClassName} ${option.value.toLowerCase().replace(" ", "-")}`}>
|
||||
<span>{option.value}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,61 @@
|
||||
import { getCssVariable } from "../../../utils/theme";
|
||||
|
||||
export const LinkIcon = () => (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M10.975 14.51a1.05 1.05 0 0 0 0-1.485 2.95 2.95 0 0 1 0-4.172l3.536-3.535a2.95 2.95 0 1 1 4.172 4.172l-1.093 1.092a1.05 1.05 0 0 0 1.485 1.485l1.093-1.092a5.05 5.05 0 0 0-7.142-7.142L9.49 7.368a5.05 5.05 0 0 0 0 7.142c.41.41 1.075.41 1.485 0m2.05-5.02a1.05 1.05 0 0 0 0 1.485 2.95 2.95 0 0 1 0 4.172l-3.5 3.5a2.95 2.95 0 1 1-4.171-4.172l1.025-1.025a1.05 1.05 0 0 0-1.485-1.485L3.87 12.99a5.05 5.05 0 0 0 7.142 7.142l3.5-3.5a5.05 5.05 0 0 0 0-7.142 1.05 1.05 0 0 0-1.485 0z"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const GroupIcon = () => (
|
||||
<svg
|
||||
fill="currentColor"
|
||||
viewBox="0 0 512 512"
|
||||
>
|
||||
<path d="M170.667 64v42.667h-64v298.666h64V448H64V64zM448 64v384H341.333v-42.667h64V106.667h-64V64zm-85.333 256v42.667H149.333V320zm0-85.333v42.666H149.333v-42.666zm0-85.334V192H149.333v-42.667z"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const DetailsIcon = () => (
|
||||
<svg
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path d="M12 3a2 2 0 1 0-4 0 2 2 0 0 0 4 0m-2 5a2 2 0 1 1 0 4 2 2 0 0 1 0-4m0 7a2 2 0 1 1 0 4 2 2 0 0 1 0-4"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const AlertIcon = () => (
|
||||
<svg
|
||||
viewBox="-1 0 30 30"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="m3 24 3-6v-8a8 8 0 0 1 16 0v8l3 6zm11 4a2.99 2.99 0 0 1-2.816-2h5.632A2.99 2.99 0 0 1 14 28m10-10v-8c0-5.522-4.478-10-10-10S4 4.478 4 10v8l-4 8h9.101a5 5 0 0 0 9.798 0H28z"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const AlertingRuleIcon = () => (
|
||||
<svg
|
||||
viewBox="411.014 448.582 21.637 17.836"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M411.47 453.837a.72.72 0 0 0-.422.679c0 .319.137.535.422.667.139.065.322.068 3.814.069l3.668.001.164-.083c.484-.245.535-.904.096-1.254l-.141-.112-3.746-.008c-3.05-.006-3.766.001-3.855.041m-.016 5.54c-.284.124-.44.372-.439.701.001.354.204.614.553.708.092.025 1.214.034 3.267.028 3.481-.011 3.23.008 3.468-.264a.714.714 0 0 0-.003-.963c-.249-.283-.03-.267-3.564-.266-2.735 0-3.171.008-3.282.056m-.018 5.016a.72.72 0 0 0-.422.679c0 .319.137.535.422.667.139.065.322.068 3.814.069l3.668.001.164-.083c.484-.245.535-.904.096-1.254l-.141-.112-3.746-.008c-3.05-.006-3.766.001-3.855.041m.161-15.761a.85.85 0 0 0-.389.417c-.023.07-.034.224-.026.343.018.239.078.345.301.525l.139.112 8.729.01 8.73.009.163-.079c.193-.093.36-.332.395-.565a.75.75 0 0 0-.388-.75c-.142-.072-.227-.073-8.844-.072-7.105.002-8.721.011-8.81.05m14.005 2.595c-.286.18-.371.401-.371.961v.334l-.499.024c-.598.028-.961.126-1.456.392a3.5 3.5 0 0 0-1.721 2.199c-.081.307-.091.479-.115 1.923-.027 1.566-.028 1.59-.138 1.966-.145.496-.557 1.361-.929 1.945a5 5 0 0 0-.368.677c-.1.292-.095.679.013.982.112.32.461.686.75.789.276.099 1.255.259 2.268.373l.84.095.028.287q.089.935.767 1.579a2.383 2.383 0 0 0 3.659-.434c.227-.351.36-.745.396-1.161l.023-.283.291-.027c.956-.093 2.47-.32 2.715-.408.393-.14.694-.464.817-.875.16-.539.093-.833-.354-1.554-.373-.601-.832-1.565-.956-2.007-.083-.29-.093-.448-.119-1.903-.027-1.427-.039-1.619-.118-1.924a3.5 3.5 0 0 0-1.895-2.327c-.422-.202-.758-.282-1.309-.312l-.489-.025-.022-.473c-.022-.521-.062-.621-.325-.806-.124-.088-.182-.096-.69-.096-.489.001-.57.012-.693.089m2.696 2.786c.546.176.994.583 1.249 1.135l.149.326.025 1.543c.027 1.672.046 1.837.286 2.598.166.52.621 1.468.974 2.028.189.303.274.472.244.492-.104.066-1.778.288-2.915.387-.788.068-3.246.068-4.037 0-1.154-.099-2.811-.32-2.919-.39-.035-.023.03-.16.223-.469.375-.603.805-1.493.976-2.024.246-.763.272-1 .272-2.426 0-.701.019-1.398.043-1.549.083-.554.47-1.148.931-1.429.103-.063.308-.157.453-.209.258-.092.315-.094 2.025-.096 1.642-.001 1.776.005 2.021.083m-1.384 10.771a1.06 1.06 0 0 1-.748.2c-.394-.066-.776-.451-.835-.841l-.026-.168h2.005v.108z"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const RecordingRuleIcon = () => (
|
||||
<svg
|
||||
viewBox="411.014 448.582 23.358 18.492"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M411.47 453.837a.72.72 0 0 0-.422.679c0 .319.137.535.422.667.139.065.322.068 3.814.069l3.668.001.164-.083c.484-.245.535-.904.096-1.254l-.141-.112-3.746-.008c-3.05-.006-3.766.001-3.855.041m15.967 7.103a1.592 1.612 0 1 1 1.592-1.612 1.592 1.612 0 0 1-1.592 1.612m0-1.612"/>
|
||||
<path d="M427.405 466.377a6.966 7.052 0 1 1 6.965-7.053 6.974 7.06 0 0 1-6.965 7.053m0-12.09a4.975 5.037 0 1 0 4.975 5.037 4.981 5.043 0 0 0-4.975-5.037"/>
|
||||
<path d="M421.832 467.074a.996 1.008 0 0 1-.708-1.715l3.582-3.675a.995 1.008 0 0 1 1.417 1.415l-3.582 3.675a.995 1.007 0 0 1-.709.3m-10.378-7.697c-.284.124-.44.372-.439.701.001.354.204.614.553.708.092.025 1.214.034 3.267.028 3.481-.011 3.23.008 3.468-.264a.714.714 0 0 0-.003-.963c-.249-.283-.03-.267-3.564-.266-2.735 0-3.171.008-3.282.056m-.018 5.016a.72.72 0 0 0-.422.679c0 .319.137.535.422.667.139.065.322.068 3.814.069l3.668.001.164-.083c.484-.245.535-.904.096-1.254l-.141-.112-3.746-.008c-3.05-.006-3.766.001-3.855.041m.161-15.761a.85.85 0 0 0-.389.417c-.023.07-.034.224-.026.343.018.239.078.345.301.525l.139.112 8.729.01 8.73.009.163-.079c.193-.093.36-.332.395-.565a.75.75 0 0 0-.388-.75c-.142-.072-.227-.073-8.844-.072-7.105.002-8.721.011-8.81.05"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const LogoIcon = () => (
|
||||
<svg
|
||||
viewBox="0 0 74 24"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { FC, useCallback, useEffect, createPortal } from "preact/compat";
|
||||
import { JSX } from "preact/jsx-runtime";
|
||||
import { CloseIcon } from "../Icons";
|
||||
import Button from "../Button/Button";
|
||||
import { ReactNode, MouseEvent } from "react";
|
||||
@@ -9,7 +10,7 @@ import { useLocation, useNavigate } from "react-router-dom";
|
||||
import useEventListener from "../../../hooks/useEventListener";
|
||||
|
||||
interface ModalProps {
|
||||
title?: string
|
||||
title: JSX.Element | string
|
||||
children: ReactNode
|
||||
onClose: () => void
|
||||
className?: string
|
||||
|
||||
@@ -5,10 +5,11 @@ import { MouseEvent } from "react";
|
||||
|
||||
interface MultipleSelectedValueProps {
|
||||
values: string[]
|
||||
itemClassName?: string
|
||||
onRemoveItem: (val: string) => void
|
||||
}
|
||||
|
||||
const MultipleSelectedValue: FC<MultipleSelectedValueProps> = ({ values, onRemoveItem }) => {
|
||||
const MultipleSelectedValue: FC<MultipleSelectedValueProps> = ({ values, itemClassName, onRemoveItem }) => {
|
||||
const { isMobile } = useDeviceDetect();
|
||||
|
||||
const createHandleClick = (value: string) => (e: MouseEvent<HTMLDivElement>) => {
|
||||
@@ -27,7 +28,7 @@ const MultipleSelectedValue: FC<MultipleSelectedValueProps> = ({ values, onRemov
|
||||
return <>
|
||||
{values.map(item => (
|
||||
<div
|
||||
className="vm-select-input-content__selected"
|
||||
className={`vm-select-input-content__selected ${itemClassName} ${item.toLowerCase().replace(" ", "-")}`}
|
||||
key={item}
|
||||
>
|
||||
<span>{item}</span>
|
||||
|
||||
@@ -11,6 +11,7 @@ import useEventListener from "../../../hooks/useEventListener";
|
||||
import useClickOutside from "../../../hooks/useClickOutside";
|
||||
|
||||
interface SelectProps {
|
||||
itemClassName?: string
|
||||
value: string | string[]
|
||||
list: string[]
|
||||
label?: string
|
||||
@@ -20,6 +21,7 @@ interface SelectProps {
|
||||
searchable?: boolean
|
||||
autofocus?: boolean
|
||||
disabled?: boolean
|
||||
includeAll?: boolean
|
||||
onChange: (value: string) => void
|
||||
}
|
||||
|
||||
@@ -27,12 +29,14 @@ const Select: FC<SelectProps> = ({
|
||||
value,
|
||||
list,
|
||||
label,
|
||||
itemClassName,
|
||||
placeholder,
|
||||
noOptionsText,
|
||||
clearable = false,
|
||||
searchable = false,
|
||||
autofocus,
|
||||
disabled,
|
||||
includeAll,
|
||||
onChange
|
||||
}) => {
|
||||
const { isDarkTheme } = useAppState();
|
||||
@@ -46,7 +50,7 @@ const Select: FC<SelectProps> = ({
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const isMultiple = Array.isArray(value);
|
||||
const selectedValues = Array.isArray(value) ? value : undefined;
|
||||
const selectedValues = Array.isArray(value) ? value.slice() : [];
|
||||
const hideInput = isMobile && isMultiple && !!selectedValues?.length;
|
||||
|
||||
const textFieldValue = useMemo(() => {
|
||||
@@ -119,6 +123,9 @@ const Select: FC<SelectProps> = ({
|
||||
useEventListener("keyup", handleKeyUp);
|
||||
useClickOutside(autocompleteAnchorEl, handleCloseList, wrapperRef);
|
||||
|
||||
includeAll && !list.includes("All") && list.push("All");
|
||||
includeAll && !selectedValues?.length && selectedValues.push("All");
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames({
|
||||
@@ -135,11 +142,12 @@ const Select: FC<SelectProps> = ({
|
||||
<div className="vm-select-input-content">
|
||||
{!!selectedValues?.length && (
|
||||
<MultipleSelectedValue
|
||||
itemClassName={itemClassName}
|
||||
values={selectedValues}
|
||||
onRemoveItem={handleSelected}
|
||||
/>
|
||||
)}
|
||||
{!hideInput && (
|
||||
{!hideInput && !selectedValues?.length && (
|
||||
<input
|
||||
value={textFieldValue}
|
||||
type="text"
|
||||
@@ -171,9 +179,10 @@ const Select: FC<SelectProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
<Autocomplete
|
||||
itemClassName={itemClassName}
|
||||
label={label}
|
||||
value={autocompleteValue}
|
||||
options={list.map(el => ({ value: el }))}
|
||||
options={list.map(l => ({ value: l }))}
|
||||
anchor={autocompleteAnchorEl}
|
||||
selected={selectedValues}
|
||||
minLength={1}
|
||||
|
||||
17
app/vmui/packages/vmui/src/constants/alerts.ts
Normal file
17
app/vmui/packages/vmui/src/constants/alerts.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { RuleType } from "../types";
|
||||
|
||||
export const RULE_TYPES: RuleType[] = [
|
||||
{
|
||||
id: "alerts",
|
||||
title: "Alerts",
|
||||
isDefault: true,
|
||||
},
|
||||
{
|
||||
title: "Recording",
|
||||
id: "recording",
|
||||
},
|
||||
{
|
||||
title: "All",
|
||||
id: "all",
|
||||
},
|
||||
];
|
||||
@@ -7,4 +7,11 @@ export const APP_TYPE = import.meta.env.VITE_APP_TYPE;
|
||||
export const APP_TYPE_VM = APP_TYPE === AppType.victoriametrics;
|
||||
export const APP_TYPE_ANOMALY = APP_TYPE === AppType.vmanomaly;
|
||||
|
||||
|
||||
export const isDefaultDatasourceType = (datasourceType: string): boolean => {
|
||||
switch (APP_TYPE) {
|
||||
case AppType.victoriametrics:
|
||||
return datasourceType == "prometheus" || datasourceType == "";
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
export const darkPalette = {
|
||||
"color-primary": "#589DF6",
|
||||
"color-primary": "#589df6",
|
||||
"color-secondary": "#316eca",
|
||||
"color-error": "#e5534b",
|
||||
"color-background-error": "#240705",
|
||||
"color-warning": "#c69026",
|
||||
"color-background-warning": "#221906",
|
||||
"color-info": "#539bf5",
|
||||
"color-background-info": "#021327",
|
||||
"color-success": "#57ab5a",
|
||||
"color-background-success": "#0e1b0e",
|
||||
"color-passive": "#a7acb3",
|
||||
"color-background-body": "#22272e",
|
||||
"color-background-block": "#2d333b",
|
||||
"color-background-tooltip": "rgba(22, 22, 22, 0.8)",
|
||||
"color-background-item": "#313944",
|
||||
"color-background-badge": "#4e5a6a",
|
||||
"color-background-hover": "#3D4652",
|
||||
"color-text": "#cdd9e5",
|
||||
"color-text-secondary": "#768390",
|
||||
"color-text-disabled": "#636e7b",
|
||||
@@ -25,15 +33,23 @@ export const darkPalette = {
|
||||
};
|
||||
|
||||
export const lightPalette = {
|
||||
"color-primary": "#3F51B5",
|
||||
"color-secondary": "#E91E63",
|
||||
"color-error": "#FD080E",
|
||||
"color-warning": "#FF8308",
|
||||
"color-info": "#03A9F4",
|
||||
"color-success": "#4CAF50",
|
||||
"color-primary": "#3f51b5",
|
||||
"color-secondary": "#e91e63",
|
||||
"color-error": "#fd080e",
|
||||
"color-background-error": "#ffd7d8",
|
||||
"color-warning": "#ff8308",
|
||||
"color-background-warning": "#ffd6ad",
|
||||
"color-info": "#03a9f4",
|
||||
"color-background-info": "#d7f2fe",
|
||||
"color-success": "#4caf50",
|
||||
"color-background-success": "#d4ecd5",
|
||||
"color-passive": "#5d6267",
|
||||
"color-background-body": "#FEFEFF",
|
||||
"color-background-block": "#FFFFFF",
|
||||
"color-background-tooltip": "rgba(80,80,80,0.9)",
|
||||
"color-background-item": "#f8f9fa",
|
||||
"color-background-badge": "#e1e4e7",
|
||||
"color-background-hover": "#edf0f2",
|
||||
"color-text": "#110f0f",
|
||||
"color-text-secondary": "#706F6F",
|
||||
"color-text-disabled": "#A09F9F",
|
||||
|
||||
@@ -18,7 +18,7 @@ const useFetchFlags = () => {
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const url = getUrlWithoutTenant(serverUrl).replace(/\/prometheus\/?$/, "");
|
||||
const url = getUrlWithoutTenant(serverUrl);
|
||||
const response = await fetch(`${url}/flags`);
|
||||
const data = await response.text();
|
||||
const flags = data.split("\n").filter(flag => flag.trim() !== "")
|
||||
|
||||
@@ -7,12 +7,20 @@ const useSearchParamsFromObject = () => {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
const setSearchParamsFromKeys = useCallback((objectParams: Record<string, string | number>) => {
|
||||
const hasSearchParams = !!Array.from(searchParams.values()).length;
|
||||
const hasSearchParams = !!searchParams.size;
|
||||
let hasChanged = false;
|
||||
|
||||
const newSearchParams = new URLSearchParams(searchParams);
|
||||
searchParams.keys().forEach(key => {
|
||||
if (!(key in objectParams)) {
|
||||
newSearchParams.delete(key);
|
||||
hasChanged = true;
|
||||
}
|
||||
});
|
||||
|
||||
Object.entries(objectParams).forEach(([key, value]) => {
|
||||
if (searchParams.get(key) !== `${value}`) {
|
||||
searchParams.set(key, `${value}`);
|
||||
if (newSearchParams.get(key) !== `${value}`) {
|
||||
newSearchParams.set(key, `${value}`);
|
||||
hasChanged = true;
|
||||
}
|
||||
});
|
||||
@@ -20,7 +28,7 @@ const useSearchParamsFromObject = () => {
|
||||
if (!hasChanged) return;
|
||||
|
||||
if (hasSearchParams) {
|
||||
setSearchParams(searchParams);
|
||||
setSearchParams(newSearchParams);
|
||||
} else {
|
||||
navigate(`?${searchParams.toString()}`, { replace: true });
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@ const ControlsAnomalyLayout: FC<ControlsProps> = ({
|
||||
displaySidebar,
|
||||
isMobile,
|
||||
headerSetup,
|
||||
accountIds
|
||||
accountIds,
|
||||
closeModal,
|
||||
}) => {
|
||||
|
||||
return (
|
||||
@@ -28,7 +29,11 @@ const ControlsAnomalyLayout: FC<ControlsProps> = ({
|
||||
{headerSetup?.stepControl && <StepConfigurator/>}
|
||||
{headerSetup?.timeSelector && <TimeSelector/>}
|
||||
{headerSetup?.cardinalityDatePicker && <CardinalityDatePicker/>}
|
||||
{headerSetup?.executionControls && <ExecutionControls/>}
|
||||
{headerSetup?.executionControls && <ExecutionControls
|
||||
tooltip={headerSetup?.executionControls?.tooltip}
|
||||
useAutorefresh={headerSetup?.executionControls?.useAutorefresh}
|
||||
closeModal={closeModal}
|
||||
/>}
|
||||
<GlobalSettings/>
|
||||
{!displaySidebar && <ShortcutKeys/>}
|
||||
</div>
|
||||
|
||||
@@ -105,6 +105,7 @@ const Header: FC<HeaderProps> = ({ controlsComponent }) => {
|
||||
controlsComponent={controlsComponent}
|
||||
displaySidebar={displaySidebar}
|
||||
isMobile={isMobile}
|
||||
closeModal={() => {}}
|
||||
/>
|
||||
</header>;
|
||||
};
|
||||
|
||||
@@ -18,6 +18,7 @@ export interface ControlsProps {
|
||||
isMobile?: boolean;
|
||||
headerSetup?: RouterOptionsHeader;
|
||||
accountIds?: string[];
|
||||
closeModal: () => void;
|
||||
}
|
||||
|
||||
const HeaderControls: FC<ControlsProps & HeaderProps> = ({
|
||||
@@ -45,6 +46,7 @@ const HeaderControls: FC<ControlsProps & HeaderProps> = ({
|
||||
isMobile={isMobile}
|
||||
accountIds={accountIds}
|
||||
headerSetup={headerSetup}
|
||||
closeModal={handleCloseList}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -14,7 +14,8 @@ const ControlsMainLayout: FC<ControlsProps> = ({
|
||||
displaySidebar,
|
||||
isMobile,
|
||||
headerSetup,
|
||||
accountIds
|
||||
accountIds,
|
||||
closeModal,
|
||||
}) => {
|
||||
|
||||
return (
|
||||
@@ -28,7 +29,11 @@ const ControlsMainLayout: FC<ControlsProps> = ({
|
||||
{headerSetup?.stepControl && <StepConfigurator/>}
|
||||
{headerSetup?.timeSelector && <TimeSelector/>}
|
||||
{headerSetup?.cardinalityDatePicker && <CardinalityDatePicker/>}
|
||||
{headerSetup?.executionControls && <ExecutionControls/>}
|
||||
{headerSetup?.executionControls && <ExecutionControls
|
||||
tooltip={headerSetup?.executionControls?.tooltip}
|
||||
useAutorefresh={headerSetup?.executionControls?.useAutorefresh}
|
||||
closeModal={closeModal}
|
||||
/>}
|
||||
<GlobalSettings/>
|
||||
{!displaySidebar && <ShortcutKeys/>}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import Spinner from "../../components/Main/Spinner/Spinner";
|
||||
import Alert from "../../components/Main/Alert/Alert";
|
||||
import { useFetchItem } from "./hooks/useFetchItem";
|
||||
import "./style.scss";
|
||||
import { Alert as APIAlert } from "../../types";
|
||||
import ItemHeader from "../../components/ExploreAlerts/ItemHeader";
|
||||
import BaseAlert from "../../components/ExploreAlerts/BaseAlert";
|
||||
import Modal from "../../components/Main/Modal/Modal";
|
||||
|
||||
interface ExploreAlertProps {
|
||||
groupId: string;
|
||||
id: string;
|
||||
mode: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const ExploreAlert = ({ groupId, id, mode, onClose }: ExploreAlertProps) => {
|
||||
const {
|
||||
item,
|
||||
isLoading,
|
||||
error,
|
||||
} = useFetchItem<APIAlert>({ groupId, id, mode });
|
||||
|
||||
if (isLoading) return (
|
||||
<Spinner />
|
||||
);
|
||||
|
||||
if (error) return (
|
||||
<Alert variant="error">{error}</Alert>
|
||||
);
|
||||
|
||||
const noItemFound = `No alert with group ID=${groupId}, alert ID=${id} found!`;
|
||||
const states = {
|
||||
firing: 1,
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className="vm-explore-alerts"
|
||||
title={item ? (
|
||||
<ItemHeader
|
||||
entity="alert"
|
||||
type="alerting"
|
||||
groupId={item.group_id}
|
||||
id={item.id}
|
||||
name={item.name}
|
||||
states={states}
|
||||
onClose={onClose}
|
||||
/>
|
||||
) : "Alert not found"}
|
||||
onClose={onClose}
|
||||
>
|
||||
<div className="vm-explore-alerts">
|
||||
{item && (<BaseAlert item={item} />) || (
|
||||
<Alert variant="info">{noItemFound}</Alert>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExploreAlert;
|
||||
@@ -0,0 +1,55 @@
|
||||
import Spinner from "../../components/Main/Spinner/Spinner";
|
||||
import Alert from "../../components/Main/Alert/Alert";
|
||||
import { useFetchGroup } from "./hooks/useFetchGroup";
|
||||
import "./style.scss";
|
||||
import { Group as APIGroup } from "../../types";
|
||||
import ItemHeader from "../../components/ExploreAlerts/ItemHeader";
|
||||
import BaseGroup from "../../components/ExploreAlerts/BaseGroup";
|
||||
import Modal from "../../components/Main/Modal/Modal";
|
||||
|
||||
interface ExploreGroupProps {
|
||||
id: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const ExploreGroup = ({ id, onClose }: ExploreGroupProps) => {
|
||||
const {
|
||||
group,
|
||||
isLoading,
|
||||
error,
|
||||
} = useFetchGroup<APIGroup>({ id });
|
||||
|
||||
if (isLoading) return (
|
||||
<Spinner />
|
||||
);
|
||||
|
||||
if (error) return (
|
||||
<Alert variant="error">{error}</Alert>
|
||||
);
|
||||
|
||||
const noGroupFound = `No group ID=${id} found!`;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className="vm-explore-alerts"
|
||||
title={group ? (
|
||||
<ItemHeader
|
||||
entity="group"
|
||||
groupId={id}
|
||||
name={group.name}
|
||||
states={group.states}
|
||||
onClose={onClose}
|
||||
/>
|
||||
) : "Rule not found"}
|
||||
onClose={onClose}
|
||||
>
|
||||
<div className="vm-explore-alerts">
|
||||
{group && (<BaseGroup group={group} />) || (
|
||||
<Alert variant="info">{noGroupFound}</Alert>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExploreGroup;
|
||||
@@ -0,0 +1,142 @@
|
||||
import { FC, useEffect, useState } from "preact/compat";
|
||||
import { useLocation } from "react-router";
|
||||
import { useNotifiersSetQueryParams as useSetQueryParams } from "./hooks/useSetQueryParams";
|
||||
import Spinner from "../../components/Main/Spinner/Spinner";
|
||||
import Alert from "../../components/Main/Alert/Alert";
|
||||
import Accordion from "../../components/Main/Accordion/Accordion";
|
||||
import { useFetchNotifiers } from "./hooks/useFetchNotifiers";
|
||||
import "./style.scss";
|
||||
import NotifiersHeader from "../../components/ExploreAlerts/NotifiersHeader";
|
||||
import NotifierHeader from "../../components/ExploreAlerts/NotifierHeader";
|
||||
import Target from "../../components/ExploreAlerts/Target";
|
||||
import { Notifier as APINotifier, Target as APITarget } from "../../types";
|
||||
import { getQueryStringValue } from "../../utils/query-string";
|
||||
import { getChanges } from "./helpers";
|
||||
import debounce from "lodash.debounce";
|
||||
|
||||
const defaultKindsStr = getQueryStringValue("kinds", "") as string;
|
||||
const defaultKinds = defaultKindsStr.split("&").filter((rt) => rt) as string[];
|
||||
const defaultSearchInput = getQueryStringValue("search", "") as string;
|
||||
|
||||
const ExploreNotifiers: FC = () => {
|
||||
const {
|
||||
notifiers,
|
||||
isLoading,
|
||||
error,
|
||||
} = useFetchNotifiers();
|
||||
|
||||
const [searchInput, setSearchInput] = useState(defaultSearchInput);
|
||||
const [kinds, setKinds] = useState(defaultKinds);
|
||||
|
||||
useSetQueryParams({
|
||||
kinds: kinds.join("&"),
|
||||
search: searchInput,
|
||||
});
|
||||
|
||||
const location = useLocation();
|
||||
const pageLoaded = !isLoading && !error && !!notifiers?.length;
|
||||
const savedScrollTop = localStorage.getItem("scrollTop");
|
||||
|
||||
useEffect(() => {
|
||||
if (!pageLoaded) return;
|
||||
if (location.hash) {
|
||||
const target = document.querySelector(location.hash);
|
||||
if (target) {
|
||||
let parent = target.closest("details");
|
||||
while (parent) {
|
||||
parent.open = true;
|
||||
if (!parent?.parentElement) return;
|
||||
parent = parent.parentElement.closest("details");
|
||||
}
|
||||
target.scrollIntoView();
|
||||
}
|
||||
} else {
|
||||
if (savedScrollTop) {
|
||||
window.scrollTo(0, parseInt(savedScrollTop));
|
||||
}
|
||||
const handleBeforeUnload = () => {
|
||||
localStorage.setItem("scrollTop", (window.scrollY || 0).toString());
|
||||
};
|
||||
window.addEventListener("beforeunload", handleBeforeUnload);
|
||||
return () => {
|
||||
window.removeEventListener("beforeunload", handleBeforeUnload);
|
||||
};
|
||||
}
|
||||
}, [location, savedScrollTop, pageLoaded]);
|
||||
|
||||
const handleChangeSearch = (input: string) => {
|
||||
if (!input) {
|
||||
setSearchInput("");
|
||||
} else {
|
||||
setSearchInput(input);
|
||||
}
|
||||
};
|
||||
|
||||
const allKinds: Set<string> = new Set();
|
||||
const filteredNotifiers: APINotifier[] = [];
|
||||
|
||||
notifiers.forEach((notifier) => {
|
||||
const filteredTargets: APITarget[] = [];
|
||||
const targets = notifier.targets || [];
|
||||
targets.forEach((target) => {
|
||||
allKinds.add(notifier.kind);
|
||||
if (kinds?.length && !kinds.includes(notifier.kind)) return;
|
||||
if (
|
||||
searchInput &&
|
||||
!target.address.toLowerCase().includes(searchInput.toLowerCase()) &&
|
||||
!notifier.kind.toLowerCase().includes(searchInput.toLowerCase())
|
||||
)
|
||||
return;
|
||||
filteredTargets.push(target);
|
||||
});
|
||||
if (filteredTargets.length) {
|
||||
const n = Object.assign({}, notifier);
|
||||
n.targets = filteredTargets;
|
||||
filteredNotifiers.push(n);
|
||||
}
|
||||
});
|
||||
|
||||
const handleChangeKinds = (title: string) => {
|
||||
setKinds(getChanges(title, kinds));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="vm-explore-alerts">
|
||||
<NotifiersHeader
|
||||
kinds={kinds}
|
||||
allKinds={Array.from(allKinds)}
|
||||
onChangeKinds={handleChangeKinds}
|
||||
onChangeSearch={debounce(handleChangeSearch, 500)}
|
||||
/>
|
||||
{(isLoading && <Spinner />) || (error && <Alert variant="error">{error}</Alert>) || (
|
||||
!filteredNotifiers.length && <Alert variant="info">No notifiers found!</Alert>
|
||||
) || (
|
||||
<div className="vm-explore-alerts-body">
|
||||
{filteredNotifiers.map((notifier) => (
|
||||
<div
|
||||
key={notifier.kind}
|
||||
className="vm-explore-alert-group vm-block vm-block_empty-padding"
|
||||
>
|
||||
<Accordion
|
||||
key={`notifier-${notifier.kind}`}
|
||||
id={`notifier-${notifier.kind}`}
|
||||
title={<NotifierHeader notifier={notifier} />}
|
||||
>
|
||||
<div className="vm-explore-alerts-items">
|
||||
{notifier.targets.map((target) => (
|
||||
<Target
|
||||
key={`target-${target.address}`}
|
||||
target={target}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Accordion>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExploreNotifiers;
|
||||
@@ -0,0 +1,60 @@
|
||||
import Spinner from "../../components/Main/Spinner/Spinner";
|
||||
import Alert from "../../components/Main/Alert/Alert";
|
||||
import { useFetchItem } from "./hooks/useFetchItem";
|
||||
import "./style.scss";
|
||||
import { Rule as APIRule } from "../../types";
|
||||
import ItemHeader from "../../components/ExploreAlerts/ItemHeader";
|
||||
import BaseRule from "../../components/ExploreAlerts/BaseRule";
|
||||
import Modal from "../../components/Main/Modal/Modal";
|
||||
import { getStates } from "./helpers";
|
||||
|
||||
interface ExploreRuleProps {
|
||||
groupId: string;
|
||||
id: string;
|
||||
mode: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const ExploreRule = ({ groupId, id, mode, onClose }: ExploreRuleProps) => {
|
||||
const {
|
||||
item,
|
||||
isLoading,
|
||||
error,
|
||||
} = useFetchItem<APIRule>({ groupId, id, mode });
|
||||
|
||||
if (isLoading) return (
|
||||
<Spinner />
|
||||
);
|
||||
|
||||
if (error) return (
|
||||
<Alert variant="error">{error}</Alert>
|
||||
);
|
||||
|
||||
const noItemFound = `No rule with group ID=${groupId}, rule ID=${id} found!`;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className="vm-explore-alerts"
|
||||
title={item ? (
|
||||
<ItemHeader
|
||||
entity="rule"
|
||||
type={item.type}
|
||||
groupId={item.group_id}
|
||||
states={getStates(item)}
|
||||
id={item.id}
|
||||
name={item.name}
|
||||
onClose={onClose}
|
||||
/>
|
||||
) : "Rule not found"}
|
||||
onClose={onClose}
|
||||
>
|
||||
<div className="vm-explore-alerts">
|
||||
{item && (<BaseRule item={item} />) || (
|
||||
<Alert variant="info">{noItemFound}</Alert>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExploreRule;
|
||||
206
app/vmui/packages/vmui/src/pages/ExploreAlerts/ExploreRules.tsx
Normal file
206
app/vmui/packages/vmui/src/pages/ExploreAlerts/ExploreRules.tsx
Normal file
@@ -0,0 +1,206 @@
|
||||
import { FC, useEffect, useMemo, useState, useCallback } from "preact/compat";
|
||||
import { useNavigate, useLocation, useSearchParams } from "react-router";
|
||||
import { useRulesSetQueryParams as useSetQueryParams } from "./hooks/useSetQueryParams";
|
||||
import Spinner from "../../components/Main/Spinner/Spinner";
|
||||
import Alert from "../../components/Main/Alert/Alert";
|
||||
import Accordion from "../../components/Main/Accordion/Accordion";
|
||||
import { useFetchGroups } from "./hooks/useFetchGroups";
|
||||
import "./style.scss";
|
||||
import RulesHeader from "../../components/ExploreAlerts/RulesHeader";
|
||||
import GroupHeader from "../../components/ExploreAlerts/GroupHeader";
|
||||
import Rule from "../../components/ExploreAlerts/Rule";
|
||||
import ExploreRule from "../../pages/ExploreAlerts/ExploreRule";
|
||||
import ExploreAlert from "../../pages/ExploreAlerts/ExploreAlert";
|
||||
import ExploreGroup from "../../pages/ExploreAlerts/ExploreGroup";
|
||||
import { getQueryStringValue } from "../../utils/query-string";
|
||||
import { getStates, getChanges, filterGroups } from "./helpers";
|
||||
import debounce from "lodash.debounce";
|
||||
|
||||
const defaultTypesStr = getQueryStringValue("types", "") as string;
|
||||
const defaultTypes = defaultTypesStr.split("&").filter((rt) => rt) as string[];
|
||||
const defaultStatesStr = getQueryStringValue("states", "") as string;
|
||||
const defaultStates = defaultStatesStr.split("&").filter((s) => s) as string[];
|
||||
const defaultSearchInput = getQueryStringValue("search", "") as string;
|
||||
|
||||
const ExploreRules: FC = () => {
|
||||
const groupId = getQueryStringValue("group_id", "") as string;
|
||||
const ruleId = getQueryStringValue("rule_id", "") as string;
|
||||
const alertId = getQueryStringValue("alert_id", "") as string;
|
||||
|
||||
const [searchInput, setSearchInput] = useState(defaultSearchInput);
|
||||
const [types, setTypes] = useState(defaultTypes);
|
||||
const [states, setStates] = useState(defaultStates);
|
||||
const [modalOpen, setModalOpen] = useState(true);
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
if (!location.hash && groupId) {
|
||||
setModalOpen(true);
|
||||
} else {
|
||||
setModalOpen(false);
|
||||
}
|
||||
}, [location.hash, groupId]);
|
||||
|
||||
useSetQueryParams({
|
||||
types: types.join("&"),
|
||||
states: states.join("&"),
|
||||
search: searchInput,
|
||||
group_id: groupId,
|
||||
alert_id: alertId,
|
||||
rule_id: ruleId,
|
||||
});
|
||||
|
||||
const handleChangeSearch = useCallback((input: string) => {
|
||||
if (!input) {
|
||||
setSearchInput("");
|
||||
} else {
|
||||
setSearchInput(input);
|
||||
}
|
||||
}, [searchInput]);
|
||||
|
||||
const getModal = () => {
|
||||
if (ruleId !== "") {
|
||||
return (
|
||||
<ExploreRule
|
||||
groupId={groupId}
|
||||
id={ruleId}
|
||||
mode={ruleId !== "" ? "rule" : "alert"}
|
||||
onClose={handleClose(`rule-${ruleId}`)}
|
||||
/>
|
||||
);
|
||||
} else if (alertId !== "") {
|
||||
return (
|
||||
<ExploreAlert
|
||||
groupId={groupId}
|
||||
id={alertId}
|
||||
mode={ruleId !== "" ? "rule" : "alert"}
|
||||
onClose={handleClose(`alert-${alertId}`)}
|
||||
/>
|
||||
);
|
||||
} else if (groupId !== "") {
|
||||
return (
|
||||
<ExploreGroup
|
||||
id={groupId}
|
||||
onClose={handleClose(`group-${groupId}`)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleChangeStates = useCallback((title: string) => {
|
||||
setStates(getChanges(title, states));
|
||||
}, [states]);
|
||||
|
||||
const handleChangeTypes = useCallback((title: string) => {
|
||||
setTypes(getChanges(title, types));
|
||||
}, [types]);
|
||||
|
||||
const noRuleFound = "No rules found!";
|
||||
|
||||
const handleClose = (id: string) => {
|
||||
return () => {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.delete("group_id");
|
||||
newParams.delete("rule_id");
|
||||
newParams.delete("alert_id");
|
||||
setSearchParams(newParams);
|
||||
setModalOpen(false);
|
||||
navigate({
|
||||
hash: `#${id}`,
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
const {
|
||||
groups,
|
||||
isLoading,
|
||||
error,
|
||||
} = useFetchGroups({ blockFetch: modalOpen });
|
||||
|
||||
const pageLoaded = !isLoading && !error && !!groups?.length;
|
||||
const savedScrollTop = localStorage.getItem("scrollTop");
|
||||
|
||||
useEffect(() => {
|
||||
if (!pageLoaded) return;
|
||||
if (location.hash) {
|
||||
const target = document.querySelector(location.hash);
|
||||
if (target) {
|
||||
let parent = target.closest("details");
|
||||
while (parent) {
|
||||
parent.open = true;
|
||||
if (!parent?.parentElement) return;
|
||||
parent = parent.parentElement.closest("details");
|
||||
}
|
||||
target.scrollIntoView();
|
||||
}
|
||||
} else {
|
||||
if (savedScrollTop) {
|
||||
window.scrollTo(0, parseInt(savedScrollTop));
|
||||
}
|
||||
const updateScrollPosition = () => {
|
||||
localStorage.setItem("scrollTop", (window.scrollY || 0).toString());
|
||||
};
|
||||
window.addEventListener("scroll", updateScrollPosition);
|
||||
return () => {
|
||||
window.removeEventListener("scroll", updateScrollPosition);
|
||||
};
|
||||
}
|
||||
}, [location, savedScrollTop, pageLoaded]);
|
||||
|
||||
const { filteredGroups, allTypes, allStates } = useMemo(
|
||||
() => filterGroups(groups || [], types, states, searchInput),
|
||||
[groups, types, states, searchInput]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{modalOpen && getModal()}
|
||||
{(!modalOpen || !!allStates?.size) && (
|
||||
<div className="vm-explore-alerts">
|
||||
<RulesHeader
|
||||
types={types}
|
||||
allTypes={Array.from(allTypes)}
|
||||
states={states}
|
||||
allStates={Array.from(allStates)}
|
||||
onChangeTypes={handleChangeTypes}
|
||||
onChangeStates={handleChangeStates}
|
||||
onChangeSearch={debounce(handleChangeSearch, 500)}
|
||||
/>
|
||||
{(isLoading && <Spinner />) || (error && <Alert variant="error">{error}</Alert>) || (
|
||||
!filteredGroups.length && <Alert variant="info">{noRuleFound}</Alert>
|
||||
) || (
|
||||
<div className="vm-explore-alerts-body">
|
||||
{filteredGroups.map((group) => (
|
||||
<div
|
||||
key={group.id}
|
||||
className="vm-explore-alert-group vm-block vm-block_empty-padding"
|
||||
>
|
||||
<Accordion
|
||||
key={`group-${group.id}`}
|
||||
id={`group-${group.id}`}
|
||||
title={<GroupHeader group={group} />}
|
||||
>
|
||||
<div className="vm-explore-alerts-items">
|
||||
{group.rules.map((rule) => (
|
||||
<Rule
|
||||
key={`rule-${rule.id}`}
|
||||
rule={rule}
|
||||
states={getStates(rule)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Accordion>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExploreRules;
|
||||
88
app/vmui/packages/vmui/src/pages/ExploreAlerts/helpers.ts
Normal file
88
app/vmui/packages/vmui/src/pages/ExploreAlerts/helpers.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { Rule, Group } from "../../types";
|
||||
|
||||
export const getChanges = (title: string, prevValues: string[]): string[] => {
|
||||
if (title === "All") return [];
|
||||
|
||||
const newValues = new Set<string>(prevValues);
|
||||
if (newValues.has(title)) {
|
||||
newValues.delete(title);
|
||||
} else {
|
||||
newValues.add(title);
|
||||
}
|
||||
|
||||
return Array.from(newValues);
|
||||
};
|
||||
|
||||
export const getState = (rule: Rule) => {
|
||||
let state = rule?.state || "ok";
|
||||
if (rule?.health !== "ok") {
|
||||
state = "unhealthy";
|
||||
} else if (!rule?.lastSamples && !rule?.lastSeriesFetched) {
|
||||
state = "no match";
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
export const getStates = (rule: Rule) => {
|
||||
const output: Record<string, number> = {};
|
||||
const alertsCount = rule?.alerts?.length || 0;
|
||||
if (alertsCount > 0) {
|
||||
rule.alerts.forEach((alert) => {
|
||||
if (alert.state in output) {
|
||||
output[alert.state] += 1;
|
||||
} else {
|
||||
output[alert.state] = 1;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
output[getState(rule)] = 1;
|
||||
}
|
||||
return output;
|
||||
};
|
||||
|
||||
export const filterGroups = (groups: Group[], types: string[], states: string[], searchInput: string) => {
|
||||
const allTypes: Set<string> = new Set();
|
||||
const allStates: Set<string> = new Set();
|
||||
const filteredGroups: Group[] = [];
|
||||
|
||||
groups.forEach((group) => {
|
||||
const filteredRules: Rule[] = [];
|
||||
const statesPerGroup: Record<string, number> = {};
|
||||
group.rules.forEach((rule) => {
|
||||
const ruleType = rule.type.charAt(0).toUpperCase() + rule.type.slice(1);
|
||||
allTypes.add(ruleType);
|
||||
if (types?.length && !types.includes(ruleType)) return;
|
||||
|
||||
const state = getState(rule);
|
||||
const stateName = state.charAt(0).toUpperCase() + state.slice(1);
|
||||
allStates.add(stateName);
|
||||
if (states?.length && !states.includes(stateName)) return;
|
||||
|
||||
if (
|
||||
searchInput &&
|
||||
!rule.name.toLowerCase().includes(searchInput.toLowerCase()) &&
|
||||
!group.name.toLowerCase().includes(searchInput.toLowerCase()) &&
|
||||
!group.file.toLowerCase().includes(searchInput.toLowerCase())
|
||||
)
|
||||
return;
|
||||
|
||||
filteredRules.push(rule);
|
||||
if (state !== "no match" && state !== "unhealthy" && state !== "firing" && state !== "pending")
|
||||
return;
|
||||
|
||||
const count = state === "firing" || state === "pending" ? rule?.alerts?.length : 1;
|
||||
if (stateName in statesPerGroup) {
|
||||
statesPerGroup[stateName] += count;
|
||||
} else {
|
||||
statesPerGroup[stateName] = count;
|
||||
}
|
||||
});
|
||||
if (filteredRules.length) {
|
||||
const g = Object.assign({}, group);
|
||||
g.rules = filteredRules;
|
||||
g.states = statesPerGroup;
|
||||
filteredGroups.push(g);
|
||||
}
|
||||
});
|
||||
return { filteredGroups, allTypes, allStates };
|
||||
};
|
||||
@@ -0,0 +1,71 @@
|
||||
import { useTimeState } from "../../../state/time/TimeStateContext";
|
||||
import { useEffect, useMemo, useState } from "preact/compat";
|
||||
import { getGroupUrl } from "../../../api/explore-alerts";
|
||||
import { useAppState } from "../../../state/common/StateContext";
|
||||
import { ErrorTypes } from "../../../types";
|
||||
|
||||
interface FetchGroupReturn<T> {
|
||||
group?: T;
|
||||
isLoading: boolean;
|
||||
error?: ErrorTypes | string;
|
||||
}
|
||||
|
||||
interface FetchGroupProps {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export const useFetchGroup = <T>({
|
||||
id,
|
||||
}: FetchGroupProps): FetchGroupReturn<T> => {
|
||||
const { serverUrl } = useAppState();
|
||||
const { period } = useTimeState();
|
||||
|
||||
const [group, setGroup] = useState<T>();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<ErrorTypes | string>();
|
||||
|
||||
const fetchUrl = useMemo(
|
||||
() => getGroupUrl(serverUrl, id),
|
||||
[serverUrl, id],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await fetch(fetchUrl);
|
||||
switch (response.headers.get("Content-Type")) {
|
||||
case "application/json": {
|
||||
const resp = await response.json();
|
||||
if (response.ok) {
|
||||
setGroup(resp as T);
|
||||
setError(undefined);
|
||||
} else {
|
||||
setError(`${resp.errorType}\r\n${resp?.error}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
let err = await response.text();
|
||||
if (err.startsWith("unsupported path requested")) {
|
||||
err = `Failed to show group details. Request to ${fetchUrl} failed with error: ${err.trim()}.\nMake sure that vmalert is reachable at ${fetchUrl} and is of the same or higher version than vmselect`;
|
||||
} else {
|
||||
err = `${response.statusText}\r\n${err}`;
|
||||
}
|
||||
setError(err);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
setError(`${e.name}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
fetchData().catch(console.error);
|
||||
}, [fetchUrl, period]);
|
||||
|
||||
return { group, isLoading, error };
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
import { useTimeState } from "../../../state/time/TimeStateContext";
|
||||
import { useEffect, useMemo, useState } from "preact/compat";
|
||||
import { getGroupsUrl } from "../../../api/explore-alerts";
|
||||
import { useAppState } from "../../../state/common/StateContext";
|
||||
import { ErrorTypes, Group } from "../../../types";
|
||||
|
||||
interface FetchGroupsReturn {
|
||||
groups: Group[];
|
||||
isLoading: boolean;
|
||||
error?: ErrorTypes | string;
|
||||
}
|
||||
|
||||
interface FetchGroupsProps {
|
||||
blockFetch: boolean
|
||||
}
|
||||
|
||||
export const useFetchGroups = ({ blockFetch }: FetchGroupsProps): FetchGroupsReturn => {
|
||||
const { serverUrl } = useAppState();
|
||||
const { period } = useTimeState();
|
||||
|
||||
const [groups, setGroups] = useState<Group[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<ErrorTypes | string>();
|
||||
|
||||
const fetchUrl = useMemo(
|
||||
() => getGroupsUrl(serverUrl),
|
||||
[serverUrl],
|
||||
);
|
||||
|
||||
const loaded = !!groups.length || !blockFetch;
|
||||
|
||||
useEffect(() => {
|
||||
if (blockFetch) return;
|
||||
const fetchData = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await fetch(fetchUrl);
|
||||
const resp = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
const data = (resp.data.groups || []) as Group[];
|
||||
setGroups(data.sort((a, b) => a.name.localeCompare(b.name)));
|
||||
setError(undefined);
|
||||
} else {
|
||||
setError(`${resp.errorType}\r\n${resp?.error}`);
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
setError(`${e.name}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
fetchData().catch(console.error);
|
||||
}, [fetchUrl, period, loaded]);
|
||||
|
||||
return { groups, isLoading, error };
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
import { useTimeState } from "../../../state/time/TimeStateContext";
|
||||
import { useEffect, useMemo, useState } from "preact/compat";
|
||||
import { getItemUrl } from "../../../api/explore-alerts";
|
||||
import { useAppState } from "../../../state/common/StateContext";
|
||||
import { ErrorTypes } from "../../../types";
|
||||
|
||||
interface FetchItemReturn<T> {
|
||||
item?: T;
|
||||
isLoading: boolean;
|
||||
error?: ErrorTypes | string;
|
||||
}
|
||||
|
||||
interface FetchItemProps {
|
||||
groupId: string;
|
||||
id: string;
|
||||
mode: string;
|
||||
}
|
||||
|
||||
export const useFetchItem = <T>({
|
||||
groupId,
|
||||
id,
|
||||
mode,
|
||||
}: FetchItemProps): FetchItemReturn<T> => {
|
||||
const { serverUrl } = useAppState();
|
||||
const { period } = useTimeState();
|
||||
|
||||
const [item, setItem] = useState<T>();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<ErrorTypes | string>();
|
||||
|
||||
const fetchUrl = useMemo(
|
||||
() => getItemUrl(serverUrl, groupId, id, mode),
|
||||
[serverUrl, groupId, id, mode],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await fetch(fetchUrl);
|
||||
const resp = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
setItem(resp as T);
|
||||
setError(undefined);
|
||||
} else {
|
||||
setError(`${resp.errorType}\r\n${resp?.error}`);
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
setError(`${e.name}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
fetchData().catch(console.error);
|
||||
}, [fetchUrl, period]);
|
||||
|
||||
return { item, isLoading, error };
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
import { useTimeState } from "../../../state/time/TimeStateContext";
|
||||
import { useEffect, useMemo, useState } from "preact/compat";
|
||||
import { getNotifiersUrl } from "../../../api/explore-alerts";
|
||||
import { useAppState } from "../../../state/common/StateContext";
|
||||
import { Notifier, ErrorTypes } from "../../../types";
|
||||
|
||||
interface FetchNotifiersReturn {
|
||||
notifiers: Notifier[];
|
||||
isLoading: boolean;
|
||||
error?: ErrorTypes | string;
|
||||
}
|
||||
|
||||
export const useFetchNotifiers = (): FetchNotifiersReturn => {
|
||||
const { serverUrl } = useAppState();
|
||||
const { period } = useTimeState();
|
||||
|
||||
const [notifiers, setNotifiers] = useState<Notifier[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<ErrorTypes | string>();
|
||||
|
||||
const fetchUrl = useMemo(() => getNotifiersUrl(serverUrl), [serverUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await fetch(fetchUrl);
|
||||
const resp = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
const data = (resp.data.notifiers || []) as Notifier[];
|
||||
setNotifiers(data.sort((a, b) => a.kind.localeCompare(b.kind)));
|
||||
setError(undefined);
|
||||
} else {
|
||||
setError(`${resp.errorType}\r\n${resp?.error}`);
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
setError(`${e.name}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
fetchData().catch(console.error);
|
||||
}, [fetchUrl, period]);
|
||||
|
||||
return { notifiers, isLoading, error };
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
import { useEffect } from "react";
|
||||
import { compactObject } from "../../../utils/object";
|
||||
import useSearchParamsFromObject from "../../../hooks/useSearchParamsFromObject";
|
||||
|
||||
interface rulesQueryProps {
|
||||
types?: string;
|
||||
states?: string;
|
||||
search?: string;
|
||||
rule_id: string;
|
||||
group_id: string;
|
||||
alert_id: string;
|
||||
}
|
||||
|
||||
export const useRulesSetQueryParams = ({
|
||||
types,
|
||||
states,
|
||||
search,
|
||||
rule_id,
|
||||
alert_id,
|
||||
group_id,
|
||||
}: rulesQueryProps) => {
|
||||
const { setSearchParamsFromKeys } = useSearchParamsFromObject();
|
||||
|
||||
const setSearchParamsFromState = () => {
|
||||
const params = compactObject({
|
||||
types,
|
||||
states,
|
||||
search,
|
||||
alert_id,
|
||||
rule_id,
|
||||
group_id,
|
||||
});
|
||||
|
||||
setSearchParamsFromKeys(params);
|
||||
};
|
||||
|
||||
useEffect(setSearchParamsFromState, [
|
||||
types,
|
||||
states,
|
||||
search,
|
||||
rule_id,
|
||||
group_id,
|
||||
alert_id,
|
||||
]);
|
||||
};
|
||||
|
||||
interface notifiersQueryProps {
|
||||
kinds: string;
|
||||
search: string;
|
||||
}
|
||||
|
||||
export const useNotifiersSetQueryParams = ({
|
||||
kinds,
|
||||
search,
|
||||
}: notifiersQueryProps) => {
|
||||
const { setSearchParamsFromKeys } = useSearchParamsFromObject();
|
||||
|
||||
const setSearchParamsFromState = () => {
|
||||
const params = compactObject({
|
||||
kinds,
|
||||
search,
|
||||
});
|
||||
|
||||
setSearchParamsFromKeys(params);
|
||||
};
|
||||
|
||||
useEffect(setSearchParamsFromState, [kinds, search]);
|
||||
};
|
||||
77
app/vmui/packages/vmui/src/pages/ExploreAlerts/style.scss
Normal file
77
app/vmui/packages/vmui/src/pages/ExploreAlerts/style.scss
Normal file
@@ -0,0 +1,77 @@
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-explore-alert-group {
|
||||
width: 100%;
|
||||
&:has(.vm-accordion-header_open) {
|
||||
border: $border-divider;
|
||||
border-radius: $border-radius-small;
|
||||
}
|
||||
}
|
||||
|
||||
.vm-explore-alerts.vm-modal {
|
||||
align-items: center;
|
||||
.vm-explore-rule-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: $padding-tiny;
|
||||
}
|
||||
.vm-modal-content {
|
||||
max-width: 90vw;
|
||||
}
|
||||
}
|
||||
|
||||
.vm-list-item-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: $padding-tiny;
|
||||
}
|
||||
|
||||
.vm-explore-alerts-items {
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
row-gap: 10px;
|
||||
padding: 10px;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.vm-explore-alerts-notifier {
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
row-gap: 10px;
|
||||
padding: $padding-tiny 0;
|
||||
}
|
||||
|
||||
.vm-explore-alerts {
|
||||
font-size: 12px;
|
||||
.vm-modal-content-header__title {
|
||||
max-width: unset;
|
||||
}
|
||||
|
||||
.vm-accordion-header {
|
||||
padding-right: 40px;
|
||||
}
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: $padding-medium;
|
||||
max-width: calc(100vw - var(--scrollbar-width));
|
||||
|
||||
@media (max-width: 500px) {
|
||||
gap: $padding-small;
|
||||
}
|
||||
|
||||
&-body {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: $padding-medium;
|
||||
|
||||
@media (max-width: 500px) {
|
||||
gap: $padding-small;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -98,7 +98,7 @@ const RawQueryPage: FC = () => {
|
||||
})}
|
||||
>
|
||||
<QueryConfigurator
|
||||
label={"Time series selector"}
|
||||
label="Time series selector"
|
||||
queryErrors={!hideError ? queryErrors : []}
|
||||
setQueryErrors={setQueryErrors}
|
||||
setHideError={setHideError}
|
||||
|
||||
@@ -15,20 +15,28 @@ const router = {
|
||||
rawQuery: "/raw-query",
|
||||
downsamplingDebug: "/downsampling-filters-debug",
|
||||
retentionDebug: "/retention-filters-debug",
|
||||
alerts: "/alerts",
|
||||
rules: "/rules",
|
||||
notifiers: "/notifiers",
|
||||
};
|
||||
|
||||
export interface RouterOptionsHeader {
|
||||
tenant?: boolean,
|
||||
stepControl?: boolean,
|
||||
timeSelector?: boolean,
|
||||
executionControls?: boolean,
|
||||
globalSettings?: boolean,
|
||||
cardinalityDatePicker?: boolean
|
||||
tenant?: boolean;
|
||||
stepControl?: boolean;
|
||||
timeSelector?: boolean;
|
||||
executionControls?: ExecutionControlsProps;
|
||||
globalSettings?: boolean;
|
||||
cardinalityDatePicker?: boolean;
|
||||
}
|
||||
|
||||
export interface RouterOptions {
|
||||
title?: string,
|
||||
header: RouterOptionsHeader
|
||||
title?: string;
|
||||
header: RouterOptionsHeader;
|
||||
}
|
||||
|
||||
interface ExecutionControlsProps {
|
||||
tooltip: string;
|
||||
useAutorefresh: boolean;
|
||||
}
|
||||
|
||||
const routerOptionsDefault = {
|
||||
@@ -36,18 +44,21 @@ const routerOptionsDefault = {
|
||||
tenant: true,
|
||||
stepControl: true,
|
||||
timeSelector: true,
|
||||
executionControls: true,
|
||||
}
|
||||
executionControls: {
|
||||
tooltip: "Refresh dashboard",
|
||||
useAutorefresh: true,
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export const routerOptions: { [key: string]: RouterOptions } = {
|
||||
[router.home]: {
|
||||
title: "Query",
|
||||
...routerOptionsDefault
|
||||
...routerOptionsDefault,
|
||||
},
|
||||
[router.rawQuery]: {
|
||||
title: "Raw query",
|
||||
...routerOptionsDefault
|
||||
...routerOptionsDefault,
|
||||
},
|
||||
[router.metrics]: {
|
||||
title: "Explore Prometheus metrics",
|
||||
@@ -55,65 +66,83 @@ export const routerOptions: { [key: string]: RouterOptions } = {
|
||||
tenant: true,
|
||||
stepControl: true,
|
||||
timeSelector: true,
|
||||
}
|
||||
},
|
||||
},
|
||||
[router.cardinality]: {
|
||||
title: "Explore cardinality",
|
||||
header: {
|
||||
tenant: true,
|
||||
cardinalityDatePicker: true,
|
||||
}
|
||||
},
|
||||
},
|
||||
[router.topQueries]: {
|
||||
title: "Top queries",
|
||||
header: {
|
||||
tenant: true,
|
||||
}
|
||||
},
|
||||
},
|
||||
[router.trace]: {
|
||||
title: "Trace analyzer",
|
||||
header: {}
|
||||
header: {},
|
||||
},
|
||||
[router.queryAnalyzer]: {
|
||||
title: "Query analyzer",
|
||||
header: {}
|
||||
header: {},
|
||||
},
|
||||
[router.dashboards]: {
|
||||
title: "Dashboards",
|
||||
...routerOptionsDefault,
|
||||
},
|
||||
[router.rules]: {
|
||||
title: "Rules",
|
||||
header: {
|
||||
executionControls: {
|
||||
tooltip: "Refresh alerts",
|
||||
useAutorefresh: false,
|
||||
}
|
||||
},
|
||||
},
|
||||
[router.notifiers]: {
|
||||
title: "Notifiers",
|
||||
header: {
|
||||
executionControls: {
|
||||
tooltip: "Refresh notifiers",
|
||||
useAutorefresh: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
[router.withTemplate]: {
|
||||
title: "WITH templates",
|
||||
header: {}
|
||||
header: {},
|
||||
},
|
||||
[router.relabel]: {
|
||||
title: "Metric relabel debug",
|
||||
header: {}
|
||||
header: {},
|
||||
},
|
||||
[router.activeQueries]: {
|
||||
title: "Active Queries",
|
||||
header: {}
|
||||
header: {},
|
||||
},
|
||||
[router.icons]: {
|
||||
title: "Icons",
|
||||
header: {}
|
||||
header: {},
|
||||
},
|
||||
[router.anomaly]: {
|
||||
title: "Anomaly exploration",
|
||||
...routerOptionsDefault
|
||||
...routerOptionsDefault,
|
||||
},
|
||||
[router.query]: {
|
||||
title: "Query",
|
||||
...routerOptionsDefault
|
||||
...routerOptionsDefault,
|
||||
},
|
||||
[router.downsamplingDebug]: {
|
||||
title: "Downsampling filters debug",
|
||||
header: {}
|
||||
header: {},
|
||||
},
|
||||
[router.retentionDebug]: {
|
||||
title: "Retention filters debug",
|
||||
header: {}
|
||||
}
|
||||
header: {},
|
||||
},
|
||||
};
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import router, { routerOptions } from "./index";
|
||||
import { getTenantIdFromUrl } from "../utils/tenants";
|
||||
|
||||
export enum NavigationItemType {
|
||||
internalLink,
|
||||
@@ -21,21 +20,6 @@ interface NavigationConfig {
|
||||
showAlertLink: boolean,
|
||||
}
|
||||
|
||||
/**
|
||||
* Special case for alert link
|
||||
*/
|
||||
const getAlertLink = (url: string, showAlertLink: boolean) => {
|
||||
// see more https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/#vmalert
|
||||
const isCluster = !!getTenantIdFromUrl(url);
|
||||
const value = isCluster ? `${url}/vmalert` : url.replace(/\/prometheus$/, "/vmalert");
|
||||
return {
|
||||
label: "Alerts",
|
||||
value,
|
||||
type: NavigationItemType.externalLink,
|
||||
hide: !showAlertLink,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Submenu for Tools tab
|
||||
*/
|
||||
@@ -58,11 +42,19 @@ const getExploreNav = () => [
|
||||
{ value: router.activeQueries },
|
||||
];
|
||||
|
||||
/**
|
||||
* Submenu for Alerts tab
|
||||
*/
|
||||
|
||||
const getAlertingNav = () => [
|
||||
{ value: router.rules },
|
||||
{ value: router.notifiers },
|
||||
];
|
||||
|
||||
/**
|
||||
* Default navigation menu
|
||||
*/
|
||||
export const getDefaultNavigation = ({
|
||||
serverUrl,
|
||||
isEnterpriseLicense,
|
||||
showPredefinedDashboards,
|
||||
showAlertLink,
|
||||
@@ -72,7 +64,7 @@ export const getDefaultNavigation = ({
|
||||
{ label: "Explore", submenu: getExploreNav() },
|
||||
{ label: "Tools", submenu: getToolsNav(isEnterpriseLicense) },
|
||||
{ value: router.dashboards, hide: !showPredefinedDashboards },
|
||||
getAlertLink(serverUrl, showAlertLink),
|
||||
{ value: "Alerting", submenu: getAlertingNav(), hide: !showAlertLink },
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
animation: vm-scale 150ms cubic-bezier(0.280, 0.840, 0.420, 1);
|
||||
}
|
||||
|
||||
span {
|
||||
.vm-list-item-inner {
|
||||
grid-column: 2;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,9 +17,12 @@
|
||||
--color-primary: #3F51B5;
|
||||
--color-secondary: #E91E63;
|
||||
--color-error: #FD080E;
|
||||
--color-broken: #FF8C33;
|
||||
--color-warning: #FF8308;
|
||||
--color-info: #03A9F4;
|
||||
--color-success: #4CAF50;
|
||||
--color-notice: #BD33FF;
|
||||
--color-keyword: rgba(161, 15, 94, 1);
|
||||
|
||||
/* text palette */
|
||||
--color-primary-text: #FFFFFF;
|
||||
@@ -45,4 +48,5 @@
|
||||
|
||||
--border-divider: 1px solid rgba(0, 0, 0, 0.15);
|
||||
--color-hover-black: rgba(0, 0, 0, 0.06);
|
||||
--color-hover-darker: rgba(0, 0, 0, 0.10);
|
||||
}
|
||||
|
||||
@@ -2,9 +2,17 @@
|
||||
$color-primary: var(--color-primary);
|
||||
$color-secondary: var(--color-secondary);
|
||||
$color-error: var(--color-error);
|
||||
$color-background-error: var(--color-background-error);
|
||||
$color-warning: var(--color-warning);
|
||||
$color-background-warning: var(--color-background-warning);
|
||||
$color-info: var(--color-info);
|
||||
$color-background-info: var(--color-background-info);
|
||||
$color-success: var(--color-success);
|
||||
$color-background-success: var(--color-background-success);
|
||||
$color-notice: var(--color-notice);
|
||||
$color-passive: var(--color-passive);
|
||||
$color-broken: var(--color-broken);
|
||||
$color-keyword: var(--color-keyword);
|
||||
|
||||
$color-primary-text: var(--color-primary-text);
|
||||
$color-secondary-text: var(--color-secondary-text);
|
||||
@@ -28,15 +36,21 @@ $color-tropical-blue: #C9E3F6;
|
||||
$color-background-body: var(--color-background-body);
|
||||
$color-background-block: var(--color-background-block);
|
||||
$color-background-tooltip: var(--color-background-tooltip);
|
||||
|
||||
$color-background-item: var(--color-background-item);
|
||||
$color-background-badge: var(--color-background-badge);
|
||||
$color-background-hover: var(--color-background-hover);
|
||||
|
||||
/************* padding *************/
|
||||
$padding-global: 12px;
|
||||
$padding-large: 16px;
|
||||
$padding-medium: 12px;
|
||||
$padding-small: 8px;
|
||||
$padding-tiny: 6px;
|
||||
|
||||
|
||||
/************* line height *************/
|
||||
$line-height-table: 40px;
|
||||
|
||||
/************* fonts *************/
|
||||
$font-family-global: system-ui;
|
||||
$font-family-monospace: monospace;
|
||||
@@ -61,4 +75,5 @@ $box-shadow: var(--box-shadow);
|
||||
$box-shadow-popper: var(--box-shadow-popper);
|
||||
|
||||
$color-hover-black: var(--color-hover-black);
|
||||
$color-hover-darker: var(--color-hover-darker);
|
||||
$vh: var(--vh);
|
||||
|
||||
@@ -30,17 +30,17 @@ export interface DataValue {
|
||||
value: number; // y axis value
|
||||
}
|
||||
|
||||
export interface DataSeries extends MetricBase{
|
||||
export interface DataSeries extends MetricBase {
|
||||
metadata: {
|
||||
name: string;
|
||||
},
|
||||
};
|
||||
values: DataValue[]; // sorted by key which is timestamp
|
||||
}
|
||||
|
||||
export interface InstantDataSeries {
|
||||
metadata: string[]; // just ordered columns
|
||||
value: string;
|
||||
values: string[]
|
||||
values: string[];
|
||||
copyValue: string;
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ export interface PanelSettings {
|
||||
expr: string[];
|
||||
alias?: string[];
|
||||
showLegend?: boolean;
|
||||
width?: number
|
||||
width?: number;
|
||||
}
|
||||
|
||||
export interface DashboardRow {
|
||||
@@ -77,55 +77,61 @@ export interface DashboardSettings {
|
||||
}
|
||||
|
||||
export interface RelativeTimeOption {
|
||||
id: string,
|
||||
duration: string,
|
||||
until: () => Date,
|
||||
title: string,
|
||||
isDefault?: boolean,
|
||||
id: string;
|
||||
duration: string;
|
||||
until: () => Date;
|
||||
title: string;
|
||||
isDefault?: boolean;
|
||||
}
|
||||
|
||||
export interface TopQuery {
|
||||
accountID: number
|
||||
avgDurationSeconds: number
|
||||
count: number
|
||||
projectID: number
|
||||
query: string
|
||||
timeRangeSeconds: number
|
||||
sumDurationSeconds: number
|
||||
timeRange: string
|
||||
url?: string
|
||||
accountID: number;
|
||||
avgDurationSeconds: number;
|
||||
count: number;
|
||||
projectID: number;
|
||||
query: string;
|
||||
timeRangeSeconds: number;
|
||||
sumDurationSeconds: number;
|
||||
timeRange: string;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
export interface TopQueryStats {
|
||||
"search.queryStats.lastQueriesCount": number
|
||||
"search.queryStats.minQueryDuration": string
|
||||
"search.queryStats.lastQueriesCount": number;
|
||||
"search.queryStats.minQueryDuration": string;
|
||||
}
|
||||
|
||||
export interface TopQueriesData extends TopQueryStats {
|
||||
maxLifetime: string
|
||||
topN: string
|
||||
topByAvgDuration: TopQuery[]
|
||||
topByCount: TopQuery[]
|
||||
topBySumDuration: TopQuery[]
|
||||
error?: string
|
||||
maxLifetime: string;
|
||||
topN: string;
|
||||
topByAvgDuration: TopQuery[];
|
||||
topByCount: TopQuery[];
|
||||
topBySumDuration: TopQuery[];
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface SeriesLimits {
|
||||
table: number,
|
||||
chart: number,
|
||||
code: number,
|
||||
table: number;
|
||||
chart: number;
|
||||
code: number;
|
||||
}
|
||||
|
||||
export interface Timezone {
|
||||
region: string,
|
||||
utc: string,
|
||||
search?: string
|
||||
region: string;
|
||||
utc: string;
|
||||
search?: string;
|
||||
}
|
||||
|
||||
export interface GraphSize {
|
||||
id: string,
|
||||
isDefault?: boolean,
|
||||
height: () => number
|
||||
id: string;
|
||||
isDefault?: boolean;
|
||||
height: () => number;
|
||||
}
|
||||
|
||||
export interface RuleType {
|
||||
id: string;
|
||||
title: string;
|
||||
isDefault?: boolean;
|
||||
}
|
||||
|
||||
export enum Theme {
|
||||
@@ -141,7 +147,7 @@ export interface RelabelStep {
|
||||
errors: {
|
||||
inLabels: string;
|
||||
outLabels: string;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export interface RelabelData {
|
||||
@@ -173,5 +179,84 @@ export enum QueryContextType {
|
||||
export interface AppConfig {
|
||||
license?: {
|
||||
type?: "enterprise" | "opensource";
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export interface Group {
|
||||
name: string;
|
||||
file: string;
|
||||
rules: Rule[];
|
||||
interval: number;
|
||||
limit: number;
|
||||
lastEvaluation: number;
|
||||
evaluationTime: number;
|
||||
type: string;
|
||||
id: string;
|
||||
concurrency: number;
|
||||
params: string[];
|
||||
headers: string[];
|
||||
notifier_headers: string[];
|
||||
labels: Record<string, string>;
|
||||
eval_offset: number;
|
||||
eval_delay: number;
|
||||
states: Record<string, number>;
|
||||
}
|
||||
|
||||
export interface Rule {
|
||||
state: string;
|
||||
name: string;
|
||||
query: string;
|
||||
duration: number;
|
||||
keepFiringFor: number;
|
||||
labels: Record<string, string>;
|
||||
annotations: Record<string, string>;
|
||||
alerts: Alert[];
|
||||
health: string;
|
||||
lastEvaluation: number;
|
||||
lastError: string;
|
||||
evaluationTime: number;
|
||||
type: string;
|
||||
datasourceType: string;
|
||||
lastSamples: bigint;
|
||||
lastSeriesFetched: bigint;
|
||||
id: string;
|
||||
group_id: string;
|
||||
debug: boolean;
|
||||
updates: RuleUpdate[];
|
||||
max_updates_entries: number;
|
||||
}
|
||||
|
||||
interface RuleUpdate {
|
||||
time: string;
|
||||
at: string;
|
||||
duration: number;
|
||||
error: string;
|
||||
samples: number;
|
||||
series_fetched: number;
|
||||
}
|
||||
|
||||
export interface Alert {
|
||||
name: string;
|
||||
state: string;
|
||||
value: string;
|
||||
group_id: string;
|
||||
expression: string;
|
||||
labels: Record<string, string>;
|
||||
annotations: Record<string, string>;
|
||||
activeAt: number;
|
||||
id: string;
|
||||
source: string;
|
||||
restored: boolean;
|
||||
stabilizing: boolean;
|
||||
}
|
||||
|
||||
export interface Notifier {
|
||||
kind: string;
|
||||
targets: Target[];
|
||||
}
|
||||
|
||||
export interface Target {
|
||||
address: string;
|
||||
labels: Record<string, string>;
|
||||
lastError: string;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@ export const getDefaultServer = (tenantId?: string): string => {
|
||||
const { serverURL } = getAppModeParams();
|
||||
const storageURL = getFromStorage("SERVER_URL") as string;
|
||||
const anomalyURL = `${window.location.origin}${window.location.pathname.replace(/^\/vmui/, "")}`;
|
||||
const defaultURL = window.location.href.replace(/(\/(?:prometheus\/)?(?:graph|vmui)\/.*|\/#\/.*)/, "/prometheus");
|
||||
const baseURL = window.location.href.replace(/(\/(?:prometheus\/)?(?:graph|vmui)\/.*|\/#\/.*)/, "");
|
||||
const defaultURL = baseURL.replace(/(\/select\/[\d:]+)$/, "$1/prometheus");
|
||||
const url = serverURL || storageURL || defaultURL;
|
||||
|
||||
switch (APP_TYPE) {
|
||||
|
||||
@@ -10,33 +10,24 @@ const getProxy = (): Record<string, ProxyOptions> | undefined => {
|
||||
switch (playground) {
|
||||
case "METRICS": {
|
||||
return {
|
||||
"^/vmalert/.*": {
|
||||
"^/(api|vmalert)/.*": {
|
||||
target: "https://play.victoriametrics.com/select/0/prometheus",
|
||||
changeOrigin: true,
|
||||
configure: (proxy) => {
|
||||
proxy.on("error", (err) => {
|
||||
console.error("[proxy error]", err.message);
|
||||
});
|
||||
},
|
||||
},
|
||||
"/flags": {
|
||||
target: "https://play.victoriametrics.com",
|
||||
changeOrigin: true,
|
||||
configure: (proxy) => {
|
||||
proxy.on("error", (err) => {
|
||||
console.error("[proxy error]", err.message);
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
"^/api/.*": {
|
||||
target: "https://play.victoriametrics.com/select/0/prometheus/",
|
||||
changeOrigin: true,
|
||||
configure: (proxy) => {
|
||||
proxy.on("error", (err) => {
|
||||
console.error("[proxy error]", err.message);
|
||||
});
|
||||
}
|
||||
},
|
||||
"^/prometheus/api.*": {
|
||||
target: "https://play.victoriametrics.com/select/0/",
|
||||
changeOrigin: true,
|
||||
configure: (proxy) => {
|
||||
proxy.on("error", (err) => {
|
||||
console.error("[proxy error]", err.message);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
default: {
|
||||
@@ -48,10 +39,7 @@ const getProxy = (): Record<string, ProxyOptions> | undefined => {
|
||||
export default defineConfig(({ mode }) => {
|
||||
return {
|
||||
base: "",
|
||||
plugins: [
|
||||
preact(),
|
||||
dynamicIndexHtmlPlugin({ mode })
|
||||
],
|
||||
plugins: [preact(), dynamicIndexHtmlPlugin({ mode })],
|
||||
assetsInclude: ["**/*.md"],
|
||||
server: {
|
||||
open: true,
|
||||
@@ -60,7 +48,7 @@ export default defineConfig(({ mode }) => {
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
"src": path.resolve(__dirname, "src"),
|
||||
src: path.resolve(__dirname, "src"),
|
||||
},
|
||||
},
|
||||
build: {
|
||||
@@ -71,12 +59,9 @@ export default defineConfig(({ mode }) => {
|
||||
if (id.includes("node_modules")) {
|
||||
return "vendor";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -292,6 +292,9 @@ VMUI provides the following features:
|
||||
- [Metric relabel debugger](https://play.victoriametrics.com/select/accounting/1/6a716b0f-38bc-4856-90ce-448fd713e3fe/prometheus/graph/#/relabeling) - debug [relabeling](#relabeling) rules.
|
||||
- [Downsampling filters debugger](https://play.victoriametrics.com/select/accounting/1/6a716b0f-38bc-4856-90ce-448fd713e3fe/prometheus/graph/#/downsampling-filters-debug) {{% available_from "v1.105.0" %}} - debug [downsampling](#downsampling) configs.
|
||||
- [Retention filters debugger](https://play.victoriametrics.com/select/accounting/1/6a716b0f-38bc-4856-90ce-448fd713e3fe/prometheus/graph/#/retention-filters-debug) {{% available_from "v1.105.0" %}} - debug [retention filter](#retention-filters) configs.
|
||||
- `Alerting` {{% available_from "#" %}} for displaying groups and rules from the [vmalert](https://docs.victoriametrics.com/victoriametrics/vmalert/) service.
|
||||
The tab is available only if [VictoriaMetrics single-node](https://docs.victoriametrics.com/#vmalert) or
|
||||
[vmselect](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/#vmalert) are configured with `-vmalert.proxyURL` command-line flag.
|
||||
|
||||
**Querying**
|
||||
|
||||
|
||||
@@ -26,6 +26,9 @@ See also [LTS releases](https://docs.victoriametrics.com/victoriametrics/lts-rel
|
||||
|
||||
* FEATURE: upgrade Go builder from Go1.24.6 to Go1.25. See [Go1.25 release notes](https://tip.golang.org/doc/go1.25).
|
||||
* FEATURE: [vmui](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#vmui): add export functionality for Query (Table view) and RawQuery tabs in CSV/JSON format. See [#9332](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9332).
|
||||
* FEATURE: [vmui](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#vmui): replace `Alerts` tab with `Alerting` tab in vmui. The new `Alerting` tab displays vmalert groups and rules directly in vmui interface without redirecting user to vmalert's WEB UI. Links of format `.*/prometheus/vmalert/.*` will continue working by redirecting to vmalert's UI. This functionality is available only if `-vmalert.proxyURL` is set on vmselect. Some functionality of the new `Alerting` tab requires vmalert to be of the same version as vmselect, or higher.
|
||||
* FEATURE: [vmalert](https://docs.victoriametrics.com/victoriametrics/vmalert/): add `/api/v1/group?group_id=<id>` API endpoint for viewing details of a specific [rules group](https://docs.victoriametrics.com/vmalert.html#groups). The new handler is used by new `Alerting` tab in vmselect.
|
||||
* FEATURE: [vmalert](https://docs.victoriametrics.com/victoriametrics/vmalert/): add `lastError` field to `/api/v1/notifiers` response. The new field contains an error message if the last attempt to send data to the notifier failed. The error can be also viewed in `Alerting. Notifiers` tab.
|
||||
|
||||
* BUGFIX: [vmagent](https://docs.victoriametrics.com/victoriametrics/vmagent/): prevent remote write ingestion stop on push error for [Google Pub/Sub](https://docs.victoriametrics.com/victoriametrics/vmagent/#writing-metrics-to-pubsub) integration.
|
||||
* BUGFIX: [vmauth](https://docs.victoriametrics.com/victoriametrics/vmauth/): properly handle [mTLS authorization and routing](https://docs.victoriametrics.com/victoriametrics/vmauth/#mtls-based-request-routing). Previously it didn't work. See [#29](https://github.com/VictoriaMetrics/VictoriaLogs/issues/29).
|
||||
@@ -40,7 +43,7 @@ Released at 2025-08-15
|
||||
* SECURITY: upgrade Go builder from Go1.24.5 to Go1.24.6. See [the list of issues addressed in Go1.24.6](https://github.com/golang/go/issues?q=milestone%3AGo1.24.6+label%3ACherryPickApproved).
|
||||
|
||||
* FEATURE: [vmsingle](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/) and [vmselect](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/) in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/): protect graphite `/render` API endpoint with new flag `-search.maxGraphitePathExpressionLen`. See this PR [#9534](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/9534) for details.
|
||||
* FEATURE: expose `vm_total_disk_space_bytes` metric at the [`/metrics` page](https://docs.victoriametrics.com/#monitoring), which shows the total disk space for the data directory specified via [`-storageDataPath`](https://docs.victoriametrics.com/#storage). This metric can be useful for building alerts and graphs for the percentatge of free disk space via `vm_free_disk_space_bytes / vm_total_disk_space_bytes`. See [this comment](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/9523#issuecomment-3149459926).
|
||||
* FEATURE: expose `vm_total_disk_space_bytes` metric at the [`/metrics` page](https://docs.victoriametrics.com/#monitoring), which shows the total disk space for the data directory specified via [`-storageDataPath`](https://docs.victoriametrics.com/#storage). This metric can be useful for building alerts and graphs for the percentage of free disk space via `vm_free_disk_space_bytes / vm_total_disk_space_bytes`. See [this comment](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/9523#issuecomment-3149459926).
|
||||
* FEATURE: all: leave non-existing environment variables as is in config files instead of failure. For example, if the file referred by `-promscrape.config` contains `%{NON_EXISTING_ENV_VAR}` placeholder, then it is left as is instead of failing to load the file. This simplifies the usage of environment variables in config files and in command-line flags according to [these docs](https://docs.victoriametrics.com/victoriametrics/#environment-variables). Users can easily notice non-existing env vars in config files and in command-line flags by looking at their values - they will literally contain `%{NON_EXISTING_ENV_VAR}` strings.
|
||||
* FEATURE: `vmselect`, `vminsert` and `vmstorage` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/): add rpc handshake timeout configuration via `-rpc.handshakeTimeout` flag (default 5s). Set deadline for the entire handshake process instead of per-operation timeout. See [#9345](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9345) for more details.
|
||||
* FEATURE: [vmagent](https://docs.victoriametrics.com/victoriametrics/vmagent/): add `-enableMetadata` command-line flag to allow sending metadata to the configured `-remoteWrite.url`, metadata can be scraped from targets, received via VictoriaMetrics remote write, Prometheus remote write v1 or OpenTelemetry protocol. See [#2974](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2974).
|
||||
|
||||
@@ -26,7 +26,7 @@ Configure `-vmalert.proxyURL` on VictoriaMetrics [single-node](https://docs.vict
|
||||
or [vmselect in cluster version](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/#vmalert)
|
||||
to proxy requests to `vmalert`. Proxying is needed for the following cases:
|
||||
* to proxy requests from [Grafana Alerting UI](https://grafana.com/docs/grafana/latest/alerting/);
|
||||
* to access `vmalert`'s UI through [VictoriaMetrics Web interface](https://docs.victoriametrics.com/#vmui).
|
||||
* to access `vmalert`'s UI through [vmui](https://docs.victoriametrics.com/#vmui).
|
||||
|
||||
[VictoriaMetrics Cloud](https://console.victoriametrics.cloud/signUp?utm_source=website&utm_campaign=docs_vm_vmalert_intro)
|
||||
provides out-of-the-box alerting functionality based on `vmalert`. This service simplifies the setup
|
||||
@@ -740,7 +740,9 @@ or time series modification via [relabeling](https://docs.victoriametrics.com/vi
|
||||
* `http://<vmalert-addr>/api/v1/alerts` - list of all active alerts;
|
||||
* `http://<vmalert-addr>/api/v1/notifiers` - list all available notifiers;
|
||||
* `http://<vmalert-addr>/vmalert/api/v1/alert?group_id=<group_id>&alert_id=<alert_id>` - get alert status in JSON format.
|
||||
Used as alert source in AlertManager.
|
||||
* `http://<vmalert-addr>/vmalert/api/v1/rule?group_id=<group_id>&rule_id=<rule_id>` - get rule status in JSON format.
|
||||
* `http://<vmalert-addr>/vmalert/api/v1/group?group_id=<group_id>` - get group status in JSON format.
|
||||
Used as alert source in AlertManager.
|
||||
* `http://<vmalert-addr>/vmalert/alert?group_id=<group_id>&alert_id=<alert_id>` - get alert status in web UI.
|
||||
* `http://<vmalert-addr>/vmalert/rule?group_id=<group_id>&rule_id=<rule_id>` - get rule status in web UI.
|
||||
* `http://<vmalert-addr>/vmalert/api/v1/rule?group_id=<group_id>&alert_id=<alert_id>` - get rule status in JSON format.
|
||||
|
||||
Reference in New Issue
Block a user