Compare commits

...

102 Commits

Author SHA1 Message Date
Aliaksandr Valialkin
ad105147dd docs/CHANGELOG.md: cut v1.84.0 2022-11-25 19:53:29 -08:00
Aliaksandr Valialkin
e2a061b6a3 vendor: make vendor-update 2022-11-25 19:52:00 -08:00
Aliaksandr Valialkin
e014467f42 docs/README.md: make docs-sync after 58d459e8a8 2022-11-25 16:55:47 -08:00
Aliaksandr Valialkin
58d459e8a8 app/{vminsert,vmagent}: follow-up after 53a63c6c4c
Extend /api/v1/import/prometheus with the support for Pushgateway way of specifying additional labels.

Updates https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1415
2022-11-25 16:48:14 -08:00
Pedro Gonçalves
53a63c6c4c Adding pushgateway basic capabilities to vmagent (#3360)
* init pushgateway implementation

* Initial implementation of pushgateway in vmagent

* Initial implementation of pushgateway in vmagent
2022-11-25 16:35:01 -08:00
Zakhar Bessarab
8b6d528fbd {app/vmstorage,app/vmselect}: add API to get list of existing tenants (#3348)
* {app/vmstorage,app/vmselect}: add API to get list of existing tenants

* {app/vmstorage,app/vmselect}: add API to get list of existing tenants

* app/vmselect: fix error message

* {app/vmstorage,app/vmselect}: fix error messages

* app/vmselect: change log level for error handling

* wip

Co-authored-by: Aliaksandr Valialkin <valyala@victoriametrics.com>
2022-11-25 11:05:47 -08:00
Aliaksandr Valialkin
4ca44cfe9c app/vmselect/vmui: make vmui-update after 37cda9abd0 2022-11-25 07:33:09 -08:00
Yury Molodov
37cda9abd0 fix: change header settings (#3391) 2022-11-25 07:26:35 -08:00
Yury Molodov
1ab66186ca refactor: create Autocomplete component (#3390) 2022-11-25 07:25:35 -08:00
Roman Khavronenko
42e63fe0fd dashboards: cleanup & remove artifacts (#3387)
* some unexpected DS UIDs were removed;
* replace `$instance.*` filter with `$instance` since we respect
the instance port anyway;
* remove predefined datasource for `clusterbytenant`
in favour of datasource variable `ds`.

Signed-off-by: hagen1778 <roman@victoriametrics.com>

Signed-off-by: hagen1778 <roman@victoriametrics.com>
2022-11-25 09:28:14 +01:00
Aliaksandr Valialkin
da13d36af9 app/vmselect/vmui: make vmui-update after eb772aa50e 2022-11-24 17:37:18 -08:00
Yury Molodov
eb772aa50e vmui: improve table view (#3377)
* vmui: add compact table view (#3365)

* feat: add compact table view

* fix: add overflow table

* fix: change table styles

* vmui: compact table view

* Update docs/CHANGELOG.md

Co-authored-by: Michal Kralik <michal.kralik@percona.com>
Co-authored-by: Aliaksandr Valialkin <valyala@victoriametrics.com>
2022-11-24 17:33:07 -08:00
Aliaksandr Valialkin
399ed9a3b9 docs/MetricsQL.md: document that histogram_share() accepts optional boundsLabel arg 2022-11-24 17:27:30 -08:00
Aliaksandr Valialkin
045fec631b docs/vmagent.md: typo fix 2022-11-24 17:19:45 -08:00
Roman Khavronenko
3407006cdb dashboards: cluster dashboard update (#3380)
The purpose of the update is to make the dash more usable
for large installations with many instances. Panels which showed
metrics per-instance (Mem, CPU) now are showing metrics per-job or min/max/avg
aggregations in % instead. This supposed to help immediately to identify
resource shortage and remain usable for small and big installations.

For cases when detailed info is needed, to the bottom of the dashboard
a new row `Drilldown` was added. Panels like Mem or CPU now contain
a `data-link` named `Drilldown` (cis shown on line click) which takes
user to more detailed panel.

The change list is the following:
* bump Grafana version to 9.1.0;
* replace old "Graph" panel with "TimeSeries" panel;
* improve Uptime panel to show number of instances per job;
* show % usage of Mem and CPU instead of absolute values;
* `Caches` row was removed. All needed info for caches is now part of `Troubleshooting`;
* add `Drilldown` section for detailed resource usage;
* add Annotations for Alert triggers. Not all alerts are supposed to be displayed
on the dashboard, but only those with label `show_at: dashboard`.
See `alerts-cluster.yml` change.

Signed-off-by: hagen1778 <roman@victoriametrics.com>

Signed-off-by: hagen1778 <roman@victoriametrics.com>
2022-11-23 18:03:25 -08:00
Aliaksandr Valialkin
04bb2e14dd docs/Articles.md: add a link to "The cost of scale in Prometheus ecosystem" talk 2022-11-23 00:11:51 +02:00
Aliaksandr Valialkin
ccf9bb32ac app/vmselect/vmui: make vmui-update after 7dc2349913 2022-11-22 15:36:03 +02:00
Yury Molodov
7dc2349913 vmui: add set up series custom limits (#3368)
* feat: add set up series custom limits

* feat: add button for show series without limits

* fix: resolve merge conflicts
2022-11-22 15:31:17 +02:00
Aliaksandr Valialkin
633ad34eb7 vendor: make vendor-update 2022-11-22 11:26:16 +02:00
Aliaksandr Valialkin
b1622ad63e docs/Articles.md: add a link to https://www.youtube.com/watch?v=_zORxrgLtec (OSA Con 2022: Specifics of data analysis in Time Series Databases) 2022-11-22 01:08:21 +02:00
Aliaksandr Valialkin
9498f871e7 app/vminsert: add missing vm_relabel_config_* metrics after 03d88bc066
Updates https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3345
2022-11-22 00:47:49 +02:00
Roman Khavronenko
03d88bc066 vmagent: expose metrics for tracking config state (#3375)
Expose `vm_relabel_config_*` and `vm_promscrape_config_*` metrics
for tracking relabel and scrape configuration hot-reloads.

https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3345
Signed-off-by: hagen1778 <roman@victoriametrics.com>

Signed-off-by: hagen1778 <roman@victoriametrics.com>
2022-11-22 00:38:43 +02:00
Aliaksandr Valialkin
2ddfde78c3 app/vmselect/vmui: make vmui-update after 7d1b3e7e14 2022-11-22 00:34:33 +02:00
Yury Molodov
7d1b3e7e14 vmui: add copy button to row on Table view (#3363)
* feat: add copy button to row on Table view

* vmui: add copy button to row on Table view

Co-authored-by: Aliaksandr Valialkin <valyala@victoriametrics.com>
2022-11-22 00:31:33 +02:00
Yury Molodov
f81072f9a7 vmui: minor fixes (#3361)
* fix: reset the value of the switches trace and cache

* fix: add cursor text for inputs

* fix: solve the Infinite loop of useFetchQuery.ts

* fix: change condition for show/hide autocomplete

* fix: add limit error length for input
2022-11-22 00:28:03 +02:00
Yury Molodov
82d254af08 vmui: sticky tooltip (#3376)
* feat: add ability to make tooltip "sticky"

* vmui: add ability to make tooltip "sticky"
2022-11-22 00:26:53 +02:00
Aliaksandr Valialkin
ee1479bac6 docs/CHANGELOG.md: link to the related issue for range_normalize() function 2022-11-21 23:27:00 +02:00
Aliaksandr Valialkin
d9c3a2b605 app/vmselect/promql: add range_normalize(q1, ..., qN) function for normalizing query results into [0..1] value range
This may be useful for analyzing correlation between time series with different value ranges
2022-11-21 23:25:00 +02:00
Aliaksandr Valialkin
95f0266558 lib/promscrape/discovery/gce: do not pass filter arg when discovering zones
The filter arg isn't supported by zones API in GCE.

See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3202
2022-11-21 22:32:05 +02:00
Aliaksandr Valialkin
05ed98c98b app/vmselect/promql: allow using SI and IEC suffixes in numeric values inside queries
For example, 10Ki is equivalent to 10*1024, while 5.3M is equivalent to 5.3*1000*1000
2022-11-21 21:27:55 +02:00
Aliaksandr Valialkin
2c9e403d5f app/vmselect/promql: properly return an empty result from limit_offset() if offset exceeds the number of inner time series
Updates https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3312
2022-11-21 16:47:37 +02:00
Roman Khavronenko
0b6f439b11 vmalert: bump alerting rules evaluation interval to reasonable 30s (#3374)
Signed-off-by: hagen1778 <roman@victoriametrics.com>
2022-11-21 15:23:23 +01:00
Aliaksandr Valialkin
b796a0dc3f app/vmselect/promql: optimize e1 op e2 when e1 returns an empty result
Updates https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3349
2022-11-21 16:09:10 +02:00
Roman Khavronenko
84742f229a vmalert: add default list of alerting rules (#3373)
The default list of alerting rules contains the basic
rules for checking vmalert's health state and is recommended
to use for monitoring vmalert deployments.

Signed-off-by: hagen1778 <roman@victoriametrics.com>
2022-11-21 14:45:45 +01:00
Aliaksandr Valialkin
20d758e3e4 all: add a link to https://docs.victoriametrics.com/enterprise.html into description for enterprise flags 2022-11-21 15:42:01 +02:00
Aliaksandr Valialkin
cb1a621d63 app/{vminsert,vmselect}: add -storageNode.filter command-line flag for filtering the discovered storage nodes
Updates https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3353
2022-11-21 15:20:14 +02:00
Aliaksandr Valialkin
65b4e96a80 docs/Cluster-VictoriaMetrics.md: sync with cluster branch 2022-11-18 14:06:23 +02:00
Aliaksandr Valialkin
a061d33400 app/vmselect: clarify that it isnt recommended setting -replicationFactor at vmselect nodes even if the replication is enabled at vminsert nodes 2022-11-18 14:04:53 +02:00
Aliaksandr Valialkin
cae0f37edd app/vmselect/netstorage: remove superflouos map lookup at ProcessSearchQuery
This should reduce CPU usage a bit during querying
2022-11-18 13:40:04 +02:00
Yury Molodov
519bd2af7b vmui: add trace analyzer (#3310)
* refactor: change structure project

* refactor: change structure project

* fix: add hooks for set query params

* refactor: add index for pages

* docs: add TESTCASES.md

* refactor: restructure components

* feat: add page with trace analyzer

* fix: change detect trace data

* Update app/vmui/packages/vmui/src/pages/TracePage/index.tsx

Co-authored-by: Zakhar Bessarab <z.bessarab@victoriametrics.com>

* Update app/vmui/packages/vmui/src/pages/TracePage/index.tsx

Co-authored-by: Zakhar Bessarab <z.bessarab@victoriametrics.com>

* fix: change descriptions on trace page

* Update app/vmui/packages/vmui/src/pages/TracePage/index.tsx

Co-authored-by: Zakhar Bessarab <z.bessarab@victoriametrics.com>

* feat: add base components

* feat: add reset styles

* docs: add description about trace analyzer

* feat: add styles for custom panel page

* feat: add styles for predefined panels

* feat: add style for TracingsView.tsx

* feat: add Alerts

* feat: add Tooltip.tsx

* fix: correct styles

* feat: add DatePicker.tsx

* feat: add tables

* feat: add theme provider

* fix: replace using callbacks as props to handlers

* fix: correct update time

* fix: change TimePicker.tsx

* fix: correct styles

* fix: update packages

* vmui: refactor code, remove material-ui

* feat: add paste json for trace analyzer

* vmui: update trace analyzer docs

* app/vmselect/vmui: `make vmui-update`

Co-authored-by: Zakhar Bessarab <z.bessarab@victoriametrics.com>
Co-authored-by: Aliaksandr Valialkin <valyala@victoriametrics.com>
2022-11-17 22:22:01 +02:00
Aliaksandr Valialkin
e79bfdf4b8 docs/CHANGELOG.md: document the fix for CPU usage spikes
Updates https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3343
2022-11-17 22:02:02 +02:00
Aliaksandr Valialkin
353396aa23 lib/workingsetcache: expose -cacheExpireDuration command-line flag for fine-tuning of the cache expiration
While at it, decrease -prevCacheRemovalPercent from 0.2 to 0.1 and increase -cacheExpireDuration from 20 minutes to 30 minutes.

This is needed for https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3343
2022-11-17 19:59:13 +02:00
Aliaksandr Valialkin
578bb58ea9 app/vmselect/vmui: make vmui-update after 51bfd1ab80 2022-11-17 18:54:55 +02:00
Yury Molodov
51bfd1ab80 vmui: add ability hide query (#3359)
* feat: add ability hide query

* fix: change logic hide query

* fix: remove console.log
2022-11-17 18:42:33 +02:00
dependabot[bot]
3ed238b75b build(deps): bump loader-utils in /app/vmui/packages/vmui (#3350)
Bumps [loader-utils](https://github.com/webpack/loader-utils) from 2.0.3 to 2.0.4.
- [Release notes](https://github.com/webpack/loader-utils/releases)
- [Changelog](https://github.com/webpack/loader-utils/blob/v2.0.4/CHANGELOG.md)
- [Commits](https://github.com/webpack/loader-utils/compare/v2.0.3...v2.0.4)

---
updated-dependencies:
- dependency-name: loader-utils
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-11-17 12:49:09 +01:00
Aliaksandr Valialkin
2c9017f6df vendor: make vendor-update 2022-11-17 01:38:29 +02:00
Dmytro Kozlov
fb65fb39d2 app/vmstorage: fix potential file inclusion via variable (#3339)
* app/vmstorage: fix potential file inclusion via variable

* app/vmstorage: cleanup
2022-11-17 01:29:43 +02:00
Aliaksandr Valialkin
a21c8e7b9a app/vmselect/vmui: make vmui-update after bc8a782f74 2022-11-17 01:15:45 +02:00
Yury Molodov
bc8a782f74 vmui/refactor (#3298)
* refactor: change structure project

* refactor: change structure project

* fix: add hooks for set query params

* refactor: add index for pages

* docs: add TESTCASES.md

* refactor: restructure components

* feat: add base components

* feat: add reset styles

* feat: add styles for custom panel page

* feat: add styles for predefined panels

* feat: add style for TracingsView.tsx

* feat: add Alerts

* feat: add Tooltip.tsx

* fix: correct styles

* feat: add DatePicker.tsx

* feat: add tables

* feat: add theme provider

* fix: replace using callbacks as props to handlers

* fix: correct update time

* fix: change TimePicker.tsx

* fix: correct styles

* fix: update packages

* vmui: refactor code, remove material-ui

Co-authored-by: Aliaksandr Valialkin <valyala@victoriametrics.com>
2022-11-17 01:09:14 +02:00
Aliaksandr Valialkin
a260e2659e app/vmselect/promql: add range_stdvar() and range_stddev() functions for calculating variance and deviation over time series on the selected time range 2022-11-17 01:03:40 +02:00
Aliaksandr Valialkin
c1a3192d8b app/vmselect/promql: add range_linear_regression(q) function for calculating simple linear regression for the selected time series on the selected time range 2022-11-17 00:38:48 +02:00
Aliaksandr Valialkin
5955d23232 lib/promscrape: add a benchmark for internLabelStrings() 2022-11-16 23:02:49 +02:00
Aliaksandr Valialkin
a75137c1c2 lib/mergeset: properly reset bsr.bhIdx after the call to blockStreamReader.readNextBHS()
The issue has been introduced in 58b40f514c

Updates https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3343
2022-11-16 21:23:35 +02:00
Aliaksandr Valialkin
c3362e3db4 lib/workingsetcache: add -prevCacheRemovalPercent command-line flag for tuning memory usage vs CPU usage ratio
Reduce the default value of this flag from 1% to 0.2% after 71335e6024

This flag should help determining the best ratio for https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3343
2022-11-16 12:39:39 +02:00
Aliaksandr Valialkin
4106f197f2 lib/mergeset: retain the buffer with the data used by indexBlock.bhs, inside indexBlock.buf
Previously indexBlock.bhs pointed to the buffer, which could be changed over time.
This could result in incorrect time series search over time.

This is a follow-up for 58b40f514c

Updates https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3343
2022-11-16 12:09:23 +02:00
Aliaksandr Valialkin
58b40f514c lib/mergeset: remove string allocation and copying when unmarshaling blockHeader
This should reduce CPU usage for the case from https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3343
2022-11-15 16:30:54 +02:00
Aliaksandr Valialkin
09b79d74a7 docs/CHANGELOG.md: document changes in v1.79.5 release 2022-11-11 01:27:45 +02:00
Aliaksandr Valialkin
99f187d9bc deployment/docker: update VictoriaMetrics version from v1.83.0 to v1.83.1 2022-11-11 01:24:40 +02:00
Aliaksandr Valialkin
bbe1a1472c docs/CHANGELOG.md: cut v1.83.1 2022-11-10 14:06:57 +02:00
Aliaksandr Valialkin
1b9dff133a docs/CHANGELOG.md: document the fix at 71335e6024 2022-11-10 13:46:51 +02:00
Aliaksandr Valialkin
2bcafbef25 vendor: make vendor-update 2022-11-10 13:46:33 +02:00
Aliaksandr Valialkin
71335e6024 lib/workingsetcache: tune cache miss threshold for resetting the previous cache from 5% to 1%
It has been appeared that some production workloads could suffer for some time
after every reset of the previous cache when it gets less than 5% of requests
after the needed item isn't found in the current cache. This could result
in reduced cache hit rates, which, in turn, could increase CPU, disk IO and RAM
usage needed for reading, unpacking and caching the missed data from disk.

This commit reduces the cache miss threshold for resetting the previous cache from 5% to 1%.
This should reduce the possible negative impact after each cache reset by at least 5x,
while reducing the total memory used by caches.

This is a follow-up for d906d8573e
2022-11-10 13:31:54 +02:00
Dmytro Kozlov
5ff6e0fb02 vmui: fix vmui vulnerability (#3336)
* vmui: fix vmui vulnerability

* vmui: code cleanup
2022-11-10 02:28:37 +01:00
Aliaksandr Valialkin
6c7361b1c5 app/vmselect/vmui: make vmui-update after 7130af7fd2 2022-11-09 16:43:18 +02:00
Aliaksandr Valialkin
86bce7f5f9 lib/promscrape: add more cases to TestAddRowToTimeseries
This is a follow-up for 16fdd2af8a
2022-11-09 16:13:56 +02:00
Jeremy PLANCKEEL
16fdd2af8a test(golang): add test to function addRowToTimeseries (#3282)
Co-authored-by: jplanckeel-externe <jplanckeel.externe@bedrockstreaming.com>
2022-11-09 15:41:26 +02:00
Aliaksandr Valialkin
b8839df32c lib/protoparser/opentsdb: follow-up after 04b0e4e7bf
- Simplify the parser code to be less error prone
- Document the change
- Add a test for OpenTSDB put line with trailing whitespace without tags

Updates https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3290
2022-11-09 15:35:05 +02:00
Roman Khavronenko
04b0e4e7bf protoparser/opentsdb: allow lines without tags (#3303)
According to http://opentsdb.net/docs/build/html/api_telnet/put.html
"At least one tag pair must be present".
However, in VictoriaMetrics datamodel tags aren't required.
This could be confusing for users. Allowing accept lines without
tags seems to do no harm.

https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3290
Signed-off-by: hagen1778 <roman@victoriametrics.com>

Signed-off-by: hagen1778 <roman@victoriametrics.com>
2022-11-09 15:32:47 +02:00
Aliaksandr Valialkin
e17a1acf4a docs/CHANGELOG.md: document 7130af7fd2
Updates https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2814
2022-11-09 12:15:49 +02:00
Michal Kralik
7130af7fd2 vmui: show tracing in json view (#3316)
* vmui: show tracing in json view

* vmui: refactor tracing view
2022-11-09 12:12:28 +02:00
dependabot[bot]
10791bf224 build(deps): bump loader-utils in /app/vmui/packages/vmui (#3328)
Bumps [loader-utils](https://github.com/webpack/loader-utils) from 2.0.2 to 2.0.3.
- [Release notes](https://github.com/webpack/loader-utils/releases)
- [Changelog](https://github.com/webpack/loader-utils/blob/v2.0.3/CHANGELOG.md)
- [Commits](https://github.com/webpack/loader-utils/compare/v2.0.2...v2.0.3)

---
updated-dependencies:
- dependency-name: loader-utils
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-11-09 12:11:13 +02:00
Denys Holius
aebe21e2c8 guides/README.md: fix link to guide-delete-or-replace-metrics.html (#3331) 2022-11-09 12:00:46 +02:00
Aliaksandr Valialkin
34aa3f6404 README.md: sync changes with 9f8bf524ad 2022-11-09 11:55:50 +02:00
Aliaksandr Valialkin
20046dab6e app/vmui/packages/vmui: return back accidental changes at 9f8bf524ad 2022-11-09 11:55:34 +02:00
Aliaksandr Valialkin
c973aca617 app/vminsert/netstorage: move nodesHash from global state to storageNodesBucket
This should prevent from panics when the list of discovered vmstorage nodes changes.

Updates https://github.com/VictoriaMetrics/VictoriaMetrics/pull/3329
2022-11-09 11:51:10 +02:00
Roman Khavronenko
9f8bf524ad bump go version to 1.19.3 (#3327)
Signed-off-by: hagen1778 <roman@victoriametrics.com>

Signed-off-by: hagen1778 <roman@victoriametrics.com>
2022-11-08 16:43:59 +01:00
Michal Kralik
9b540bba6f vmui: change graph legend label format (#3315) 2022-11-08 15:16:24 +01:00
Zakhar Bessarab
91dd79f40f docs/operator: fix description for SA at VMClusterSpec (#3313) 2022-11-07 16:51:01 +01:00
Aliaksandr Valialkin
7fa5d043f5 lib/promscrape/discovery/consul: add __meta_consul_partition label in the same way as Prometheus does
See https://github.com/prometheus/prometheus/pull/11482
2022-11-07 15:25:53 +02:00
Aliaksandr Valialkin
8332622037 vendor: update github.com/urfave/cli/v2 from v2.23.2 to v2.23.4 2022-11-07 14:58:35 +02:00
Aliaksandr Valialkin
daa70e6560 lib/storage: follow-up for 790768f20b
- Document the bugfix at docs/CHANGELOG.md
- Simplify the bugfix a bit
2022-11-07 14:04:08 +02:00
Aliaksandr Valialkin
f9dc3da9e2 lib/storage: typo fix after 32d48f8dfbb03174858c00bdfe6d9d22431dc8d8 2022-11-07 13:58:27 +02:00
Aliaksandr Valialkin
116811d761 lib/envtemplate: allow non-env var names inside "%{ ... }" 2022-11-07 13:58:27 +02:00
Aliaksandr Valialkin
dd88c628aa lib/storage: remove unused isFull field from hourMetricIDs struct 2022-11-07 13:58:26 +02:00
Łukasz Marszał
790768f20b Fix issue-3309 - currHourMetricIDs shouldn't contain metrics from prev hour (#3320)
* fix issue-3309 currHourMetricIDs shouldn't contain metrics from prev hour

* Update storage.go
2022-11-07 13:55:37 +02:00
Aliaksandr Valialkin
63d4cf661b vendor: make vendor-update 2022-11-05 10:34:35 +02:00
Aliaksandr Valialkin
d61691d5fa deployment/docker: update Go builder from v1.19.2 to v1.19.3
See https://github.com/golang/go/issues?q=milestone%3AGo1.19.3+label%3ACherryPickApproved
2022-11-05 10:19:54 +02:00
Aliaksandr Valialkin
23c79e2e49 docs/Single-server-VictoriaMetrics.md: mention about security certifications at Security chapter
This is a follow-up for 94bd49402e
2022-11-05 10:07:29 +02:00
Aliaksandr Valialkin
4ef5fe1317 docs/guides/guide-vmcluster-multiple-retention-setup.md: clarify docs after a75d85b11e 2022-11-05 10:02:48 +02:00
Artem Navoiev
94bd49402e docs: Add link to security page from Readme (#3286)
Signed-off-by: Artem Navoiev <tenmozes@gmail.com>
2022-11-04 09:49:12 +01:00
Dmytro Kozlov
99d8fcb332 docs/operator: change VMAgentRemoteWriteSettings.MaxDiskUsagePerURL type from int32 to int64 (#3307)
* docs/operator: change VMAgentRemoteWriteSettings.MaxDiskUsagePerURL type from int32 to int64

* docs: operator updates api description

Co-authored-by: f41gh7 <nik@victoriametrics.com>
2022-11-04 01:12:57 +01:00
Roman Khavronenko
ac4e23de39 vmctl: fix panic on start (#3300)
The change disables initing the `-version` flag in new
`urfave/cli/v2` update. The `-version` flag conflicts
with the identical flag from `lib/buildinfo` and causes panic.

See https://github.com/VictoriaMetrics/VictoriaMetrics/pull/3299

Signed-off-by: hagen1778 <roman@victoriametrics.com>
2022-11-01 19:50:22 +01:00
Artem Navoiev
a75d85b11e update multi-tenancy guide. Add infromation about Enterprise and Rete… (#3285)
docs: update multi-tenancy guide

Add information about Enterprise and Retention Filters

Signed-off-by: Artem Navoiev <tenmozes@gmail.com>
2022-11-01 15:19:09 +01:00
Aliaksandr Valialkin
df88832c86 docs/Single-server-VictoriaMetrics.md: follow-up after a1a97b9321
- Remove trailing whitespace at the end of lines
- Remove redundant sentence stating that time series matching the given selector will be deleted.
  It should be clear from the surrounding context.
2022-11-01 10:49:10 +02:00
Aliaksandr Valialkin
a3dc324b19 vendor: update github.com/urfave/cli/v2 from 2.20.3 to 2.23.0 2022-11-01 10:46:26 +02:00
Dmytro Kozlov
a1a97b9321 docs: clarify information about usage of the api/v1/admin/tsdb/delete_series API (#3287)
docs: clarify information about usage of the `api/v1/admin/tsdb/delete_series` API
2022-11-01 09:36:28 +01:00
Aliaksandr Valialkin
a1011931ac docs/Release-Guide.md: instruct to update VictoriaMetrics version in deployment/docker/docker-compose*.yml files after creating new release
This is a follow-up for d1509f4559
2022-11-01 10:31:02 +02:00
Aliaksandr Valialkin
619b3c926d docs/enterprise.md: mention that feature requests from enterprise customers are prioritized 2022-11-01 10:28:12 +02:00
Denys Holius
d1509f4559 docker-compose: bump version of container tags for VictoriaMetrics components (#3294)
* deployment/docker/docker-compose-cluster.yml: bump VictoriaMetrics Cluster components to the latest v1.83.0 version

* deployment/docker/docker-compose.yml: bump VictoriaMetrics Single node and vmutils to the latest v1.83.0 version
2022-11-01 09:25:07 +01:00
Aliaksandr Valialkin
869e0f9f85 lib/promrelabel: go fmt after 5cec9706dc 2022-10-29 05:17:10 +03:00
Aliaksandr Valialkin
0f8f36de24 docs: typo fixes 2022-10-29 04:52:18 +03:00
Aliaksandr Valialkin
5cec9706dc lib/promrelabel: add a test from https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3251
Updates https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3251
2022-10-29 04:33:38 +03:00
493 changed files with 31225 additions and 20308 deletions

View File

@@ -17,7 +17,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@main
with:
go-version: 1.19.2
go-version: 1.19.3
id: go
- name: Code checkout
uses: actions/checkout@master

View File

@@ -19,7 +19,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@main
with:
go-version: 1.19.2
go-version: 1.19.3
id: go
- name: Code checkout
uses: actions/checkout@master

View File

@@ -30,6 +30,8 @@ from [the releases page](https://github.com/VictoriaMetrics/VictoriaMetrics/rele
VictoriaMetrics is developed at a fast pace, so it is recommended periodically checking the [CHANGELOG](https://docs.victoriametrics.com/CHANGELOG.html) and performing [regular upgrades](#how-to-upgrade-victoriametrics).
VictoriaMetrics has achieved security certifications for Database Software Development and Software-Based Monitoring Services. We apply strict security measures in everything we do. See our [Security page](https://victoriametrics.com/security/) for more details.
## Prominent features
VictoriaMetrics has the following prominent features:
@@ -287,7 +289,10 @@ When querying the [backfilled data](https://docs.victoriametrics.com/#backfillin
VMUI automatically adjusts the interval between datapoints on the graph depending on the horizontal resolution and on the selected time range. The step value can be customized by changing `Step value` input.
VMUI allows investigating correlations between multiple queries on the same graph. Just click `Add Query` button, enter an additional query in the newly appeared input field and press `Ctrl+Enter`. Results for all the queries should be displayed simultaneously on the same graph.
VMUI allows investigating correlations between multiple queries on the same graph. Just click `Add Query` button,
enter an additional query in the newly appeared input field and press `Enter`.
Results for all the queries are displayed simultaneously on the same graph.
Graphs for a particular query can be temporarily hidden by clicking the `eye` icon on the right side of the input field.
See the [example VMUI at VictoriaMetrics playground](https://play.victoriametrics.com/select/accounting/1/6a716b0f-38bc-4856-90ce-448fd713e3fe/prometheus/graph/?g0.expr=100%20*%20sum(rate(process_cpu_seconds_total))%20by%20(job)&g0.range_input=1d).
@@ -780,7 +785,7 @@ to your needs or when testing bugfixes.
### Development build
1. [Install Go](https://golang.org/doc/install). The minimum supported version is Go 1.19.2.
1. [Install Go](https://golang.org/doc/install). The minimum supported version is Go 1.19.3.
2. Run `make victoria-metrics` from the root folder of [the repository](https://github.com/VictoriaMetrics/VictoriaMetrics).
It builds `victoria-metrics` binary and puts it into the `bin` folder.
@@ -796,7 +801,7 @@ ARM build may run on Raspberry Pi or on [energy-efficient ARM servers](https://b
### Development ARM build
1. [Install Go](https://golang.org/doc/install). The minimum supported version is Go 1.19.2.
1. [Install Go](https://golang.org/doc/install). The minimum supported version is Go 1.19.3.
2. Run `make victoria-metrics-linux-arm` or `make victoria-metrics-linux-arm64` from the root folder of [the repository](https://github.com/VictoriaMetrics/VictoriaMetrics).
It builds `victoria-metrics-linux-arm` or `victoria-metrics-linux-arm64` binary respectively and puts it into the `bin` folder.
@@ -810,7 +815,7 @@ ARM build may run on Raspberry Pi or on [energy-efficient ARM servers](https://b
`Pure Go` mode builds only Go code without [cgo](https://golang.org/cmd/cgo/) dependencies.
1. [Install Go](https://golang.org/doc/install). The minimum supported version is Go 1.19.2.
1. [Install Go](https://golang.org/doc/install). The minimum supported version is Go 1.19.3.
2. Run `make victoria-metrics-pure` from the root folder of [the repository](https://github.com/VictoriaMetrics/VictoriaMetrics).
It builds `victoria-metrics-pure` binary and puts it into the `bin` folder.
@@ -872,8 +877,9 @@ Steps for restoring from a snapshot:
Send a request to `http://<victoriametrics-addr>:8428/api/v1/admin/tsdb/delete_series?match[]=<timeseries_selector_for_delete>`,
where `<timeseries_selector_for_delete>` may contain any [time series selector](https://prometheus.io/docs/prometheus/latest/querying/basics/#time-series-selectors)
for metrics to delete. After that all the time series matching the given selector are deleted. Storage space for
the deleted time series isn't freed instantly - it is freed during subsequent [background merges of data files](https://medium.com/@valyala/how-victoriametrics-makes-instant-snapshots-for-multi-terabyte-time-series-data-e1f3fb0e0282).
for metrics to delete. Delete API doesn't support the deletion of specific time ranges, the series can only be deleted completely.
Storage space for the deleted time series isn't freed instantly - it is freed during subsequent
[background merges of data files](https://medium.com/@valyala/how-victoriametrics-makes-instant-snapshots-for-multi-terabyte-time-series-data-e1f3fb0e0282).
Note that background merges may never occur for data from previous months, so storage space won't be freed for historical data.
In this case [forced merge](#forced-merge) may help freeing up storage space.
@@ -1042,7 +1048,8 @@ Time series data can be imported into VictoriaMetrics via any supported data ing
* `/api/v1/import/native` for importing data obtained from [/api/v1/export/native](#how-to-export-data-in-native-format).
See [these docs](#how-to-import-data-in-native-format) for details.
* `/api/v1/import/csv` for importing arbitrary CSV data. See [these docs](#how-to-import-csv-data) for details.
* `/api/v1/import/prometheus` for importing data in Prometheus exposition format. See [these docs](#how-to-import-data-in-prometheus-exposition-format) for details.
* `/api/v1/import/prometheus` for importing data in Prometheus exposition format and in [Pushgateway format](https://github.com/prometheus/pushgateway#url).
See [these docs](#how-to-import-data-in-prometheus-exposition-format) for details.
### How to import data in JSON line format
@@ -1147,9 +1154,11 @@ Note that it could be required to flush response cache after importing historica
### How to import data in Prometheus exposition format
VictoriaMetrics accepts data in [Prometheus exposition format](https://github.com/prometheus/docs/blob/master/content/docs/instrumenting/exposition_formats.md#text-based-format)
and in [OpenMetrics format](https://github.com/OpenObservability/OpenMetrics/blob/master/specification/OpenMetrics.md)
via `/api/v1/import/prometheus` path. For example, the following line imports a single line in Prometheus exposition format into VictoriaMetrics:
VictoriaMetrics accepts data in [Prometheus exposition format](https://github.com/prometheus/docs/blob/master/content/docs/instrumenting/exposition_formats.md#text-based-format),
in [OpenMetrics format](https://github.com/OpenObservability/OpenMetrics/blob/master/specification/OpenMetrics.md)
and in [Pushgateway format](https://github.com/prometheus/pushgateway#url) via `/api/v1/import/prometheus` path.
For example, the following command imports a single line in Prometheus exposition format into VictoriaMetrics:
<div class="with-copy" markdown="1">
@@ -1175,6 +1184,16 @@ It should return something like the following:
{"metric":{"__name__":"foo","bar":"baz"},"values":[123],"timestamps":[1594370496905]}
```
The following command imports a single metric via [Pushgateway format](https://github.com/prometheus/pushgateway#url) with `{job="my_app",instance="host123"}` labels:
<div class="with-copy" markdown="1">
```console
curl -d 'metric{label="abc"} 123' -X POST 'http://localhost:8428/api/v1/import/prometheus/metrics/job/my_app/instance/host123'
```
</div>
Pass `Content-Encoding: gzip` HTTP request header to `/api/v1/import/prometheus` for importing gzipped data:
<div class="with-copy" markdown="1">
@@ -1186,8 +1205,8 @@ curl -X POST -H 'Content-Encoding: gzip' http://destination-victoriametrics:8428
</div>
Extra labels may be added to all the imported metrics by passing `extra_label=name=value` query args.
For example, `/api/v1/import/prometheus?extra_label=foo=bar` would add `{foo="bar"}` label to all the imported metrics.
Extra labels may be added to all the imported metrics either via [Pushgateway format](https://github.com/prometheus/pushgateway#url)
or by passing `extra_label=name=value` query args. For example, `/api/v1/import/prometheus?extra_label=foo=bar` would add `{foo="bar"}` label to all the imported metrics.
If timestamp is missing in `<metric> <value> <timestamp>` Prometheus exposition format line, then the current timestamp is used during data ingestion.
It can be overridden by passing unix timestamp in *milliseconds* via `timestamp` query arg. For example, `/api/v1/import/prometheus?timestamp=1594370496905`.
@@ -1517,6 +1536,8 @@ VictoriaMetrics provides the following security-related command-line flags:
Explicitly set internal network interface for TCP and UDP ports for data ingestion with Graphite and OpenTSDB formats.
For example, substitute `-graphiteListenAddr=:2003` with `-graphiteListenAddr=<internal_iface_ip>:2003`. This protects from unexpected requests from untrusted network interfaces.
VictoriaMetrics has achieved security certifications for Database Software Development and Software-Based Monitoring Services. We apply strict security measures in everything we do. See our [Security page](https://victoriametrics.com/security/) for more details.
## Tuning
* No need in tuning for VictoriaMetrics - it uses reasonable defaults for command-line flags,
@@ -1636,7 +1657,9 @@ All the durations and timestamps in traces are in milliseconds.
Query tracing is allowed by default. It can be denied by passing `-denyQueryTracing` command-line flag to VictoriaMetrics.
[VMUI](#vmui) provides an UI for query tracing - just click `Trace query` checkbox and re-run the query in order to investigate its' trace.
[VMUI](#vmui) provides an UI:
- for query tracing - just click `Trace query` checkbox and re-run the query in order to investigate its' trace.
- for exploring custom trace - go to the tab `Trace analyzer` and upload or paste JSON with trace information.
## Cardinality limiter
@@ -2025,6 +2048,8 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li
```
-bigMergeConcurrency int
The maximum number of CPU cores to use for big merges. Default value is used if set to 0
-cacheExpireDuration duration
Items are removed from in-memory caches after they aren't accessed for this duration. Lower values may reduce memory usage at the cost of higher CPU usage. See also -prevCacheRemovalPercent (default 30m0s)
-configAuthKey string
Authorization key for accessing /config page. It must be passed via authKey query arg
-csvTrimTimestamp duration
@@ -2043,7 +2068,7 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li
-denyQueryTracing
Whether to disable the ability to trace queries. See https://docs.victoriametrics.com/#query-tracing
-downsampling.period array
Comma-separated downsampling periods in the format 'offset:period'. For example, '30d:10m' instructs to leave a single sample per 10 minutes for samples older than 30 days. See https://docs.victoriametrics.com/#downsampling for details. This flag is available only in enterprise version of VictoriaMetrics
Comma-separated downsampling periods in the format 'offset:period'. For example, '30d:10m' instructs to leave a single sample per 10 minutes for samples older than 30 days. See https://docs.victoriametrics.com/#downsampling for details. This flag is available only in VictoriaMetrics enterprise. See https://docs.victoriametrics.com/enterprise.html
Supports an array of values separated by comma or specified via multiple flags.
-dryRun
Whether to check only -promscrape.config and then exit. Unknown config entries aren't allowed in -promscrape.config by default. This can be changed with -promscrape.config.strictParse=false command-line flag
@@ -2054,7 +2079,7 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li
-envflag.prefix string
Prefix for environment variables if -envflag.enable is set
-eula
By specifying this flag, you confirm that you have an enterprise license and accept the EULA https://victoriametrics.com/assets/VM_EULA.pdf . This flag is available only in enterprise version of VictoriaMetrics
By specifying this flag, you confirm that you have an enterprise license and accept the EULA https://victoriametrics.com/assets/VM_EULA.pdf . This flag is available only in VictoriaMetrics enterprise. See https://docs.victoriametrics.com/enterprise.html
-finalMergeDelay duration
The delay before starting final merge for per-month partition after no new data is ingested into it. Final merge may require additional disk IO and CPU resources. Final merge may increase query speed and reduce disk space usage in some cases. Zero value disables final merge
-flagsAuthKey string
@@ -2157,6 +2182,8 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li
Auth key for /debug/pprof/* endpoints. It must be passed via authKey query arg. It overrides httpAuth.* settings
-precisionBits int
The number of precision bits to store per each value. Lower precision bits improves data compression at the cost of precision loss (default 64)
-prevCacheRemovalPercent float
Items in the previous caches are removed when the percent of requests it serves becomes lower than this value. Higher values reduce memory usage at the cost of higher CPU usage. See also -cacheExpireDuration (default 0.1)
-promscrape.azureSDCheckInterval duration
Interval for checking for changes in Azure. This works only if azure_sd_configs is configured in '-promscrape.config' file. See https://docs.victoriametrics.com/sd_configs.html#azure_sd_configs for details (default 1m0s)
-promscrape.cluster.memberNum string
@@ -2251,7 +2278,7 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li
-relabelDebug
Whether to log metrics before and after relabeling with -relabelConfig. If the -relabelDebug is enabled, then the metrics aren't sent to storage. This is useful for debugging the relabeling configs
-retentionFilter array
Retention filter in the format 'filter:retention'. For example, '{env="dev"}:3d' configures the retention for time series with env="dev" label to 3 days. See https://docs.victoriametrics.com/#retention-filters for details. This flag is available only in enterprise version of VictoriaMetrics
Retention filter in the format 'filter:retention'. For example, '{env="dev"}:3d' configures the retention for time series with env="dev" label to 3 days. See https://docs.victoriametrics.com/#retention-filters for details. This flag is available only in VictoriaMetrics enterprise. See https://docs.victoriametrics.com/enterprise.html
Supports an array of values separated by comma or specified via multiple flags.
-retentionPeriod value
Data with timestamps outside the retentionPeriod is automatically deleted. See also -retentionFilter
@@ -2265,9 +2292,9 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li
-search.disableCache
Whether to disable response caching. This may be useful during data backfilling
-search.graphiteMaxPointsPerSeries int
The maximum number of points per series Graphite render API can return. This flag is available only in enterprise version of VictoriaMetrics (default 1000000)
The maximum number of points per series Graphite render API can return. This flag is available only in VictoriaMetrics enterprise. See https://docs.victoriametrics.com/enterprise.html (default 1000000)
-search.graphiteStorageStep duration
The interval between datapoints stored in the database. It is used at Graphite Render API handler for normalizing the interval between datapoints in case it isn't normalized. It can be overridden by sending 'storage_step' query arg to /render API or by sending the desired interval via 'Storage-Step' http header during querying /render API. This flag is available only in enterprise version of VictoriaMetrics (default 10s)
The interval between datapoints stored in the database. It is used at Graphite Render API handler for normalizing the interval between datapoints in case it isn't normalized. It can be overridden by sending 'storage_step' query arg to /render API or by sending the desired interval via 'Storage-Step' http header during querying /render API. This flag is available only in VictoriaMetrics enterprise. See https://docs.victoriametrics.com/enterprise.html (default 10s)
-search.latencyOffset duration
The time when data points become visible in query results after the collection. Too small value can result in incomplete last points for query results (default 30s)
-search.logSlowQueryDuration duration
@@ -2281,7 +2308,7 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li
-search.maxFederateSeries int
The maximum number of time series, which can be returned from /federate. This option allows limiting memory usage (default 1000000)
-search.maxGraphiteSeries int
The maximum number of time series, which can be scanned during queries to Graphite Render API. See https://docs.victoriametrics.com/#graphite-render-api-usage . This flag is available only in enterprise version of VictoriaMetrics (default 300000)
The maximum number of time series, which can be scanned during queries to Graphite Render API. See https://docs.victoriametrics.com/#graphite-render-api-usage . This flag is available only in VictoriaMetrics enterprise. See https://docs.victoriametrics.com/enterprise.html (default 300000)
-search.maxLookback duration
Synonym to -search.lookback-delta from Prometheus. The value is dynamically detected from interval between time series datapoints if not set. It can be overridden on per-query basis via max_lookback arg. See also '-search.maxStalenessInterval' flag, which has the same meaining due to historical reasons
-search.maxMemoryPerQuery size

View File

@@ -545,7 +545,7 @@ The following articles contain useful information about Prometheus relabeling:
* `keep_metrics`: keeps all the metrics with names matching the given `regex`,
while dropping all the other metrics. For example, the following relabeling config keeps metrics
with `fo` and `bar` names, while dropping all the other metrics:
with `foo` and `bar` names, while dropping all the other metrics:
```yaml
- action: keep_metrics
@@ -1027,7 +1027,7 @@ It may be needed to build `vmagent` from source code when developing or testing
### Development build
1. [Install Go](https://golang.org/doc/install). The minimum supported version is Go 1.19.2.
1. [Install Go](https://golang.org/doc/install). The minimum supported version is Go 1.19.3.
2. Run `make vmagent` from the root folder of [the repository](https://github.com/VictoriaMetrics/VictoriaMetrics).
It builds the `vmagent` binary and puts it into the `bin` folder.
@@ -1056,7 +1056,7 @@ ARM build may run on Raspberry Pi or on [energy-efficient ARM servers](https://b
### Development ARM build
1. [Install Go](https://golang.org/doc/install). The minimum supported version is Go 1.19.2.
1. [Install Go](https://golang.org/doc/install). The minimum supported version is Go 1.19.3.
2. Run `make vmagent-linux-arm` or `make vmagent-linux-arm64` from the root folder of [the repository](https://github.com/VictoriaMetrics/VictoriaMetrics)
It builds `vmagent-linux-arm` or `vmagent-linux-arm64` binary respectively and puts it into the `bin` folder.
@@ -1107,6 +1107,8 @@ vmagent collects metrics data via popular data ingestion protocols and routes th
See the docs at https://docs.victoriametrics.com/vmagent.html .
-cacheExpireDuration duration
Items are removed from in-memory caches after they aren't accessed for this duration. Lower values may reduce memory usage at the cost of higher CPU usage. See also -prevCacheRemovalPercent (default 30m0s)
-configAuthKey string
Authorization key for accessing /config page. It must be passed via authKey query arg
-csvTrimTimestamp duration
@@ -1127,7 +1129,7 @@ See the docs at https://docs.victoriametrics.com/vmagent.html .
-envflag.prefix string
Prefix for environment variables if -envflag.enable is set
-eula
By specifying this flag, you confirm that you have an enterprise license and accept the EULA https://victoriametrics.com/assets/VM_EULA.pdf . This flag is available only in enterprise version of VictoriaMetrics
By specifying this flag, you confirm that you have an enterprise license and accept the EULA https://victoriametrics.com/assets/VM_EULA.pdf . This flag is available only in VictoriaMetrics enterprise. See https://docs.victoriametrics.com/enterprise.html
-flagsAuthKey string
Auth key for /flags endpoint. It must be passed via authKey query arg. It overrides httpAuth.* settings
-fs.disableMmap
@@ -1178,30 +1180,30 @@ See the docs at https://docs.victoriametrics.com/vmagent.html .
-insert.maxQueueDuration duration
The maximum duration for waiting in the queue for insert requests due to -maxConcurrentInserts (default 1m0s)
-kafka.consumer.topic array
Kafka topic names for data consumption. This flag is available only in enterprise version of VictoriaMetrics
Kafka topic names for data consumption. This flag is available only in VictoriaMetrics enterprise. See https://docs.victoriametrics.com/enterprise.html
Supports an array of values separated by comma or specified via multiple flags.
-kafka.consumer.topic.basicAuth.password array
Optional basic auth password for -kafka.consumer.topic. Must be used in conjunction with any supported auth methods for kafka client, specified by flag -kafka.consumer.topic.options='security.protocol=SASL_SSL;sasl.mechanisms=PLAIN' . This flag is available only in enterprise version of VictoriaMetrics
Optional basic auth password for -kafka.consumer.topic. Must be used in conjunction with any supported auth methods for kafka client, specified by flag -kafka.consumer.topic.options='security.protocol=SASL_SSL;sasl.mechanisms=PLAIN' . This flag is available only in VictoriaMetrics enterprise. See https://docs.victoriametrics.com/enterprise.html
Supports an array of values separated by comma or specified via multiple flags.
-kafka.consumer.topic.basicAuth.username array
Optional basic auth username for -kafka.consumer.topic. Must be used in conjunction with any supported auth methods for kafka client, specified by flag -kafka.consumer.topic.options='security.protocol=SASL_SSL;sasl.mechanisms=PLAIN' . This flag is available only in enterprise version of VictoriaMetrics
Optional basic auth username for -kafka.consumer.topic. Must be used in conjunction with any supported auth methods for kafka client, specified by flag -kafka.consumer.topic.options='security.protocol=SASL_SSL;sasl.mechanisms=PLAIN' . This flag is available only in VictoriaMetrics enterprise. See https://docs.victoriametrics.com/enterprise.html
Supports an array of values separated by comma or specified via multiple flags.
-kafka.consumer.topic.brokers array
List of brokers to connect for given topic, e.g. -kafka.consumer.topic.broker=host-1:9092;host-2:9092 . This flag is available only in enterprise version of VictoriaMetrics
List of brokers to connect for given topic, e.g. -kafka.consumer.topic.broker=host-1:9092;host-2:9092 . This flag is available only in VictoriaMetrics enterprise. See https://docs.victoriametrics.com/enterprise.html
Supports an array of values separated by comma or specified via multiple flags.
-kafka.consumer.topic.defaultFormat string
Expected data format in the topic if -kafka.consumer.topic.format is skipped. This flag is available only in enterprise version of VictoriaMetrics (default "promremotewrite")
Expected data format in the topic if -kafka.consumer.topic.format is skipped. This flag is available only in VictoriaMetrics enterprise. See https://docs.victoriametrics.com/enterprise.html (default "promremotewrite")
-kafka.consumer.topic.format array
data format for corresponding kafka topic. Valid formats: influx, prometheus, promremotewrite, graphite, jsonline . This flag is available only in enterprise version of VictoriaMetrics
data format for corresponding kafka topic. Valid formats: influx, prometheus, promremotewrite, graphite, jsonline . This flag is available only in VictoriaMetrics enterprise. See https://docs.victoriametrics.com/enterprise.html
Supports an array of values separated by comma or specified via multiple flags.
-kafka.consumer.topic.groupID array
Defines group.id for topic. This flag is available only in enterprise version of VictoriaMetrics
Defines group.id for topic. This flag is available only in VictoriaMetrics enterprise. See https://docs.victoriametrics.com/enterprise.html
Supports an array of values separated by comma or specified via multiple flags.
-kafka.consumer.topic.isGzipped array
Enables gzip setting for topic messages payload. Only prometheus, jsonline and influx formats accept gzipped messages.This flag is available only in enterprise version of VictoriaMetrics
Enables gzip setting for topic messages payload. Only prometheus, jsonline and influx formats accept gzipped messages.This flag is available only in VictoriaMetrics enterprise. See https://docs.victoriametrics.com/enterprise.html
Supports array of values separated by comma or specified via multiple flags.
-kafka.consumer.topic.options array
Optional key=value;key1=value2 settings for topic consumer. See full configuration options at https://github.com/edenhill/librdkafka/blob/master/CONFIGURATION.md . This flag is available only in enterprise version of VictoriaMetrics
Optional key=value;key1=value2 settings for topic consumer. See full configuration options at https://github.com/edenhill/librdkafka/blob/master/CONFIGURATION.md . This flag is available only in VictoriaMetrics enterprise. See https://docs.victoriametrics.com/enterprise.html
Supports an array of values separated by comma or specified via multiple flags.
-loggerDisableTimestamps
Whether to disable writing timestamps in logs
@@ -1242,6 +1244,8 @@ See the docs at https://docs.victoriametrics.com/vmagent.html .
Trim timestamps for OpenTSDB HTTP data to this duration. Minimum practical duration is 1ms. Higher duration (i.e. 1s) may be used for reducing disk space usage for timestamp data (default 1ms)
-pprofAuthKey string
Auth key for /debug/pprof/* endpoints. It must be passed via authKey query arg. It overrides httpAuth.* settings
-prevCacheRemovalPercent float
Items in the previous caches are removed when the percent of requests it serves becomes lower than this value. Higher values reduce memory usage at the cost of higher CPU usage. See also -cacheExpireDuration (default 0.1)
-promscrape.azureSDCheckInterval duration
Interval for checking for changes in Azure. This works only if azure_sd_configs is configured in '-promscrape.config' file. See https://docs.victoriametrics.com/sd_configs.html#azure_sd_configs for details (default 1m0s)
-promscrape.cluster.memberNum string

View File

@@ -217,6 +217,16 @@ func requestHandler(w http.ResponseWriter, r *http.Request) bool {
}
path := strings.Replace(r.URL.Path, "//", "/", -1)
if strings.HasPrefix(path, "/prometheus/api/v1/import/prometheus") || strings.HasPrefix(path, "/api/v1/import/prometheus") {
prometheusimportRequests.Inc()
if err := prometheusimport.InsertHandler(nil, r); err != nil {
prometheusimportErrors.Inc()
httpserver.Errorf(w, r, "%s", err)
return true
}
w.WriteHeader(http.StatusNoContent)
return true
}
if strings.HasPrefix(path, "datadog/") {
// Trim suffix from paths starting from /datadog/ in order to support legacy DataDog agent.
// See https://github.com/VictoriaMetrics/VictoriaMetrics/pull/2670
@@ -250,15 +260,6 @@ func requestHandler(w http.ResponseWriter, r *http.Request) bool {
}
w.WriteHeader(http.StatusNoContent)
return true
case "/prometheus/api/v1/import/prometheus", "/api/v1/import/prometheus":
prometheusimportRequests.Inc()
if err := prometheusimport.InsertHandler(nil, r); err != nil {
prometheusimportErrors.Inc()
httpserver.Errorf(w, r, "%s", err)
return true
}
w.WriteHeader(http.StatusNoContent)
return true
case "/prometheus/api/v1/import/native", "/api/v1/import/native":
nativeimportRequests.Inc()
if err := native.InsertHandler(nil, r); err != nil {
@@ -409,6 +410,16 @@ func processMultitenantRequest(w http.ResponseWriter, r *http.Request, path stri
httpserver.Errorf(w, r, "cannot obtain auth token: %s", err)
return true
}
if strings.HasPrefix(p.Suffix, "prometheus/api/v1/import/prometheus") {
prometheusimportRequests.Inc()
if err := prometheusimport.InsertHandler(at, r); err != nil {
prometheusimportErrors.Inc()
httpserver.Errorf(w, r, "%s", err)
return true
}
w.WriteHeader(http.StatusNoContent)
return true
}
if strings.HasPrefix(p.Suffix, "datadog/") {
// Trim suffix from paths starting from /datadog/ in order to support legacy DataDog agent.
// See https://github.com/VictoriaMetrics/VictoriaMetrics/pull/2670
@@ -442,15 +453,6 @@ func processMultitenantRequest(w http.ResponseWriter, r *http.Request, path stri
}
w.WriteHeader(http.StatusNoContent)
return true
case "prometheus/api/v1/import/prometheus":
prometheusimportRequests.Inc()
if err := prometheusimport.InsertHandler(at, r); err != nil {
prometheusimportErrors.Inc()
httpserver.Errorf(w, r, "%s", err)
return true
}
w.WriteHeader(http.StatusNoContent)
return true
case "prometheus/api/v1/import/native":
nativeimportRequests.Inc()
if err := native.InsertHandler(at, r); err != nil {

View File

@@ -13,6 +13,7 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bloomfilter"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/cgroup"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fasttime"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/memory"
@@ -139,6 +140,8 @@ func Init() {
logger.Fatalf("cannot load relabel configs: %s", err)
}
allRelabelConfigs.Store(rcs)
configSuccess.Set(1)
configTimestamp.Set(fasttime.UnixTimestamp())
if len(*remoteWriteURLs) > 0 {
rwctxsDefault = newRemoteWriteCtxs(nil, *remoteWriteURLs)
@@ -154,18 +157,31 @@ func Init() {
case <-stopCh:
return
}
configReloads.Inc()
logger.Infof("SIGHUP received; reloading relabel configs pointed by -remoteWrite.relabelConfig and -remoteWrite.urlRelabelConfig")
rcs, err := loadRelabelConfigs()
if err != nil {
configReloadErrors.Inc()
configSuccess.Set(0)
logger.Errorf("cannot reload relabel configs; preserving the previous configs; error: %s", err)
continue
}
allRelabelConfigs.Store(rcs)
configSuccess.Set(1)
configTimestamp.Set(fasttime.UnixTimestamp())
logger.Infof("Successfully reloaded relabel configs")
}
}()
}
var (
configReloads = metrics.NewCounter(`vmagent_relabel_config_reloads_total`)
configReloadErrors = metrics.NewCounter(`vmagent_relabel_config_reloads_errors_total`)
configSuccess = metrics.NewCounter(`vmagent_relabel_config_last_reload_successful`)
configTimestamp = metrics.NewCounter(`vmagent_relabel_config_last_reload_success_timestamp_seconds`)
)
func newRemoteWriteCtxs(at *auth.Token, urls []string) []*remoteWriteCtx {
if len(urls) == 0 {
logger.Panicf("BUG: urls must be non-empty")

View File

@@ -673,10 +673,12 @@ See full description for these flags in `./vmalert -help`.
## Monitoring
`vmalert` exports various metrics in Prometheus exposition format at `http://vmalert-host:8880/metrics` page.
The default list of alerting rules for these metric can be found [here](https://github.com/VictoriaMetrics/VictoriaMetrics/tree/master/deployment/docker).
We recommend setting up regular scraping of this page either through `vmagent` or by Prometheus so that the exported
metrics may be analyzed later.
Use the official [Grafana dashboard](https://grafana.com/grafana/dashboards/14950) for `vmalert` overview. Graphs on this dashboard contain useful hints - hover the `i` icon at the top left corner of each graph in order to read it.
Use the official [Grafana dashboard](https://grafana.com/grafana/dashboards/14950) for `vmalert` overview.
Graphs on this dashboard contain useful hints - hover the `i` icon in the top left corner of each graph in order to read it.
If you have suggestions for improvements or have found a bug - please open an issue on github or add
a review to the dashboard.
@@ -781,7 +783,7 @@ The shortlist of configuration flags is the following:
{% raw %}
```
-clusterMode
If clusterMode is enabled, then vmalert automatically adds the tenant specified in config groups to -datasource.url, -remoteWrite.url and -remoteRead.url. See https://docs.victoriametrics.com/vmalert.html#multitenancy . This flag is available only in enterprise version of VictoriaMetrics
If clusterMode is enabled, then vmalert automatically adds the tenant specified in config groups to -datasource.url, -remoteWrite.url and -remoteRead.url. See https://docs.victoriametrics.com/vmalert.html#multitenancy . This flag is available only in VictoriaMetrics enterprise. See https://docs.victoriametrics.com/enterprise.html
-configCheckInterval duration
Interval for checking for changes in '-rule' or '-notifier.config' files. By default the checking is disabled. Send SIGHUP signal in order to force config check for changes.
-datasource.appendTypePrefix
@@ -805,11 +807,11 @@ The shortlist of configuration flags is the following:
-datasource.maxIdleConnections int
Defines the number of idle (keep-alive connections) to each configured datasource. Consider setting this value equal to the value: groups_total * group.concurrency. Too low a value may result in a high number of sockets in TIME_WAIT state. (default 100)
-datasource.oauth2.clientID string
Optional OAuth2 clientID to use for -datasource.url.
Optional OAuth2 clientID to use for -datasource.url.
-datasource.oauth2.clientSecret string
Optional OAuth2 clientSecret to use for -datasource.url.
-datasource.oauth2.clientSecretFile string
Optional OAuth2 clientSecretFile to use for -datasource.url.
Optional OAuth2 clientSecretFile to use for -datasource.url.
-datasource.oauth2.scopes string
Optional OAuth2 scopes to use for -datasource.url. Scopes must be delimited by ';'
-datasource.oauth2.tokenUrl string
@@ -835,9 +837,9 @@ The shortlist of configuration flags is the following:
-datasource.url string
Datasource compatible with Prometheus HTTP API. It can be single node VictoriaMetrics or vmselect URL. Required parameter. E.g. http://127.0.0.1:8428 . See also '-datasource.disablePathAppend', '-datasource.showURL'.
-defaultTenant.graphite string
Default tenant for Graphite alerting groups. See https://docs.victoriametrics.com/vmalert.html#multitenancy .This flag is available only in enterprise version of VictoriaMetrics
Default tenant for Graphite alerting groups. See https://docs.victoriametrics.com/vmalert.html#multitenancy .This flag is available only in VictoriaMetrics enterprise. See https://docs.victoriametrics.com/enterprise.html
-defaultTenant.prometheus string
Default tenant for Prometheus alerting groups. See https://docs.victoriametrics.com/vmalert.html#multitenancy . This flag is available only in enterprise version of VictoriaMetrics
Default tenant for Prometheus alerting groups. See https://docs.victoriametrics.com/vmalert.html#multitenancy . This flag is available only in VictoriaMetrics enterprise. See https://docs.victoriametrics.com/enterprise.html
-disableAlertgroupLabel
Whether to disable adding group's Name as label to generated alerts and time series.
-dryRun
@@ -849,12 +851,11 @@ The shortlist of configuration flags is the following:
-envflag.prefix string
Prefix for environment variables if -envflag.enable is set
-eula
By specifying this flag, you confirm that you have an enterprise license and accept the EULA https://victoriametrics.com/assets/VM_EULA.pdf . This flag is available only in enterprise version of VictoriaMetrics
By specifying this flag, you confirm that you have an enterprise license and accept the EULA https://victoriametrics.com/assets/VM_EULA.pdf . This flag is available only in VictoriaMetrics enterprise. See https://docs.victoriametrics.com/enterprise.html
-evaluationInterval duration
How often to evaluate the rules (default 1m0s)
-external.alert.source string
External Alert Source allows to override the Source link for alerts sent to AlertManager for cases where you want to build a custom link to Grafana, Prometheus or any other service. Supports templating - see https://docs.victoriametrics.com/vmalert.html#templating . For example, link to Grafana: -external.alert.source='explore?orgId=1&left=["now-1h","now","VictoriaMetrics",{"expr":{{$expr|jsonEscape|queryEscape}} },{"mode":"Metrics"},{"ui":[true,true,true,"none"]}]' . If empty 'vmalert/alert?group_id={{.GroupID}}&alert_id={{.AlertID}}' is used
If empty 'vmalert/alert?group_id={{.GroupID}}&alert_id={{.AlertID}}' is used.
External Alert Source allows to override the Source link for alerts sent to AlertManager for cases where you want to build a custom link to Grafana, Prometheus or any other service. Supports templating - see https://docs.victoriametrics.com/vmalert.html#templating . For example, link to Grafana: -external.alert.source='explore?orgId=1&left=["now-1h","now","VictoriaMetrics",{"expr":{{$expr|jsonEscape|queryEscape}} },{"mode":"Metrics"},{"ui":[true,true,true,"none"]}]' . If empty 'vmalert/alert?group_id={{.GroupID}}&alert_id={{.AlertID}}' is used.
-external.label array
Optional label in the form 'Name=value' to add to all generated recording rules and alerts. Pass multiple -label flags in order to add multiple label sets.
Supports an array of values separated by comma or specified via multiple flags.
@@ -1002,7 +1003,7 @@ The shortlist of configuration flags is the following:
-remoteRead.oauth2.scopes string
Optional OAuth2 scopes to use for -remoteRead.url. Scopes must be delimited by ';'.
-remoteRead.oauth2.tokenUrl string
Optional OAuth2 tokenURL to use for -remoteRead.url.
Optional OAuth2 tokenURL to use for -remoteRead.url.
-remoteRead.showURL
Whether to show -remoteRead.url in the exported metrics. It is hidden by default, since it can contain sensitive info such as auth key
-remoteRead.tlsCAFile string
@@ -1314,7 +1315,7 @@ spec:
### Development build
1. [Install Go](https://golang.org/doc/install). The minimum supported version is Go 1.19.2.
1. [Install Go](https://golang.org/doc/install). The minimum supported version is Go 1.19.3.
2. Run `make vmalert` from the root folder of [the repository](https://github.com/VictoriaMetrics/VictoriaMetrics).
It builds `vmalert` binary and puts it into the `bin` folder.
@@ -1330,7 +1331,7 @@ ARM build may run on Raspberry Pi or on [energy-efficient ARM servers](https://b
### Development ARM build
1. [Install Go](https://golang.org/doc/install). The minimum supported version is Go 1.19.2.
1. [Install Go](https://golang.org/doc/install). The minimum supported version is Go 1.19.3.
2. Run `make vmalert-linux-arm` or `make vmalert-linux-arm64` from the root folder of [the repository](https://github.com/VictoriaMetrics/VictoriaMetrics).
It builds `vmalert-linux-arm` or `vmalert-linux-arm64` binary respectively and puts it into the `bin` folder.

View File

@@ -167,7 +167,7 @@ It is recommended using [binary releases](https://github.com/VictoriaMetrics/Vic
### Development build
1. [Install Go](https://golang.org/doc/install). The minimum supported version is Go 1.19.2.
1. [Install Go](https://golang.org/doc/install). The minimum supported version is Go 1.19.3.
2. Run `make vmauth` from the root folder of [the repository](https://github.com/VictoriaMetrics/VictoriaMetrics).
It builds `vmauth` binary and puts it into the `bin` folder.
@@ -239,7 +239,7 @@ See the docs at https://docs.victoriametrics.com/vmauth.html .
-envflag.prefix string
Prefix for environment variables if -envflag.enable is set
-eula
By specifying this flag, you confirm that you have an enterprise license and accept the EULA https://victoriametrics.com/assets/VM_EULA.pdf . This flag is available only in enterprise version of VictoriaMetrics
By specifying this flag, you confirm that you have an enterprise license and accept the EULA https://victoriametrics.com/assets/VM_EULA.pdf . This flag is available only in VictoriaMetrics enterprise. See https://docs.victoriametrics.com/enterprise.html
-flagsAuthKey string
Auth key for /flags endpoint. It must be passed via authKey query arg. It overrides httpAuth.* settings
-fs.disableMmap

View File

@@ -196,7 +196,7 @@ See [this article](https://medium.com/@valyala/speeding-up-backups-for-big-time-
-envflag.prefix string
Prefix for environment variables if -envflag.enable is set
-eula
By specifying this flag, you confirm that you have an enterprise license and accept the EULA https://victoriametrics.com/assets/VM_EULA.pdf . This flag is available only in enterprise version of VictoriaMetrics
By specifying this flag, you confirm that you have an enterprise license and accept the EULA https://victoriametrics.com/assets/VM_EULA.pdf . This flag is available only in VictoriaMetrics enterprise. See https://docs.victoriametrics.com/enterprise.html
-flagsAuthKey string
Auth key for /flags endpoint. It must be passed via authKey query arg. It overrides httpAuth.* settings
-fs.disableMmap
@@ -286,7 +286,7 @@ It is recommended using [binary releases](https://github.com/VictoriaMetrics/Vic
### Development build
1. [Install Go](https://golang.org/doc/install). The minimum supported version is Go 1.19.2.
1. [Install Go](https://golang.org/doc/install). The minimum supported version is Go 1.19.3.
2. Run `make vmbackup` from the root folder of [the repository](https://github.com/VictoriaMetrics/VictoriaMetrics).
It builds `vmbackup` binary and puts it into the `bin` folder.

View File

@@ -299,6 +299,13 @@ The shortlist of configuration flags is the following:
```
vmbackupmanager performs regular backups according to the provided configs.
subcommands:
backup: provides auxiliary backup-related commands
restore: restores backup specified by restore mark if it exists
command-line flags:
-apiURL string
vmbackupmanager address to perform API requests (default "http://127.0.0.1:8300")
-concurrency int
The number of concurrent workers. Higher concurrency may reduce backup duration (default 10)
-configFilePath string
@@ -328,7 +335,7 @@ vmbackupmanager performs regular backups according to the provided configs.
-envflag.prefix string
Prefix for environment variables if -envflag.enable is set
-eula
By specifying this flag, you confirm that you have an enterprise license and accept the EULA https://victoriametrics.com/assets/VM_EULA.pdf . This flag is available only in enterprise version of VictoriaMetrics
By specifying this flag, you confirm that you have an enterprise license and accept the EULA https://victoriametrics.com/assets/VM_EULA.pdf . This flag is available only in VictoriaMetrics enterprise. See https://docs.victoriametrics.com/enterprise.html
-flagsAuthKey string
Auth key for /flags endpoint. It must be passed via authKey query arg. It overrides httpAuth.* settings
-fs.disableMmap
@@ -392,8 +399,6 @@ vmbackupmanager performs regular backups according to the provided configs.
-pushmetrics.url array
Optional URL to push metrics exposed at /metrics page. See https://docs.victoriametrics.com/#push-metrics . By default metrics exposed at /metrics page aren't pushed to any remote storage
Supports an array of values separated by comma or specified via multiple flags.
-restoreOnStart
Check if backup restore was requested and restore requested backup.
-runOnStart
Upload backups immediately after start of the service. Otherwise the backup starts on new hour
-s3ForcePathStyle

View File

@@ -700,7 +700,7 @@ It is recommended using [binary releases](https://github.com/VictoriaMetrics/Vic
### Development build
1. [Install Go](https://golang.org/doc/install). The minimum supported version is Go 1.19.2.
1. [Install Go](https://golang.org/doc/install). The minimum supported version is Go 1.19.3.
2. Run `make vmctl` from the root folder of [the repository](https://github.com/VictoriaMetrics/VictoriaMetrics).
It builds `vmctl` binary and puts it into the `bin` folder.
@@ -729,7 +729,7 @@ ARM build may run on Raspberry Pi or on [energy-efficient ARM servers](https://b
#### Development ARM build
1. [Install Go](https://golang.org/doc/install). The minimum supported version is Go 1.19.2.
1. [Install Go](https://golang.org/doc/install). The minimum supported version is Go 1.19.3.
2. Run `make vmctl-linux-arm` or `make vmctl-linux-arm64` from the root folder of [the repository](https://github.com/VictoriaMetrics/VictoriaMetrics).
It builds `vmctl-linux-arm` or `vmctl-linux-arm64` binary respectively and puts it into the `bin` folder.

View File

@@ -34,6 +34,9 @@ func main() {
Name: "vmctl",
Usage: "VictoriaMetrics command-line tool",
Version: buildinfo.Version,
// Disable `-version` flag to avoid conflict with lib/buildinfo flags
// see https://github.com/urfave/cli/issues/1560
HideVersion: true,
Commands: []*cli.Command{
{
Name: "opentsdb",

View File

@@ -176,8 +176,8 @@ curl 'http://localhost:8431/api/v1/labels' -H 'Authorization: Bearer eyJhbGciOiJ
The shortlist of configuration flags include the following:
```console
-auth.httpHeader
HTTP header name to look for JWT authorization token
-auth.httpHeader string
HTTP header name to look for JWT authorization token (default "Authorization")
-clusterMode
enable this for the cluster version
-datasource.appendTypePrefix
@@ -201,17 +201,17 @@ The shortlist of configuration flags include the following:
-datasource.maxIdleConnections int
Defines the number of idle (keep-alive connections) to each configured datasource. Consider setting this value equal to the value: groups_total * group.concurrency. Too low a value may result in a high number of sockets in TIME_WAIT state. (default 100)
-datasource.oauth2.clientID string
Optional OAuth2 clientID to use for -datasource.url.
Optional OAuth2 clientID to use for -datasource.url.
-datasource.oauth2.clientSecret string
Optional OAuth2 clientSecret to use for -datasource.url.
-datasource.oauth2.clientSecretFile string
Optional OAuth2 clientSecretFile to use for -datasource.url.
Optional OAuth2 clientSecretFile to use for -datasource.url.
-datasource.oauth2.scopes string
Optional OAuth2 scopes to use for -datasource.url. Scopes must be delimited by ';'
-datasource.oauth2.tokenUrl string
Optional OAuth2 tokenURL to use for -datasource.url.
-datasource.queryStep duration
queryStep defines how far a value can fallback to when evaluating queries. For example, if datasource.queryStep=15s then param "step" with value "15s" will be added to every query.If queryStep isn't specified, rule's evaluationInterval will be used instead.
How far a value can fallback to when evaluating queries. For example, if -datasource.queryStep=15s then param "step" with value "15s" will be added to every query. If set to 0, rule's evaluation interval will be used instead. (default 5m0s)
-datasource.queryTimeAlignment
Whether to align "time" parameter with evaluation interval.Alignment supposed to produce deterministic results despite of number of vmalert replicas or time they were started. See more details here https://github.com/VictoriaMetrics/VictoriaMetrics/pull/1257 (default true)
-datasource.roundDigits int
@@ -241,7 +241,7 @@ The shortlist of configuration flags include the following:
-envflag.prefix string
Prefix for environment variables if -envflag.enable is set
-eula
By specifying this flag, you confirm that you have an enterprise license and accept the EULA https://victoriametrics.com/assets/VM_EULA.pdf . This flag is available only in enterprise version of VictoriaMetrics
By specifying this flag, you confirm that you have an enterprise license and accept the EULA https://victoriametrics.com/assets/VM_EULA.pdf . This flag is available only in VictoriaMetrics enterprise. See https://docs.victoriametrics.com/enterprise.html
-flagsAuthKey string
Auth key for /flags endpoint. It must be passed via authKey query arg. It overrides httpAuth.* settings
-fs.disableMmap

View File

@@ -120,6 +120,16 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
staticServer.ServeHTTP(w, r)
return true
}
if strings.HasPrefix(path, "/prometheus/api/v1/import/prometheus") || strings.HasPrefix(path, "/api/v1/import/prometheus") {
prometheusimportRequests.Inc()
if err := prometheusimport.InsertHandler(r); err != nil {
prometheusimportErrors.Inc()
httpserver.Errorf(w, r, "%s", err)
return true
}
w.WriteHeader(http.StatusNoContent)
return true
}
if strings.HasPrefix(path, "/datadog/") {
// Trim suffix from paths starting from /datadog/ in order to support legacy DataDog agent.
// See https://github.com/VictoriaMetrics/VictoriaMetrics/pull/2670
@@ -153,15 +163,6 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
}
w.WriteHeader(http.StatusNoContent)
return true
case "/prometheus/api/v1/import/prometheus", "/api/v1/import/prometheus":
prometheusimportRequests.Inc()
if err := prometheusimport.InsertHandler(r); err != nil {
prometheusimportErrors.Inc()
httpserver.Errorf(w, r, "%s", err)
return true
}
w.WriteHeader(http.StatusNoContent)
return true
case "/prometheus/api/v1/import/native", "/api/v1/import/native":
nativeimportRequests.Inc()
if err := native.InsertHandler(r); err != nil {

View File

@@ -6,6 +6,7 @@ import (
"sync/atomic"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fasttime"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/procutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompb"
@@ -38,23 +39,38 @@ func Init() {
logger.Fatalf("cannot load relabelConfig: %s", err)
}
pcsGlobal.Store(pcs)
configSuccess.Set(1)
configTimestamp.Set(fasttime.UnixTimestamp())
if len(*relabelConfig) == 0 {
return
}
go func() {
for range sighupCh {
configReloads.Inc()
logger.Infof("received SIGHUP; reloading -relabelConfig=%q...", *relabelConfig)
pcs, err := loadRelabelConfig()
if err != nil {
configReloadErrors.Inc()
configSuccess.Set(0)
logger.Errorf("cannot load the updated relabelConfig: %s; preserving the previous config", err)
continue
}
pcsGlobal.Store(pcs)
configSuccess.Set(1)
configTimestamp.Set(fasttime.UnixTimestamp())
logger.Infof("successfully reloaded -relabelConfig=%q", *relabelConfig)
}
}()
}
var (
configReloads = metrics.NewCounter(`vm_relabel_config_reloads_total`)
configReloadErrors = metrics.NewCounter(`vm_relabel_config_reloads_errors_total`)
configSuccess = metrics.NewCounter(`vm_relabel_config_last_reload_successful`)
configTimestamp = metrics.NewCounter(`vm_relabel_config_last_reload_success_timestamp_seconds`)
)
var pcsGlobal atomic.Value
func loadRelabelConfig() (*promrelabel.ParsedConfigs, error) {

View File

@@ -100,7 +100,7 @@ i.e. the end result would be similar to [rsync --delete](https://askubuntu.com/q
-envflag.prefix string
Prefix for environment variables if -envflag.enable is set
-eula
By specifying this flag, you confirm that you have an enterprise license and accept the EULA https://victoriametrics.com/assets/VM_EULA.pdf . This flag is available only in enterprise version of VictoriaMetrics
By specifying this flag, you confirm that you have an enterprise license and accept the EULA https://victoriametrics.com/assets/VM_EULA.pdf . This flag is available only in VictoriaMetrics enterprise. See https://docs.victoriametrics.com/enterprise.html
-flagsAuthKey string
Auth key for /flags endpoint. It must be passed via authKey query arg. It overrides httpAuth.* settings
-fs.disableMmap
@@ -186,7 +186,7 @@ It is recommended using [binary releases](https://github.com/VictoriaMetrics/Vic
### Development build
1. [Install Go](https://golang.org/doc/install). The minimum supported version is Go 1.19.2.
1. [Install Go](https://golang.org/doc/install). The minimum supported version is Go 1.19.3.
2. Run `make vmrestore` from the root folder of [the repository](https://github.com/VictoriaMetrics/VictoriaMetrics).
It builds `vmrestore` binary and puts it into the `bin` folder.

View File

@@ -1011,7 +1011,10 @@ func ProcessSearchQuery(qt *querytracer.Tracer, sq *storage.SearchQuery, deadlin
startTime := time.Now()
maxSeriesCount := sr.Init(qt, vmstorage.Storage, tfss, tr, sq.MaxMetrics, deadline.Deadline())
indexSearchDuration.UpdateDuration(startTime)
m := make(map[string][]blockRef, maxSeriesCount)
type blockRefs struct {
brs []blockRef
}
m := make(map[string]*blockRefs, maxSeriesCount)
orderedMetricNames := make([]string, 0, maxSeriesCount)
blocksRead := 0
samples := 0
@@ -1040,13 +1043,14 @@ func ProcessSearchQuery(qt *querytracer.Tracer, sq *storage.SearchQuery, deadlin
}
metricName := sr.MetricBlockRef.MetricName
brs := m[string(metricName)]
brs = append(brs, blockRef{
if brs == nil {
brs = &blockRefs{}
}
brs.brs = append(brs.brs, blockRef{
partRef: br.PartRef(),
addr: addr,
})
if len(brs) > 1 {
m[string(metricName)] = brs
} else {
if len(brs.brs) == 1 {
// An optimization for big number of time series with long metricName values:
// use only a single copy of metricName for both orderedMetricNames and m.
orderedMetricNames = append(orderedMetricNames, string(metricName))
@@ -1075,7 +1079,7 @@ func ProcessSearchQuery(qt *querytracer.Tracer, sq *storage.SearchQuery, deadlin
for i, metricName := range orderedMetricNames {
pts[i] = packedTimeseries{
metricName: metricName,
brs: m[metricName],
brs: m[metricName].brs,
}
}
rss.packedTimeseries = pts

View File

@@ -488,6 +488,12 @@ func execBinaryOpArgs(qt *querytracer.Tracer, ec *EvalConfig, exprFirst, exprSec
if err != nil {
return nil, nil, err
}
if len(tssFirst) == 0 && strings.ToLower(be.Op) != "or" {
// Fast path: there is no sense in executing the exprSecond when exprFirst returns an empty result,
// since the "exprFirst op exprSecond" would return an empty result in any case.
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3349
return nil, nil, nil
}
lfs := getCommonLabelFilters(tssFirst)
lfs = metricsql.TrimFiltersByGroupModifier(lfs, be)
exprSecond = metricsql.PushdownBinaryOpFilters(exprSecond, lfs)

View File

@@ -2281,6 +2281,16 @@ func TestExecSuccess(t *testing.T) {
resultExpected := []netstorage.Result{r}
f(q, resultExpected)
})
t.Run(`limit_offset(too-big-offset)`, func(t *testing.T) {
t.Parallel()
q := `limit_offset(1, 10, sort_by_label((
label_set(time()*1, "foo", "y"),
label_set(time()*2, "foo", "a"),
label_set(time()*3, "foo", "x"),
), "foo"))`
resultExpected := []netstorage.Result{}
f(q, resultExpected)
})
t.Run(`limit_offset NaN`, func(t *testing.T) {
t.Parallel()
// q returns 3 time series, where foo=3 contains only NaN values
@@ -6380,19 +6390,39 @@ func TestExecSuccess(t *testing.T) {
q := `range_quantile(0.5, time())`
r := netstorage.Result{
MetricName: metricNameExpected,
// time() results in [1000 1200 1400 1600 1800 2000]
Values: []float64{1500, 1500, 1500, 1500, 1500, 1500},
Timestamps: timestampsExpected,
}
resultExpected := []netstorage.Result{r}
f(q, resultExpected)
})
t.Run(`range_stddev()`, func(t *testing.T) {
t.Parallel()
q := `round(range_stddev(time()),0.01)`
r := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{341.57, 341.57, 341.57, 341.57, 341.57, 341.57},
Timestamps: timestampsExpected,
}
resultExpected := []netstorage.Result{r}
f(q, resultExpected)
})
t.Run(`range_stdvar()`, func(t *testing.T) {
t.Parallel()
q := `round(range_stdvar(time()),0.01)`
r := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{116666.67, 116666.67, 116666.67, 116666.67, 116666.67, 116666.67},
Timestamps: timestampsExpected,
}
resultExpected := []netstorage.Result{r}
f(q, resultExpected)
})
t.Run(`range_median()`, func(t *testing.T) {
t.Parallel()
q := `range_median(time())`
r := netstorage.Result{
MetricName: metricNameExpected,
// time() results in [1000 1200 1400 1600 1800 2000]
Values: []float64{1500, 1500, 1500, 1500, 1500, 1500},
Timestamps: timestampsExpected,
}
@@ -6872,6 +6902,23 @@ func TestExecSuccess(t *testing.T) {
resultExpected := []netstorage.Result{r}
f(q, resultExpected)
})
t.Run(`range_normalize(time(),alias(-time(),"negative"))`, func(t *testing.T) {
t.Parallel()
q := `range_normalize(time(),alias(-time(), "negative"))`
r1 := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{0, 0.2, 0.4, 0.6, 0.8, 1},
Timestamps: timestampsExpected,
}
r2 := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{1, 0.8, 0.6, 0.4, 0.2, 0},
Timestamps: timestampsExpected,
}
r2.MetricName.MetricGroup = []byte("negative")
resultExpected := []netstorage.Result{r1, r2}
f(q, resultExpected)
})
t.Run(`range_first(time())`, func(t *testing.T) {
t.Parallel()
q := `range_first(time())`
@@ -6905,6 +6952,51 @@ func TestExecSuccess(t *testing.T) {
resultExpected := []netstorage.Result{r}
f(q, resultExpected)
})
t.Run(`range_linear_regression(time())`, func(t *testing.T) {
t.Parallel()
q := `range_linear_regression(time())`
r := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{1000, 1200, 1400, 1600, 1800, 2000},
Timestamps: timestampsExpected,
}
resultExpected := []netstorage.Result{r}
f(q, resultExpected)
})
t.Run(`range_linear_regression(-time())`, func(t *testing.T) {
t.Parallel()
q := `range_linear_regression(-time())`
r := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{-1000, -1200, -1400, -1600, -1800, -2000},
Timestamps: timestampsExpected,
}
resultExpected := []netstorage.Result{r}
f(q, resultExpected)
})
t.Run(`range_linear_regression(100/time())`, func(t *testing.T) {
t.Parallel()
q := `sort_desc(round((
alias(range_linear_regression(100/time()), "regress"),
alias(100/time(), "orig"),
),
0.001
))`
r1 := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{0.1, 0.083, 0.071, 0.062, 0.056, 0.05},
Timestamps: timestampsExpected,
}
r1.MetricName.MetricGroup = []byte("orig")
r2 := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{0.095, 0.085, 0.075, 0.066, 0.056, 0.046},
Timestamps: timestampsExpected,
}
r2.MetricName.MetricGroup = []byte("regress")
resultExpected := []netstorage.Result{r1, r2}
f(q, resultExpected)
})
t.Run(`deriv(N)`, func(t *testing.T) {
t.Parallel()
q := `deriv(1000)`
@@ -8034,6 +8126,8 @@ func TestExecError(t *testing.T) {
f(`nonexisting()`)
// Invalid number of args
f(`range_stddev()`)
f(`range_stdvar()`)
f(`range_quantile()`)
f(`range_quantile(1, 2, 3)`)
f(`range_median()`)
@@ -8097,6 +8191,7 @@ func TestExecError(t *testing.T) {
f(`range_sum(1, 2)`)
f(`range_first(1, 2)`)
f(`range_last(1, 2)`)
f(`range_linear_regression(1, 2)`)
f(`smooth_exponential()`)
f(`smooth_exponential(1)`)
f(`remove_resets()`)

View File

@@ -894,7 +894,7 @@ func newRollupPredictLinear(args []interface{}) (rollupFunc, error) {
return nil, err
}
rf := func(rfa *rollupFuncArg) float64 {
v, k := linearRegression(rfa)
v, k := linearRegression(rfa.values, rfa.timestamps, rfa.currTimestamp)
if math.IsNaN(v) {
return nan
}
@@ -904,13 +904,8 @@ func newRollupPredictLinear(args []interface{}) (rollupFunc, error) {
return rf, nil
}
func linearRegression(rfa *rollupFuncArg) (float64, float64) {
// There is no need in handling NaNs here, since they must be cleaned up
// before calling rollup funcs.
values := rfa.values
timestamps := rfa.timestamps
n := float64(len(values))
if n == 0 {
func linearRegression(values []float64, timestamps []int64, interceptTime int64) (float64, float64) {
if len(values) == 0 {
return nan, nan
}
if areConstValues(values) {
@@ -918,25 +913,32 @@ func linearRegression(rfa *rollupFuncArg) (float64, float64) {
}
// See https://en.wikipedia.org/wiki/Simple_linear_regression#Numerical_example
interceptTime := rfa.currTimestamp
vSum := float64(0)
tSum := float64(0)
tvSum := float64(0)
ttSum := float64(0)
n := 0
for i, v := range values {
if math.IsNaN(v) {
continue
}
dt := float64(timestamps[i]-interceptTime) / 1e3
vSum += v
tSum += dt
tvSum += dt * v
ttSum += dt * dt
n++
}
if n == 0 {
return nan, nan
}
k := float64(0)
tDiff := ttSum - tSum*tSum/n
tDiff := ttSum - tSum*tSum/float64(n)
if math.Abs(tDiff) >= 1e-6 {
// Prevent from incorrect division for too small tDiff values.
k = (tvSum - tSum*vSum/n) / tDiff
k = (tvSum - tSum*vSum/float64(n)) / tDiff
}
v := vSum/n - k*tSum/n
v := vSum/float64(n) - k*tSum/float64(n)
return v, k
}
@@ -1473,16 +1475,20 @@ func rollupStaleSamples(rfa *rollupFuncArg) float64 {
}
func rollupStddev(rfa *rollupFuncArg) float64 {
stdvar := rollupStdvar(rfa)
return math.Sqrt(stdvar)
return stddev(rfa.values)
}
func rollupStdvar(rfa *rollupFuncArg) float64 {
// See `Rapid calculation methods` at https://en.wikipedia.org/wiki/Standard_deviation
return stdvar(rfa.values)
}
// There is no need in handling NaNs here, since they must be cleaned up
// before calling rollup funcs.
values := rfa.values
func stddev(values []float64) float64 {
v := stdvar(values)
return math.Sqrt(v)
}
func stdvar(values []float64) float64 {
// See `Rapid calculation methods` at https://en.wikipedia.org/wiki/Standard_deviation
if len(values) == 0 {
return nan
}
@@ -1494,11 +1500,17 @@ func rollupStdvar(rfa *rollupFuncArg) float64 {
var count float64
var q float64
for _, v := range values {
if math.IsNaN(v) {
continue
}
count++
avgNew := avg + (v-avg)/count
q += (v - avg) * (v - avgNew)
avg = avgNew
}
if count == 0 {
return nan
}
return q / count
}
@@ -1605,7 +1617,7 @@ func rollupIdelta(rfa *rollupFuncArg) float64 {
func rollupDerivSlow(rfa *rollupFuncArg) float64 {
// Use linear regression like Prometheus does.
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/73
_, k := linearRegression(rfa)
_, k := linearRegression(rfa.values, rfa.timestamps, rfa.currTimestamp)
return k
}

View File

@@ -388,12 +388,7 @@ func TestRollupPredictLinear(t *testing.T) {
func TestLinearRegression(t *testing.T) {
f := func(values []float64, timestamps []int64, expV, expK float64) {
t.Helper()
rfa := &rollupFuncArg{
values: values,
timestamps: timestamps,
currTimestamp: timestamps[0] + 100,
}
v, k := linearRegression(rfa)
v, k := linearRegression(values, timestamps, timestamps[0]+100)
if err := compareValues([]float64{v}, []float64{expV}); err != nil {
t.Fatalf("unexpected v err: %s", err)
}

View File

@@ -88,9 +88,13 @@ var transformFuncs = map[string]transformFunc{
"range_avg": newTransformFuncRange(runningAvg),
"range_first": transformRangeFirst,
"range_last": transformRangeLast,
"range_linear_regression": transformRangeLinearRegression,
"range_max": newTransformFuncRange(runningMax),
"range_min": newTransformFuncRange(runningMin),
"range_normalize": transformRangeNormalize,
"range_quantile": transformRangeQuantile,
"range_stddev": transformRangeStddev,
"range_stdvar": transformRangeStdvar,
"range_sum": newTransformFuncRange(runningSum),
"remove_resets": transformRemoveResets,
"round": transformRound,
@@ -125,25 +129,29 @@ var transformFuncs = map[string]transformFunc{
// These functions don't change physical meaning of input time series,
// so they don't drop metric name
var transformFuncsKeepMetricName = map[string]bool{
"ceil": true,
"clamp": true,
"clamp_max": true,
"clamp_min": true,
"floor": true,
"interpolate": true,
"keep_last_value": true,
"keep_next_value": true,
"range_avg": true,
"range_first": true,
"range_last": true,
"range_max": true,
"range_min": true,
"range_quantile": true,
"round": true,
"running_avg": true,
"running_max": true,
"running_min": true,
"smooth_exponential": true,
"ceil": true,
"clamp": true,
"clamp_max": true,
"clamp_min": true,
"floor": true,
"interpolate": true,
"keep_last_value": true,
"keep_next_value": true,
"range_avg": true,
"range_first": true,
"range_last": true,
"range_linear_regression": true,
"range_max": true,
"range_min": true,
"range_normalize": true,
"range_quantile": true,
"range_stdvar": true,
"range_sddev": true,
"round": true,
"running_avg": true,
"running_max": true,
"running_min": true,
"smooth_exponential": true,
}
func getTransformFunc(s string) transformFunc {
@@ -1234,6 +1242,91 @@ func newTransformFuncRange(rf func(a, b float64, idx int) float64) transformFunc
}
}
func transformRangeNormalize(tfa *transformFuncArg) ([]*timeseries, error) {
args := tfa.args
var rvs []*timeseries
for _, tss := range args {
for _, ts := range tss {
values := ts.Values
vMin := inf
vMax := -inf
for _, v := range values {
if math.IsNaN(v) {
continue
}
if v < vMin {
vMin = v
}
if v > vMax {
vMax = v
}
}
d := vMax - vMin
if math.IsInf(d, 0) {
continue
}
for i, v := range values {
values[i] = (v - vMin) / d
}
rvs = append(rvs, ts)
}
}
return rvs, nil
}
func transformRangeLinearRegression(tfa *transformFuncArg) ([]*timeseries, error) {
args := tfa.args
if err := expectTransformArgsNum(args, 1); err != nil {
return nil, err
}
rvs := args[0]
for _, ts := range rvs {
values := ts.Values
timestamps := ts.Timestamps
if len(timestamps) == 0 {
continue
}
interceptTimestamp := timestamps[0]
v, k := linearRegression(values, timestamps, interceptTimestamp)
for i, t := range timestamps {
values[i] = v + k*float64(t-interceptTimestamp)/1e3
}
}
return rvs, nil
}
func transformRangeStddev(tfa *transformFuncArg) ([]*timeseries, error) {
args := tfa.args
if err := expectTransformArgsNum(args, 1); err != nil {
return nil, err
}
rvs := args[0]
for _, ts := range rvs {
values := ts.Values
v := stddev(values)
for i := range values {
values[i] = v
}
}
return rvs, nil
}
func transformRangeStdvar(tfa *transformFuncArg) ([]*timeseries, error) {
args := tfa.args
if err := expectTransformArgsNum(args, 1); err != nil {
return nil, err
}
rvs := args[0]
for _, ts := range rvs {
values := ts.Values
v := stdvar(values)
for i := range values {
values[i] = v
}
}
return rvs, nil
}
func transformRangeQuantile(tfa *transformFuncArg) ([]*timeseries, error) {
args := tfa.args
if err := expectTransformArgsNum(args, 2); err != nil {
@@ -1884,6 +1977,8 @@ func transformLimitOffset(tfa *transformFuncArg) ([]*timeseries, error) {
rvs := removeEmptySeries(args[2])
if len(rvs) >= offset {
rvs = rvs[offset:]
} else {
rvs = nil
}
if len(rvs) > limit {
rvs = rvs[:limit]

View File

@@ -1,12 +1,12 @@
{
"files": {
"main.css": "./static/css/main.07bcc4ad.css",
"main.js": "./static/js/main.1293a99e.js",
"static/js/27.939f971b.chunk.js": "./static/js/27.939f971b.chunk.js",
"main.css": "./static/css/main.0937c83d.css",
"main.js": "./static/js/main.e18cda26.js",
"static/js/27.c1ccfd29.chunk.js": "./static/js/27.c1ccfd29.chunk.js",
"index.html": "./index.html"
},
"entrypoints": [
"static/css/main.07bcc4ad.css",
"static/js/main.1293a99e.js"
"static/css/main.0937c83d.css",
"static/js/main.e18cda26.js"
]
}

View File

@@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="./favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="VM-UI is a metric explorer for Victoria Metrics"/><link rel="apple-touch-icon" href="./apple-touch-icon.png"/><link rel="icon" type="image/png" sizes="32x32" href="./favicon-32x32.png"><link rel="manifest" href="./manifest.json"/><title>VM UI</title><link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,300;0,400;0,700;1,300;1,400;1,700&display=swap" rel="stylesheet"><script src="./dashboards/index.js" type="module"></script><script defer="defer" src="./static/js/main.1293a99e.js"></script><link href="./static/css/main.07bcc4ad.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="./favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="VM-UI is a metric explorer for Victoria Metrics"/><link rel="apple-touch-icon" href="./apple-touch-icon.png"/><link rel="icon" type="image/png" sizes="32x32" href="./favicon-32x32.png"><link rel="manifest" href="./manifest.json"/><title>VM UI</title><link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono&family=Lato:wght@300;400;700&display=swap" rel="stylesheet"><script src="./dashboards/index.js" type="module"></script><script defer="defer" src="./static/js/main.e18cda26.js"></script><link href="./static/css/main.0937c83d.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>

View File

@@ -1 +0,0 @@
body{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-family:Lato,sans-serif}code{font-family:source-code-pro,Menlo,Monaco,Consolas,Courier New,monospace}.MuiAccordionSummary-content{margin:0!important}.shortcut-key{align-items:center;border:1px solid #dedede;border-radius:4px;cursor:default;display:inline-flex;font-size:10px;justify-content:center;line-height:22px;padding:2px 6px 0;text-align:center;white-space:nowrap}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}.uplot,.uplot *,.uplot :after,.uplot :before{box-sizing:border-box}.uplot{font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5;width:-webkit-min-content;width:min-content}.u-title{font-size:18px;font-weight:700;text-align:center}.u-wrap{position:relative;-webkit-user-select:none;-ms-user-select:none;user-select:none}.u-over,.u-under{position:absolute}.u-under{overflow:hidden}.uplot canvas{display:block;height:100%;position:relative;width:100%}.u-axis{position:absolute}.u-legend{font-size:14px;margin:auto;text-align:center}.u-inline{display:block}.u-inline *{display:inline-block}.u-inline tr{margin-right:16px}.u-legend th{font-weight:600}.u-legend th>*{display:inline-block;vertical-align:middle}.u-legend .u-marker{background-clip:padding-box!important;height:1em;margin-right:4px;width:1em}.u-inline.u-live th:after{content:":";vertical-align:middle}.u-inline:not(.u-live) .u-value{display:none}.u-series>*{padding:4px}.u-series th{cursor:pointer}.u-legend .u-off>*{opacity:.3}.u-select{background:rgba(0,0,0,.07)}.u-cursor-x,.u-cursor-y,.u-select{pointer-events:none;position:absolute}.u-cursor-x,.u-cursor-y{left:0;top:0;will-change:transform;z-index:100}.u-hz .u-cursor-x,.u-vt .u-cursor-y{border-right:1px dashed #607d8b;height:100%}.u-hz .u-cursor-y,.u-vt .u-cursor-x{border-bottom:1px dashed #607d8b;width:100%}.u-cursor-pt{background-clip:padding-box!important;border:0 solid;border-radius:50%;left:0;pointer-events:none;position:absolute;top:0;will-change:transform;z-index:100}.u-axis.u-off,.u-cursor-pt.u-off,.u-cursor-x.u-off,.u-cursor-y.u-off,.u-select.u-off,.u-tooltip{display:none}.u-tooltip{grid-gap:12px;word-wrap:break-word;background:rgba(57,57,57,.9);border-radius:4px;color:#fff;font-family:monospace;font-size:10px;font-weight:700;line-height:1.4em;max-width:300px;padding:8px;pointer-events:none;position:absolute;z-index:100}.u-tooltip-data{align-items:center;display:flex;flex-wrap:wrap;font-size:11px;line-height:150%}.u-tooltip-data__value{font-weight:700;padding:4px}.u-tooltip__info{grid-gap:4px;display:grid}.u-tooltip__marker{height:12px;margin-right:4px;width:12px}.legendWrapper{cursor:default;display:flex;flex-wrap:wrap;margin-top:20px;position:relative}.legendGroup{margin:0 12px 0 0;padding:10px 6px}.legendGroupTitle{align-items:center;border-bottom:1px solid #ecebe6;display:flex;font-size:11px;margin-bottom:5px;padding:0 10px 5px}.legendGroupQuery{font-weight:700;margin-right:4px}.legendGroupLine{margin-right:10px}.legendItem{grid-gap:6px;align-items:start;background-color:#fff;cursor:pointer;display:grid;grid-template-columns:auto auto;justify-content:start;padding:7px 50px 7px 10px;transition:.2s ease}.legendItemHide{opacity:.5;text-decoration:line-through}.legendItem:hover{background-color:rgba(0,0,0,.1)}.legendMarker{box-sizing:border-box;height:12px;transition:.2s ease;width:12px}.legendLabel{font-size:11px;font-weight:400;line-height:12px}.legendFreeFields{cursor:pointer;padding:3px}.legendFreeFields:hover{text-decoration:underline}.legendFreeFields:not(:last-child):after{content:","}.panelDescription ul{line-height:2.2}.panelDescription a{color:#fff}.panelDescription code{background-color:rgba(0,0,0,.3);border-radius:2px;color:#fff;display:inline;font-size:inherit;font-weight:400;max-width:100%;padding:4px 6px}

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
"use strict";(self.webpackChunkvmui=self.webpackChunkvmui||[]).push([[27],{4027:function(e,t,n){n.r(t),n.d(t,{getCLS:function(){return y},getFCP:function(){return g},getFID:function(){return C},getLCP:function(){return P},getTTFB:function(){return D}});var i,r,a,o,u=function(e,t){return{name:e,value:void 0===t?-1:t,delta:0,entries:[],id:"v2-".concat(Date.now(),"-").concat(Math.floor(8999999999999*Math.random())+1e12)}},c=function(e,t){try{if(PerformanceObserver.supportedEntryTypes.includes(e)){if("first-input"===e&&!("PerformanceEventTiming"in self))return;var n=new PerformanceObserver((function(e){return e.getEntries().map(t)}));return n.observe({type:e,buffered:!0}),n}}catch(e){}},f=function(e,t){var n=function n(i){"pagehide"!==i.type&&"hidden"!==document.visibilityState||(e(i),t&&(removeEventListener("visibilitychange",n,!0),removeEventListener("pagehide",n,!0)))};addEventListener("visibilitychange",n,!0),addEventListener("pagehide",n,!0)},s=function(e){addEventListener("pageshow",(function(t){t.persisted&&e(t)}),!0)},m=function(e,t,n){var i;return function(r){t.value>=0&&(r||n)&&(t.delta=t.value-(i||0),(t.delta||void 0===i)&&(i=t.value,e(t)))}},v=-1,p=function(){return"hidden"===document.visibilityState?0:1/0},d=function(){f((function(e){var t=e.timeStamp;v=t}),!0)},l=function(){return v<0&&(v=p(),d(),s((function(){setTimeout((function(){v=p(),d()}),0)}))),{get firstHiddenTime(){return v}}},g=function(e,t){var n,i=l(),r=u("FCP"),a=function(e){"first-contentful-paint"===e.name&&(f&&f.disconnect(),e.startTime<i.firstHiddenTime&&(r.value=e.startTime,r.entries.push(e),n(!0)))},o=window.performance&&performance.getEntriesByName&&performance.getEntriesByName("first-contentful-paint")[0],f=o?null:c("paint",a);(o||f)&&(n=m(e,r,t),o&&a(o),s((function(i){r=u("FCP"),n=m(e,r,t),requestAnimationFrame((function(){requestAnimationFrame((function(){r.value=performance.now()-i.timeStamp,n(!0)}))}))})))},h=!1,T=-1,y=function(e,t){h||(g((function(e){T=e.value})),h=!0);var n,i=function(t){T>-1&&e(t)},r=u("CLS",0),a=0,o=[],v=function(e){if(!e.hadRecentInput){var t=o[0],i=o[o.length-1];a&&e.startTime-i.startTime<1e3&&e.startTime-t.startTime<5e3?(a+=e.value,o.push(e)):(a=e.value,o=[e]),a>r.value&&(r.value=a,r.entries=o,n())}},p=c("layout-shift",v);p&&(n=m(i,r,t),f((function(){p.takeRecords().map(v),n(!0)})),s((function(){a=0,T=-1,r=u("CLS",0),n=m(i,r,t)})))},E={passive:!0,capture:!0},w=new Date,L=function(e,t){i||(i=t,r=e,a=new Date,F(removeEventListener),S())},S=function(){if(r>=0&&r<a-w){var e={entryType:"first-input",name:i.type,target:i.target,cancelable:i.cancelable,startTime:i.timeStamp,processingStart:i.timeStamp+r};o.forEach((function(t){t(e)})),o=[]}},b=function(e){if(e.cancelable){var t=(e.timeStamp>1e12?new Date:performance.now())-e.timeStamp;"pointerdown"==e.type?function(e,t){var n=function(){L(e,t),r()},i=function(){r()},r=function(){removeEventListener("pointerup",n,E),removeEventListener("pointercancel",i,E)};addEventListener("pointerup",n,E),addEventListener("pointercancel",i,E)}(t,e):L(t,e)}},F=function(e){["mousedown","keydown","touchstart","pointerdown"].forEach((function(t){return e(t,b,E)}))},C=function(e,t){var n,a=l(),v=u("FID"),p=function(e){e.startTime<a.firstHiddenTime&&(v.value=e.processingStart-e.startTime,v.entries.push(e),n(!0))},d=c("first-input",p);n=m(e,v,t),d&&f((function(){d.takeRecords().map(p),d.disconnect()}),!0),d&&s((function(){var a;v=u("FID"),n=m(e,v,t),o=[],r=-1,i=null,F(addEventListener),a=p,o.push(a),S()}))},k={},P=function(e,t){var n,i=l(),r=u("LCP"),a=function(e){var t=e.startTime;t<i.firstHiddenTime&&(r.value=t,r.entries.push(e),n())},o=c("largest-contentful-paint",a);if(o){n=m(e,r,t);var v=function(){k[r.id]||(o.takeRecords().map(a),o.disconnect(),k[r.id]=!0,n(!0))};["keydown","click"].forEach((function(e){addEventListener(e,v,{once:!0,capture:!0})})),f(v,!0),s((function(i){r=u("LCP"),n=m(e,r,t),requestAnimationFrame((function(){requestAnimationFrame((function(){r.value=performance.now()-i.timeStamp,k[r.id]=!0,n(!0)}))}))}))}},D=function(e){var t,n=u("TTFB");t=function(){try{var t=performance.getEntriesByType("navigation")[0]||function(){var e=performance.timing,t={entryType:"navigation",startTime:0};for(var n in e)"navigationStart"!==n&&"toJSON"!==n&&(t[n]=Math.max(e[n]-e.navigationStart,0));return t}();if(n.value=n.delta=t.responseStart,n.value<0||n.value>performance.now())return;n.entries=[t],e(n)}catch(e){}},"complete"===document.readyState?setTimeout(t,0):addEventListener("load",(function(){return setTimeout(t,0)}))}}}]);

View File

@@ -0,0 +1 @@
"use strict";(self.webpackChunkvmui=self.webpackChunkvmui||[]).push([[27],{27:function(e,t,n){n.r(t),n.d(t,{getCLS:function(){return y},getFCP:function(){return g},getFID:function(){return C},getLCP:function(){return P},getTTFB:function(){return D}});var i,r,a,o,u=function(e,t){return{name:e,value:void 0===t?-1:t,delta:0,entries:[],id:"v2-".concat(Date.now(),"-").concat(Math.floor(8999999999999*Math.random())+1e12)}},c=function(e,t){try{if(PerformanceObserver.supportedEntryTypes.includes(e)){if("first-input"===e&&!("PerformanceEventTiming"in self))return;var n=new PerformanceObserver((function(e){return e.getEntries().map(t)}));return n.observe({type:e,buffered:!0}),n}}catch(e){}},f=function(e,t){var n=function n(i){"pagehide"!==i.type&&"hidden"!==document.visibilityState||(e(i),t&&(removeEventListener("visibilitychange",n,!0),removeEventListener("pagehide",n,!0)))};addEventListener("visibilitychange",n,!0),addEventListener("pagehide",n,!0)},s=function(e){addEventListener("pageshow",(function(t){t.persisted&&e(t)}),!0)},m=function(e,t,n){var i;return function(r){t.value>=0&&(r||n)&&(t.delta=t.value-(i||0),(t.delta||void 0===i)&&(i=t.value,e(t)))}},v=-1,p=function(){return"hidden"===document.visibilityState?0:1/0},d=function(){f((function(e){var t=e.timeStamp;v=t}),!0)},l=function(){return v<0&&(v=p(),d(),s((function(){setTimeout((function(){v=p(),d()}),0)}))),{get firstHiddenTime(){return v}}},g=function(e,t){var n,i=l(),r=u("FCP"),a=function(e){"first-contentful-paint"===e.name&&(f&&f.disconnect(),e.startTime<i.firstHiddenTime&&(r.value=e.startTime,r.entries.push(e),n(!0)))},o=window.performance&&performance.getEntriesByName&&performance.getEntriesByName("first-contentful-paint")[0],f=o?null:c("paint",a);(o||f)&&(n=m(e,r,t),o&&a(o),s((function(i){r=u("FCP"),n=m(e,r,t),requestAnimationFrame((function(){requestAnimationFrame((function(){r.value=performance.now()-i.timeStamp,n(!0)}))}))})))},h=!1,T=-1,y=function(e,t){h||(g((function(e){T=e.value})),h=!0);var n,i=function(t){T>-1&&e(t)},r=u("CLS",0),a=0,o=[],v=function(e){if(!e.hadRecentInput){var t=o[0],i=o[o.length-1];a&&e.startTime-i.startTime<1e3&&e.startTime-t.startTime<5e3?(a+=e.value,o.push(e)):(a=e.value,o=[e]),a>r.value&&(r.value=a,r.entries=o,n())}},p=c("layout-shift",v);p&&(n=m(i,r,t),f((function(){p.takeRecords().map(v),n(!0)})),s((function(){a=0,T=-1,r=u("CLS",0),n=m(i,r,t)})))},E={passive:!0,capture:!0},w=new Date,L=function(e,t){i||(i=t,r=e,a=new Date,F(removeEventListener),S())},S=function(){if(r>=0&&r<a-w){var e={entryType:"first-input",name:i.type,target:i.target,cancelable:i.cancelable,startTime:i.timeStamp,processingStart:i.timeStamp+r};o.forEach((function(t){t(e)})),o=[]}},b=function(e){if(e.cancelable){var t=(e.timeStamp>1e12?new Date:performance.now())-e.timeStamp;"pointerdown"==e.type?function(e,t){var n=function(){L(e,t),r()},i=function(){r()},r=function(){removeEventListener("pointerup",n,E),removeEventListener("pointercancel",i,E)};addEventListener("pointerup",n,E),addEventListener("pointercancel",i,E)}(t,e):L(t,e)}},F=function(e){["mousedown","keydown","touchstart","pointerdown"].forEach((function(t){return e(t,b,E)}))},C=function(e,t){var n,a=l(),v=u("FID"),p=function(e){e.startTime<a.firstHiddenTime&&(v.value=e.processingStart-e.startTime,v.entries.push(e),n(!0))},d=c("first-input",p);n=m(e,v,t),d&&f((function(){d.takeRecords().map(p),d.disconnect()}),!0),d&&s((function(){var a;v=u("FID"),n=m(e,v,t),o=[],r=-1,i=null,F(addEventListener),a=p,o.push(a),S()}))},k={},P=function(e,t){var n,i=l(),r=u("LCP"),a=function(e){var t=e.startTime;t<i.firstHiddenTime&&(r.value=t,r.entries.push(e),n())},o=c("largest-contentful-paint",a);if(o){n=m(e,r,t);var v=function(){k[r.id]||(o.takeRecords().map(a),o.disconnect(),k[r.id]=!0,n(!0))};["keydown","click"].forEach((function(e){addEventListener(e,v,{once:!0,capture:!0})})),f(v,!0),s((function(i){r=u("LCP"),n=m(e,r,t),requestAnimationFrame((function(){requestAnimationFrame((function(){r.value=performance.now()-i.timeStamp,k[r.id]=!0,n(!0)}))}))}))}},D=function(e){var t,n=u("TTFB");t=function(){try{var t=performance.getEntriesByType("navigation")[0]||function(){var e=performance.timing,t={entryType:"navigation",startTime:0};for(var n in e)"navigationStart"!==n&&"toJSON"!==n&&(t[n]=Math.max(e[n]-e.navigationStart,0));return t}();if(n.value=n.delta=t.responseStart,n.value<0||n.value>performance.now())return;n.entries=[t],e(n)}catch(e){}},"complete"===document.readyState?setTimeout(t,0):addEventListener("load",(function(){return setTimeout(t,0)}))}}}]);

File diff suppressed because one or more lines are too long

View File

@@ -1,45 +0,0 @@
/**
* React Router DOM v6.3.0
*
* Copyright (c) Remix Software Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE.md file in the root directory of this source tree.
*
* @license MIT
*/
/**
* React Router v6.3.0
*
* Copyright (c) Remix Software Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE.md file in the root directory of this source tree.
*
* @license MIT
*/
/** @license MUI v5.6.1
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/** @license React v16.13.1
* react-is.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/** @license React v17.0.2
* react-is.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,29 @@
/*!
Copyright (c) 2018 Jed Watson.
Licensed under the MIT License (MIT), see
http://jedwatson.github.io/classnames
*/
/*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */
/**
* @remix-run/router v1.0.3
*
* Copyright (c) Remix Software Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE.md file in the root directory of this source tree.
*
* @license MIT
*/
/**
* React Router v6.4.3
*
* Copyright (c) Remix Software Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE.md file in the root directory of this source tree.
*
* @license MIT
*/

View File

@@ -333,12 +333,27 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
case "/delete":
w.Header().Set("Content-Type", "application/json")
snapshotName := r.FormValue("snapshot")
if err := Storage.DeleteSnapshot(snapshotName); err != nil {
err = fmt.Errorf("cannot delete snapshot %q: %w", snapshotName, err)
snapshots, err := Storage.ListSnapshots()
if err != nil {
err = fmt.Errorf("cannot list snapshots: %w", err)
jsonResponseError(w, err)
return true
}
fmt.Fprintf(w, `{"status":"ok"}`)
for _, snName := range snapshots {
if snName == snapshotName {
if err := Storage.DeleteSnapshot(snName); err != nil {
err = fmt.Errorf("cannot delete snapshot %q: %w", snName, err)
jsonResponseError(w, err)
return true
}
fmt.Fprintf(w, `{"status":"ok"}`)
return true
}
}
err = fmt.Errorf("cannot find snapshot %q: %w", snapshotName, err)
jsonResponseError(w, err)
return true
case "/delete_all":
w.Header().Set("Content-Type", "application/json")

View File

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

View File

@@ -1,3 +1,4 @@
// eslint-disable-next-line no-undef
module.exports = {
"env": {
"browser": true,
@@ -10,9 +11,7 @@ module.exports = {
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"ecmaFeatures": { "jsx": true },
"ecmaVersion": 12,
"sourceType": "module"
},
@@ -21,32 +20,15 @@ module.exports = {
"@typescript-eslint"
],
"rules": {
"indent": [
"error",
2,
{ "SwitchCase": 1 }
],
"linebreak-style": [
"error",
"unix"
],
"quotes": [
"error",
"double"
],
"semi": [
"error",
"always"
],
"react/prop-types": 0,
"max-lines": [
"error",
{
"max": 1000,
"skipBlankLines": true,
"skipComments": true,
}
]
"react/jsx-closing-bracket-location": [1, "line-aligned"],
"react/jsx-max-props-per-line":[1, { "maximum": 1 }],
"react/jsx-first-prop-new-line": [1, "multiline"],
"object-curly-spacing": [2, "always"],
"indent": ["error", 2, { "SwitchCase": 1 }],
"linebreak-style": ["error", "unix"],
"quotes": ["error", "double"],
"semi": ["error", "always"],
"react/prop-types": 0
},
"settings": {
"react": {
@@ -56,7 +38,10 @@ module.exports = {
"linkComponents": [
// Components used as alternatives to <a> for linking, eg. <Link to={ url } />
"Hyperlink",
{"name": "Link", "linkAttribute": "to"}
{
"name": "Link", "linkAttribute": "to"
}
]
}
};

View File

@@ -56,18 +56,18 @@ VMUI can be used to paste into other applications
#### Options (JSON):
| Name | Default | Description |
|:------------------------|:--------------:|--------------------------------------------------------------------------------------:|
| serverURL | domain name | Can't be changed from the UI |
| inputTenantID | - | If the flag is present, the "Tenant ID" field is displayed |
| headerStyles.background | `#FFFFFF` | Header background color |
| headerStyles.color | `primary.main` | Header font color |
| palette.primary | `#3F51B5` | used to represent primary interface elements for a user |
| palette.secondary | `#F50057` | used to represent secondary interface elements for a user |
| palette.error | `#FF4141` | used to represent interface elements that the user should be made aware of |
| palette.warning | `#FF9800` | used to represent potentially dangerous actions or important messages |
| palette.success | `#4CAF50` | used to indicate the successful completion of an action that user triggered |
| palette.info | `#03A9F4` | used to present information to the user that is neutral and not necessarily important |
| Name | Default | Description |
|:------------------------|:-----------:|--------------------------------------------------------------------------------------:|
| serverURL | domain name | Can't be changed from the UI |
| inputTenantID | - | If the flag is present, the "Tenant ID" field is displayed |
| headerStyles.background | `#FFFFFF` | Header background color |
| headerStyles.color | `#3F51B5` | Header font color |
| palette.primary | `#3F51B5` | used to represent primary interface elements for a user |
| palette.secondary | `#F50057` | used to represent secondary interface elements for a user |
| palette.error | `#FF4141` | used to represent interface elements that the user should be made aware of |
| palette.warning | `#FF9800` | used to represent potentially dangerous actions or important messages |
| palette.success | `#4CAF50` | used to indicate the successful completion of an action that user triggered |
| palette.info | `#03A9F4` | used to present information to the user that is neutral and not necessarily important |
#### JSON example:
```json
@@ -75,8 +75,8 @@ VMUI can be used to paste into other applications
"serverURL": "http://localhost:8428",
"inputTenantID": "true",
"headerStyles": {
"background": "#fff",
"color": "primary.main"
"background": "#FFFFFF",
"color": "#538DE8"
},
"palette": {
"primary": "#538DE8",
@@ -92,7 +92,7 @@ VMUI can be used to paste into other applications
#### HTML example:
```html
<div id="root" data-params='{"serverURL":"http://localhost:8428","inputTenantID":"true","headerStyles":{"background":"#fff","color":"primary.main"},"palette":{"primary":"#538DE8","secondary":"#F76F8E","error":"#FD151B","warning":"#FFB30F","success":"#7BE622","info":"#0F5BFF"}}'></div>
<div id="root" data-params='{"serverURL":"http://localhost:8428","inputTenantID":"true","headerStyles":{"background":"#FFFFFF","color":"#538DE8"},"palette":{"primary":"#538DE8","secondary":"#F76F8E","error":"#FD151B","warning":"#FFB30F","success":"#7BE622","info":"#0F5BFF"}}'></div>
```

View File

@@ -0,0 +1,98 @@
# Test cases
----
**Name:** Force execution of a queries
**Steps:**
1. click to button `Execute query`
2. click to icon `Refresh dashboard`
3. press `enter` on the query field
**Expected Result:**
For each step sends a request and render new data
----
**Name:** Time Range with auto refresh
**Steps:**
1. Set absolute time range
2. Enable auto refresh
3. Change delay auto refresh
4. Disable auto refresh
**Expected Result:**
Time range has not changed
----
**Name:** Query history
**Steps:**
1. Run query one by one: `1`, `2`, `3`
2. Press `Ctrl + ArrowUp`/`Ctrl + ArrowDown` when the query field focus
**Expected Result:**
Query value changes according to execution order (Preserve execution order).
<br/>
`Ctrl + ArrowUp` - set prev value, `Ctrl + ArrowDown` - set next value
----
**Name:** Absolute time range fields
**Steps:**
1. Open `Time range controls`
2. Change `From` or `Until` time value
3. Click to `Apply`
**Expected Result:**
When you change one of the fields, the second does not change
----
**Name:** Auto update after query delete
**Steps:**
1. Add multiple query
2. Execute queries
3. Delete one of the queries
**Expected Result:**
Graph is automatically updated after the query delete
----
**Name:** Query URL params
**Steps:**
1. [Open graph](http://localhost:3000/?g0.range_input=1d&g0.end_input=2022-10-26T14%3A00%3A00&g0.step_input=180&g0.relative_time=none&g0.tab=chart&g0.expr=1&g1.range_input=1d&g1.end_input=2022-10-26T14%3A00%3A00&g1.step_input=180&g1.relative_time=none&g1.tab=chart&g1.expr=2#/) with params:
> ?g0.range_input=1d&g0.end_input=2022-10-26T14%3A00%3A00&g0.step_input=180&g0.relative_time=none&g0.tab=chart&g0.expr=1&g1.range_input=1d&g1.end_input=2022-10-26T14%3A00%3A00&g1.step_input=180&g1.relative_time=none&g1.tab=chart&g1.expr=2#/
**Expected Result:**
Executed two query with params:
```
query: 1 and 2
start: 1666706400
end: 1666792800
step: from "Step value" field (depends on screen width)
```
- Display two queries: `1` and `2`
- Time range from `2022-10-25 16:00:00` to `2022-10-26 16:00:00` (:warning: by UTC +2)
- Display tab `Table`
----
**Name:** Prometheus query URL params
**Steps:**
1. [Open graph](http://localhost:3000/?g0.expr=node_arp_entries&g0.tab=1&g0.stacked=0&g0.range_input=30m&g0.end_input=2021-09-11%2000%3A00%3A00&g0.moment_input=2021-09-11%2000%3A00%3A00&g0.step_input=6&g1.expr=node_cpu_guest_seconds_total&g1.tab=1&g1.stacked=0&g1.range_input=30m&g1.end_input=2022-12-01%2014%3A00%3A00&g1.moment_input=2022-12-01%2014%3A00%3A00&g1.step_input=6) with params:
> ?g0.expr=node_arp_entries&g0.tab=1&g0.stacked=0&g0.range_input=30m&g0.end_input=2021-09-11%2000%3A00%3A00&g0.moment_input=2021-09-11%2000%3A00%3A00&g0.step_input=6&g1.expr=node_cpu_guest_seconds_total&g1.tab=1&g1.stacked=0&g1.range_input=30m&g1.end_input=2022-12-01%2014%3A00%3A00&g1.moment_input=2022-12-01%2014%3A00%3A00&g1.step_input=6
**Expected Result:**
- Display two queries: `node_arp_entries` and `node_cpu_guest_seconds_total`
- Time range from `2021-09-11 01:30:00` to `2021-09-11 02:00:00` (:warning: by UTC +2)
- Display tab `Table`
----

File diff suppressed because it is too large Load Diff

View File

@@ -4,11 +4,6 @@
"private": true,
"homepage": "./",
"dependencies": {
"@date-io/dayjs": "^2.13.1",
"@emotion/styled": "^11.8.1",
"@mui/icons-material": "^5.6.0",
"@mui/lab": "^5.0.0-alpha.73",
"@mui/material": "^5.5.1",
"@testing-library/jest-dom": "^5.16.2",
"@testing-library/react": "^13.0.0",
"@testing-library/user-event": "^14.0.4",
@@ -21,6 +16,7 @@
"@types/qs": "^6.9.7",
"@types/react-router-dom": "^5.3.3",
"@types/webpack-env": "^1.16.3",
"classnames": "^2.3.2",
"dayjs": "^1.11.0",
"lodash.debounce": "^4.0.8",
"lodash.get": "^4.4.2",
@@ -29,6 +25,7 @@
"preact": "^10.7.1",
"qs": "^6.10.3",
"react-router-dom": "^6.3.0",
"sass": "^1.56.0",
"typescript": "~4.6.2",
"uplot": "^1.6.19",
"web-vitals": "^2.1.4"

View File

@@ -28,7 +28,7 @@
<title>VM UI</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,300;0,400;0,700;1,300;1,400;1,700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono&family=Lato:wght@300;400;700&display=swap" rel="stylesheet">
<script src="%PUBLIC_URL%/dashboards/index.js" type="module"></script>
</head>
<body>

View File

@@ -1,5 +1,5 @@
import React from "preact/compat";
import {render, screen} from "@testing-library/react";
import { render, screen } from "@testing-library/react";
import App from "./App";
test("renders header", () => {

View File

@@ -1,56 +1,58 @@
import React, {FC} from "preact/compat";
import {HashRouter, Route, Routes} from "react-router-dom";
import {SnackbarProvider} from "./contexts/Snackbar";
import {StateProvider} from "./state/common/StateContext";
import {AuthStateProvider} from "./state/auth/AuthStateContext";
import {GraphStateProvider} from "./state/graph/GraphStateContext";
import {CardinalityStateProvider} from "./state/cardinality/CardinalityStateContext";
import {TopQueriesStateProvider} from "./state/topQueries/TopQueriesStateContext";
import THEME from "./theme/theme";
import { ThemeProvider, StyledEngineProvider } from "@mui/material/styles";
import CssBaseline from "@mui/material/CssBaseline";
import LocalizationProvider from "@mui/lab/LocalizationProvider";
import DayjsUtils from "@date-io/dayjs";
import router from "./router/index";
import CustomPanel from "./components/CustomPanel/CustomPanel";
import React, { FC, useState } from "preact/compat";
import { HashRouter, Route, Routes } from "react-router-dom";
import router from "./router";
import AppContextProvider from "./contexts/AppContextProvider";
import HomeLayout from "./components/Home/HomeLayout";
import DashboardsLayout from "./components/PredefinedPanels/DashboardsLayout";
import CardinalityPanel from "./components/CardinalityPanel/CardinalityPanel";
import TopQueries from "./components/TopQueries/TopQueries";
import CustomPanel from "./pages/CustomPanel";
import DashboardsLayout from "./pages/PredefinedPanels";
import CardinalityPanel from "./pages/CardinalityPanel";
import TopQueries from "./pages/TopQueries";
import ThemeProvider from "./components/Main/ThemeProvider/ThemeProvider";
import Spinner from "./components/Main/Spinner/Spinner";
import TracePage from "./pages/TracePage";
const App: FC = () => {
const [loadingTheme, setLoadingTheme] = useState(true);
if (loadingTheme) return (
<>
<Spinner/>
<ThemeProvider setLoadingTheme={setLoadingTheme}/>;
</>
);
return <>
<HashRouter>
<CssBaseline /> {/* CSS Baseline: kind of normalize.css made by materialUI team - can be scoped */}
<LocalizationProvider dateAdapter={DayjsUtils}> {/* Allows datepicker to work with DayJS */}
<StyledEngineProvider injectFirst>
<ThemeProvider theme={THEME}> {/* Material UI theme customization */}
<StateProvider> {/* Serialized into query string, common app settings */}
<AuthStateProvider> {/* Auth related info - optionally persisted to Local Storage */}
<GraphStateProvider> {/* Graph settings */}
<CardinalityStateProvider> {/* Cardinality settings */}
<TopQueriesStateProvider> {/* Top Queries settings */}
<SnackbarProvider> {/* Display various snackbars */}
<Routes>
<Route path={"/"} element={<HomeLayout/>}>
<Route path={router.home} element={<CustomPanel/>}/>
<Route path={router.dashboards} element={<DashboardsLayout/>}/>
<Route path={router.cardinality} element={<CardinalityPanel/>} />
<Route path={router.topQueries} element={<TopQueries/>} />
</Route>
</Routes>
</SnackbarProvider>
</TopQueriesStateProvider>
</CardinalityStateProvider>
</GraphStateProvider>
</AuthStateProvider>
</StateProvider>
</ThemeProvider>
</StyledEngineProvider>
</LocalizationProvider>
<AppContextProvider>
<Routes>
<Route
path={"/"}
element={<HomeLayout/>}
>
<Route
path={router.home}
element={<CustomPanel/>}
/>
<Route
path={router.dashboards}
element={<DashboardsLayout/>}
/>
<Route
path={router.cardinality}
element={<CardinalityPanel/>}
/>
<Route
path={router.topQueries}
element={<TopQueries/>}
/>
<Route
path={router.trace}
element={<TracePage/>}
/>
</Route>
</Routes>
</AppContextProvider>
</HashRouter>
</>;
};

View File

@@ -1,4 +1,4 @@
import {TimeParams} from "../types";
import { TimeParams } from "../types";
export const getQueryRangeUrl = (server: string, query: string, period: TimeParams, nocache: boolean, queryTracing: boolean): string =>
`${server}/api/v1/query_range?query=${encodeURIComponent(query)}&start=${period.start}&end=${period.end}&step=${period.step}${nocache ? "&nocache=1" : ""}${queryTracing ? "&trace=1" : ""}`;

View File

@@ -1,27 +0,0 @@
import React from "preact/compat";
import { styled } from "@mui/material/styles";
import LinearProgressWithLabel, {linearProgressClasses, LinearProgressProps} from "@mui/material/LinearProgress";
import {Box, Typography} from "@mui/material";
export const BorderLinearProgress = styled(LinearProgressWithLabel)(({ theme }) => ({
height: 20,
borderRadius: 5,
[`&.${linearProgressClasses.colorPrimary}`]: {
backgroundColor: theme.palette.grey[theme.palette.mode === "light" ? 200 : 800],
},
[`& .${linearProgressClasses.bar}`]: {
borderRadius: 5,
backgroundColor: theme.palette.mode === "light" ? "#1a90ff" : "#308fe8",
},
}));
export const BorderLinearProgressWithLabel = (props: LinearProgressProps & { value: number }) => (
<Box sx={{ display: "flex", alignItems: "center" }}>
<Box sx={{ width: "100%", mr: 1 }}>
<BorderLinearProgress variant="determinate" {...props} />
</Box>
<Box sx={{ minWidth: 35 }}>
<Typography variant="body2" color="text.secondary">{`${props.value.toFixed(2)}%`}</Typography>
</Box>
</Box>
);

View File

@@ -1,103 +0,0 @@
import React, {ChangeEvent, FC} from "react";
import Box from "@mui/material/Box";
import QueryEditor from "../../CustomPanel/Configurator/Query/QueryEditor";
import Tooltip from "@mui/material/Tooltip";
import IconButton from "@mui/material/IconButton";
import PlayCircleOutlineIcon from "@mui/icons-material/PlayCircleOutline";
import {useFetchQueryOptions} from "../../../hooks/useFetchQueryOptions";
import {useAppDispatch, useAppState} from "../../../state/common/StateContext";
import FormControlLabel from "@mui/material/FormControlLabel";
import BasicSwitch from "../../../theme/switch";
import {saveToStorage} from "../../../utils/storage";
import TextField from "@mui/material/TextField";
import {ErrorTypes} from "../../../types";
export interface CardinalityConfiguratorProps {
onSetHistory: (step: number, index: number) => void;
onSetQuery: (query: string, index: number) => void;
onRunQuery: () => void;
onTopNChange: (e: ChangeEvent<HTMLTextAreaElement|HTMLInputElement>) => void;
onFocusLabelChange: (e: ChangeEvent<HTMLTextAreaElement|HTMLInputElement>) => void;
query: string;
topN: number;
error?: ErrorTypes | string;
totalSeries: number;
totalLabelValuePairs: number;
date: string | null;
match: string | null;
focusLabel: string | null;
}
const CardinalityConfigurator: FC<CardinalityConfiguratorProps> = ({
topN,
error,
query,
onSetHistory,
onRunQuery,
onSetQuery,
onTopNChange,
onFocusLabelChange,
totalSeries,
totalLabelValuePairs,
date,
match,
focusLabel
}) => {
const dispatch = useAppDispatch();
const {queryControls: {autocomplete}} = useAppState();
const {queryOptions} = useFetchQueryOptions();
const onChangeAutocomplete = () => {
dispatch({type: "TOGGLE_AUTOCOMPLETE"});
saveToStorage("AUTOCOMPLETE", !autocomplete);
};
return <Box boxShadow="rgba(99, 99, 99, 0.2) 0px 2px 8px 0px;" p={4} pb={2} mb={2}>
<Box>
<Box display="grid" gridTemplateColumns="1fr auto auto auto auto" gap="4px" width="100%" mb={4}>
<QueryEditor
query={query} index={0} autocomplete={autocomplete} queryOptions={queryOptions}
error={error} setHistoryIndex={onSetHistory} runQuery={onRunQuery} setQuery={onSetQuery}
label={"Time series selector"}
/>
<Box mr={2}>
<TextField
label="Number of entries per table"
type="number"
size="medium"
variant="outlined"
value={topN}
error={topN < 1}
helperText={topN < 1 ? "Number must be bigger than zero" : " "}
onChange={onTopNChange}/>
</Box>
<Box mr={2}>
<TextField
label="Focus label"
type="text"
size="medium"
variant="outlined"
value={focusLabel}
onChange={onFocusLabelChange} />
</Box>
<Box>
<FormControlLabel label="Autocomplete"
control={<BasicSwitch checked={autocomplete} onChange={onChangeAutocomplete}/>}
/>
</Box>
<Tooltip title="Execute Query">
<IconButton onClick={onRunQuery} sx={{height: "49px", width: "49px"}}>
<PlayCircleOutlineIcon/>
</IconButton>
</Tooltip>
</Box>
</Box>
<Box>
Analyzed <b>{totalSeries}</b> series with <b>{totalLabelValuePairs}</b> &quot;label=value&quot; pairs
at <b>{date}</b> {match && <span>for series selector <b>{match}</b></span>}.
Show top {topN} entries per table.
</Box>
</Box>;
};
export default CardinalityConfigurator;

View File

@@ -1,119 +0,0 @@
import React, {ChangeEvent, FC, useState} from "react";
import {SyntheticEvent} from "react";
import {Alert} from "@mui/material";
import {useFetchQuery} from "../../hooks/useCardinalityFetch";
import {queryUpdater} from "./helpers";
import {Data} from "../Table/types";
import CardinalityConfigurator from "./CardinalityConfigurator/CardinalityConfigurator";
import Spinner from "../common/Spinner";
import {useCardinalityDispatch, useCardinalityState} from "../../state/cardinality/CardinalityStateContext";
import MetricsContent from "./MetricsContent/MetricsContent";
import {DefaultActiveTab, Tabs, TSDBStatus, Containers} from "./types";
const spinnerContainerStyles = (height: string) => {
return {
width: "100%",
maxWidth: "100%",
position: "absolute",
height: height ?? "50%",
background: "rgba(255, 255, 255, 0.7)",
pointerEvents: "none",
zIndex: 1000,
};
};
const CardinalityPanel: FC = () => {
const cardinalityDispatch = useCardinalityDispatch();
const {topN, match, date, focusLabel} = useCardinalityState();
const configError = "";
const [query, setQuery] = useState(match || "");
const [queryHistoryIndex, setQueryHistoryIndex] = useState(0);
const [queryHistory, setQueryHistory] = useState<string[]>([]);
const onRunQuery = () => {
setQueryHistory(prev => [...prev, query]);
setQueryHistoryIndex(prev => prev + 1);
cardinalityDispatch({type: "SET_MATCH", payload: query});
cardinalityDispatch({type: "RUN_QUERY"});
};
const onSetQuery = (query: string) => {
setQuery(query);
};
const onSetHistory = (step: number) => {
const newIndexHistory = queryHistoryIndex + step;
if (newIndexHistory < 0 || newIndexHistory >= queryHistory.length) return;
setQueryHistoryIndex(newIndexHistory);
setQuery(queryHistory[newIndexHistory]);
};
const onTopNChange = (e: ChangeEvent<HTMLTextAreaElement|HTMLInputElement>) => {
cardinalityDispatch({type: "SET_TOP_N", payload: +e.target.value});
};
const onFocusLabelChange = (e: ChangeEvent<HTMLTextAreaElement|HTMLInputElement>) => {
cardinalityDispatch({type: "SET_FOCUS_LABEL", payload: e.target.value});
};
const {isLoading, appConfigurator, error} = useFetchQuery();
const [stateTabs, setTab] = useState(appConfigurator.defaultState.defaultActiveTab);
const {tsdbStatusData, defaultState, tablesHeaders} = appConfigurator;
const handleTabChange = (e: SyntheticEvent, newValue: number) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
setTab({...stateTabs, [e.target.id]: newValue});
};
const handleFilterClick = (key: string) => (e: SyntheticEvent) => {
const name = e.currentTarget.id;
const query = queryUpdater[key](focusLabel, name);
setQuery(query);
setQueryHistory(prev => [...prev, query]);
setQueryHistoryIndex(prev => prev + 1);
cardinalityDispatch({type: "SET_MATCH", payload: query});
let newFocusLabel = "";
if (key === "labelValueCountByLabelName" || key == "seriesCountByLabelName") {
newFocusLabel = name;
}
cardinalityDispatch({type: "SET_FOCUS_LABEL", payload: newFocusLabel});
cardinalityDispatch({type: "RUN_QUERY"});
};
return (
<>
{isLoading && <Spinner
isLoading={isLoading}
height={"800px"}
containerStyles={spinnerContainerStyles("100%")}
title={<Alert color="error" severity="error" sx={{whiteSpace: "pre-wrap", mt: 2}}>
Please wait while cardinality stats is calculated. This may take some time if the db contains big number of time series
</Alert>}
/>}
<CardinalityConfigurator error={configError} query={query} onRunQuery={onRunQuery} onSetQuery={onSetQuery}
onSetHistory={onSetHistory} onTopNChange={onTopNChange} topN={topN} date={date} match={match}
totalSeries={tsdbStatusData.totalSeries} totalLabelValuePairs={tsdbStatusData.totalLabelValuePairs}
focusLabel={focusLabel} onFocusLabelChange={onFocusLabelChange}
/>
{error && <Alert color="error" severity="error" sx={{whiteSpace: "pre-wrap", mt: 2}}>{error}</Alert>}
{appConfigurator.keys(focusLabel).map((keyName) => (
<MetricsContent
key={keyName}
sectionTitle={appConfigurator.sectionsTitles(focusLabel)[keyName]}
activeTab={stateTabs[keyName as keyof DefaultActiveTab]}
rows={tsdbStatusData[keyName as keyof TSDBStatus] as unknown as Data[]}
onChange={handleTabChange}
onActionClick={handleFilterClick(keyName)}
tabs={defaultState.tabs[keyName as keyof Tabs]}
chartContainer={defaultState.containerRefs[keyName as keyof Containers<HTMLDivElement>]}
totalSeries={appConfigurator.totalSeries(keyName)}
tabId={keyName}
tableHeaderCells={tablesHeaders[keyName]}
/>
))}
</>
);
};
export default CardinalityPanel;

View File

@@ -1,96 +0,0 @@
import {FC} from "react";
import {Box, Grid, Tab, Tabs, Typography} from "@mui/material";
import TableChartIcon from "@mui/icons-material/TableChart";
import ShowChartIcon from "@mui/icons-material/ShowChart";
import TabPanel from "../../TabPanel/TabPanel";
import EnhancedTable from "../../Table/Table";
import TableCells from "../TableCells/TableCells";
import BarChart from "../../BarChart/BarChart";
import {barOptions} from "../../BarChart/consts";
import React, {SyntheticEvent} from "react";
import {Data, HeadCell} from "../../Table/types";
import {MutableRef} from "preact/hooks";
interface MetricsProperties {
rows: Data[];
activeTab: number;
onChange: (e: SyntheticEvent, newValue: number) => void;
onActionClick: (e: SyntheticEvent) => void;
tabs: string[];
chartContainer: MutableRef<HTMLDivElement> | undefined;
totalSeries: number,
tabId: string;
sectionTitle: string;
tableHeaderCells: HeadCell[];
}
const MetricsContent: FC<MetricsProperties> = ({
rows,
activeTab,
onChange,
tabs,
chartContainer,
totalSeries,
tabId,
onActionClick,
sectionTitle,
tableHeaderCells,
}) => {
const tableCells = (row: Data) => (
<TableCells
row={row}
totalSeries={totalSeries}
onActionClick={onActionClick}
/>
);
return (
<>
<Grid container spacing={2} sx={{px: 2}}>
<Grid item xs={12} md={12} lg={12}>
<Typography gutterBottom variant="h5" component="h5">{sectionTitle}</Typography>
<Box sx={{ borderBottom: 1, borderColor: "divider" }}>
<Tabs
value={activeTab}
onChange={onChange} aria-label="basic tabs example">
{tabs.map((title: string, i: number) =>
<Tab
key={title}
label={title}
aria-controls={`tabpanel-${i}`}
id={tabId}
iconPosition={"start"}
icon={ i === 0 ? <TableChartIcon /> : <ShowChartIcon /> } />
)}
</Tabs>
</Box>
{tabs.map((_,idx) =>
<div
ref={chartContainer}
style={{width: "100%", paddingRight: idx !== 0 ? "40px" : 0 }} key={`chart-${idx}`}>
<TabPanel value={activeTab} index={idx}>
{activeTab === 0 ? <EnhancedTable
rows={rows}
headerCells={tableHeaderCells}
defaultSortColumn={"value"}
tableCells={tableCells}
/>: <BarChart
data={[
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
rows.map((v) => v.name),
rows.map((v) => v.value),
rows.map((_, i) => i % 12 == 0 ? 1 : i % 10 == 0 ? 2 : 0),
]}
container={chartContainer?.current || null}
configs={barOptions}
/>}
</TabPanel>
</div>
)}
</Grid>
</Grid>
</>
);
};
export default MetricsContent;

View File

@@ -1,39 +0,0 @@
import {SyntheticEvent} from "react";
import React, {FC} from "preact/compat";
import {TableCell, ButtonGroup} from "@mui/material";
import {Data} from "../../Table/types";
import {BorderLinearProgressWithLabel} from "../../BorderLineProgress/BorderLinearProgress";
import IconButton from "@mui/material/IconButton";
import PlayCircleOutlineIcon from "@mui/icons-material/PlayCircleOutline";
import Tooltip from "@mui/material/Tooltip";
interface CardinalityTableCells {
row: Data,
totalSeries: number;
onActionClick: (e: SyntheticEvent) => void;
}
const TableCells: FC<CardinalityTableCells> = ({ row, totalSeries, onActionClick }) => {
const progress = totalSeries > 0 ? row.value / totalSeries * 100 : -1;
return <>
<TableCell key={row.name}>{row.name}</TableCell>
<TableCell key={row.value}>{row.value}</TableCell>
{progress > 0 ? <TableCell key={row.progressValue}>
<BorderLinearProgressWithLabel variant="determinate" value={progress} />
</TableCell> : null}
<TableCell key={"action"}>
<ButtonGroup variant="contained">
<Tooltip title={`Filter by ${row.name}`}>
<IconButton
id={row.name}
onClick={onActionClick}
sx={{height: "20px", width: "20px"}}>
<PlayCircleOutlineIcon/>
</IconButton>
</Tooltip>
</ButtonGroup>
</TableCell>
</>;
};
export default TableCells;

View File

@@ -1,15 +1,15 @@
import React, {FC, useEffect, useRef, useState} from "preact/compat";
import uPlot, {Options as uPlotOptions} from "uplot";
import useResize from "../../hooks/useResize";
import {BarChartProps} from "./types";
import React, { FC, useEffect, useRef, useState } from "preact/compat";
import uPlot, { Options as uPlotOptions } from "uplot";
import useResize from "../../../hooks/useResize";
import { BarChartProps } from "./types";
import "./style.scss";
const BarChart: FC<BarChartProps> = ({
data,
container,
configs}) => {
configs }) => {
const uPlotRef = useRef<HTMLDivElement>(null);
const [isPanning] = useState(false);
const [uPlotInst, setUPlotInst] = useState<uPlot>();
const layoutSize = useResize(container);
@@ -21,7 +21,6 @@ const BarChart: FC<BarChartProps> = ({
const updateChart = (): void => {
if (!uPlotInst) return;
uPlotInst.setData(data);
if (!isPanning) uPlotInst.redraw();
};
useEffect(() => {
@@ -33,7 +32,7 @@ const BarChart: FC<BarChartProps> = ({
useEffect(() => updateChart(), [data]);
return <div style={{pointerEvents: isPanning ? "none" : "auto", height: "100%"}}>
return <div style={{ height: "100%" }}>
<div ref={uPlotRef}/>
</div>;
};

View File

@@ -1,7 +1,7 @@
import {seriesBarsPlugin} from "../../utils/uplot/plugin";
import {barDisp, getBarSeries} from "../../utils/uplot/series";
import {Fill, Stroke} from "../../utils/uplot/types";
import {PaddingSide, Series} from "uplot";
import { seriesBarsPlugin } from "../../../utils/uplot/plugin";
import { barDisp, getBarSeries } from "../../../utils/uplot/series";
import { Fill, Stroke } from "../../../utils/uplot/types";
import { PaddingSide, Series } from "uplot";
const stroke: Stroke = {
@@ -36,14 +36,14 @@ export const barOptions = {
const idxs = u.legend.idxs || [];
if (u.data === null || idxs.length === 0)
return {"Name": null, "Value": null,};
return { "Name": null, "Value": null, };
const dataIdx = idxs[seriesIdx] || 0;
const build = u.data[0][dataIdx];
const duration = u.data[seriesIdx][dataIdx];
return {"Name": build, "Value": duration};
return { "Name": build, "Value": duration };
}
},
] as Series[],

View File

@@ -0,0 +1,36 @@
@use "src/styles/variables" as *;
.u-legend {
font-family: $font-family-global;
font-size: $font-size-medium;
color: $color-text;
.u-thead {
display: none;
}
.u-series {
display: flex;
gap: $padding-small;
th {
display: none;
}
td {
&:nth-child(2) {
&:after {
content: ':';
margin-left: $padding-small;
}
}
}
.u-value {
display: block;
padding: 0;
text-align: left;
}
}
}

View File

@@ -1,4 +1,4 @@
import {AlignedData as uPlotData, Options as uPlotOptions} from "uplot";
import { AlignedData as uPlotData, Options as uPlotOptions } from "uplot";
export interface BarChartProps {
data: uPlotData;

View File

@@ -0,0 +1,181 @@
import React, { FC, useEffect, useMemo, useRef, useState } from "preact/compat";
import uPlot, { Series } from "uplot";
import { MetricResult } from "../../../api/types";
import { formatPrettyNumber, getColorLine, getLegendLabel } from "../../../utils/uplot/helpers";
import dayjs from "dayjs";
import { DATE_FULL_TIMEZONE_FORMAT } from "../../../constants/date";
import ReactDOM from "react-dom";
import get from "lodash.get";
import Button from "../../Main/Button/Button";
import { CloseIcon, DragIcon } from "../../Main/Icons";
import classNames from "classnames";
import { MouseEvent as ReactMouseEvent } from "react";
import "./style.scss";
export interface ChartTooltipProps {
id: string,
u: uPlot,
metrics: MetricResult[],
series: Series[],
unit?: string,
isSticky?: boolean,
tooltipOffset: { left: number, top: number },
tooltipIdx: { seriesIdx: number, dataIdx: number },
onClose?: (id: string) => void
}
const ChartTooltip: FC<ChartTooltipProps> = ({
u,
id,
unit = "",
metrics,
series,
tooltipIdx,
tooltipOffset,
isSticky,
onClose
}) => {
const tooltipRef = useRef<HTMLDivElement>(null);
const [position, setPosition] = useState({ top: -999, left: -999 });
const [moving, setMoving] = useState(false);
const [moved, setMoved] = useState(false);
const [seriesIdx, setSeriesIdx] = useState(tooltipIdx.seriesIdx);
const [dataIdx, setDataIdx] = useState(tooltipIdx.dataIdx);
const targetPortal = useMemo(() => u.root.querySelector(".u-wrap"), [u]);
const value = useMemo(() => get(u, ["data", seriesIdx, dataIdx], 0), [u, seriesIdx, dataIdx]);
const valueFormat = useMemo(() => formatPrettyNumber(value), [value]);
const dataTime = useMemo(() => u.data[0][dataIdx], [u, dataIdx]);
const date = useMemo(() => dayjs(new Date(dataTime * 1000)).format(DATE_FULL_TIMEZONE_FORMAT), [dataTime]);
const color = useMemo(() => getColorLine(series[seriesIdx]?.label || ""), [series, seriesIdx]);
const name = useMemo(() => {
const metricName = (series[seriesIdx]?.label || "").replace(/{.+}/gmi, "").trim();
return getLegendLabel(metricName);
}, []);
const fields = useMemo(() => {
const metric = metrics[seriesIdx - 1]?.metric || {};
const fields = Object.keys(metric).filter(k => k !== "__name__");
return fields.map(key => `${key}="${metric[key]}"`);
}, [metrics, seriesIdx]);
const handleClose = () => {
onClose && onClose(id);
};
const handleMouseDown = (e: ReactMouseEvent<HTMLButtonElement, MouseEvent>) => {
setMoved(true);
setMoving(true);
const { clientX, clientY } = e;
setPosition({ top: clientY, left: clientX });
};
const handleMouseMove = (e: MouseEvent) => {
if (!moving) return;
const { clientX, clientY } = e;
setPosition({ top: clientY, left: clientX });
};
const handleMouseUp = () => {
setMoving(false);
};
const calcPosition = () => {
if (!tooltipRef.current) return;
const topOnChart = u.valToPos((value || 0), series[seriesIdx]?.scale || "1");
const leftOnChart = u.valToPos(dataTime, "x");
const { width: tooltipWidth, height: tooltipHeight } = tooltipRef.current.getBoundingClientRect();
const { width, height } = u.over.getBoundingClientRect();
const margin = 10;
const overflowX = leftOnChart + tooltipWidth >= width ? tooltipWidth + (2 * margin) : 0;
const overflowY = topOnChart + tooltipHeight >= height ? tooltipHeight + (2 * margin) : 0;
setPosition({
top: topOnChart + tooltipOffset.top + margin - overflowY,
left: leftOnChart + tooltipOffset.left + margin - overflowX
});
};
useEffect(calcPosition, [u, value, dataTime, seriesIdx, tooltipOffset, tooltipRef]);
useEffect(() => {
setSeriesIdx(tooltipIdx.seriesIdx);
setDataIdx(tooltipIdx.dataIdx);
}, [tooltipIdx]);
useEffect(() => {
if (moving) {
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
}
return () => {
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
};
}, [moving]);
if (!targetPortal || tooltipIdx.seriesIdx < 0 || tooltipIdx.dataIdx < 0) return null;
return ReactDOM.createPortal((
<div
className={classNames({
"vm-chart-tooltip": true,
"vm-chart-tooltip_sticky": isSticky,
"vm-chart-tooltip_moved": moved
})}
ref={tooltipRef}
style={position}
>
<div className="vm-chart-tooltip-header">
<div className="vm-chart-tooltip-header__date">{date}</div>
{isSticky && (
<>
<Button
className="vm-chart-tooltip-header__drag"
variant="text"
size="small"
startIcon={<DragIcon/>}
onMouseDown={handleMouseDown}
/>
<Button
className="vm-chart-tooltip-header__close"
variant="text"
size="small"
startIcon={<CloseIcon/>}
onClick={handleClose}
/>
</>
)}
</div>
<div className="vm-chart-tooltip-data">
<div
className="vm-chart-tooltip-data__marker"
style={{ background: color }}
/>
<p>
{name}:
<b className="vm-chart-tooltip-data__value">{valueFormat}</b>
{unit}
</p>
</div>
{!!fields.length && (
<div className="vm-chart-tooltip-info">
{fields.map((f, i) => (
<div key={`${f}_${i}`}>{f}</div>
))}
</div>
)}
</div>
), targetPortal);
};
export default ChartTooltip;

View File

@@ -0,0 +1,80 @@
@use "src/styles/variables" as *;
$chart-tooltip-width: 300px;
$chart-tooltip-icon-width: 25px;
$chart-tooltip-half-icon: calc($chart-tooltip-icon-width/2);
$chart-tooltip-date-width: $chart-tooltip-width - (2*$chart-tooltip-icon-width) - (2*$padding-global) - $padding-small;
$chart-tooltip-x: -1 * ($padding-small + $padding-global + $chart-tooltip-date-width + $chart-tooltip-half-icon);
$chart-tooltip-y: -1 * ($padding-small + $chart-tooltip-half-icon);
.vm-chart-tooltip {
position: absolute;
display: grid;
gap: $padding-global;
width: $chart-tooltip-width;
padding: $padding-small;
border-radius: $border-radius-medium;
background: $color-background-tooltip;
color: $color-white;
font-size: $font-size-small;
font-weight: normal;
line-height: 150%;
word-wrap: break-word;
font-family: $font-family-monospace;
z-index: 98;
user-select: text;
pointer-events: none;
&_sticky {
background-color: $color-dove-gray;
pointer-events: auto;
z-index: 99;
}
&_moved {
position: fixed;
margin-top: $chart-tooltip-y;
margin-left: $chart-tooltip-x;
}
&-header {
display: grid;
grid-template-columns: 1fr $chart-tooltip-icon-width $chart-tooltip-icon-width;
gap: $padding-small;
align-items: center;
justify-content: center;
min-height: 25px;
&__close {
color: $color-white;
}
&__drag {
color: $color-white;
cursor: move;
}
}
&-data {
display: grid;
grid-template-columns: auto 1fr;
gap: $padding-small;
align-items: flex-start;
word-break: break-all;
line-height: 12px;
&__value {
padding: 4px;
font-weight: bold;
}
&__marker {
width: 12px;
height: 12px;
}
}
&-info {
display: grid;
grid-gap: 4px;
}
}

View File

@@ -0,0 +1,41 @@
import React, { FC, useMemo } from "preact/compat";
import { LegendItemType } from "../../../utils/uplot/types";
import LegendItem from "./LegendItem/LegendItem";
import "./style.scss";
interface LegendProps {
labels: LegendItemType[];
query: string[];
onChange: (item: LegendItemType, metaKey: boolean) => void;
}
const Legend: FC<LegendProps> = ({ labels, query, onChange }) => {
const groups = useMemo(() => {
return Array.from(new Set(labels.map(l => l.group)));
}, [labels]);
return <>
<div className="vm-legend">
{groups.map((group) => <div
className="vm-legend-group"
key={group}
>
<div className="vm-legend-group-title">
<span className="vm-legend-group-title__count">Query {group}: </span>
<span className="vm-legend-group-title__query">{query[group - 1]}</span>
</div>
<div>
{labels.filter(l => l.group === group).map((legendItem: LegendItemType) =>
<LegendItem
key={legendItem.label}
legend={legendItem}
onChange={onChange}
/>
)}
</div>
</div>)}
</div>
</>;
};
export default Legend;

View File

@@ -0,0 +1,75 @@
import React, { FC, useState, useMemo } from "preact/compat";
import { MouseEvent } from "react";
import { LegendItemType } from "../../../../utils/uplot/types";
import { getLegendLabel } from "../../../../utils/uplot/helpers";
import "./style.scss";
import classNames from "classnames";
import Tooltip from "../../../Main/Tooltip/Tooltip";
import { getFreeFields } from "./helpers";
interface LegendItemProps {
legend: LegendItemType;
onChange: (item: LegendItemType, metaKey: boolean) => void;
}
const LegendItem: FC<LegendItemProps> = ({ legend, onChange }) => {
const [copiedValue, setCopiedValue] = useState("");
const freeFormFields = useMemo(() => getFreeFields(legend), [legend]);
const handleClickFreeField = async (val: string, id: string) => {
await navigator.clipboard.writeText(val);
setCopiedValue(id);
setTimeout(() => setCopiedValue(""), 2000);
};
const createHandlerClick = (legend: LegendItemType) => (e: MouseEvent<HTMLDivElement>) => {
onChange(legend, e.ctrlKey || e.metaKey);
};
const createHandlerCopy = (freeField: string, id: string) => (e: MouseEvent<HTMLDivElement>) => {
e.stopPropagation();
handleClickFreeField(freeField, id);
};
return (
<div
className={classNames({
"vm-legend-item": true,
"vm-legend-item_hide": !legend.checked,
})}
onClick={createHandlerClick(legend)}
>
<div
className="vm-legend-item__marker"
style={{ backgroundColor: legend.color }}
/>
<div className="vm-legend-item-info">
<span className="vm-legend-item-info__label">
{getLegendLabel(legend.label)}
</span>
&#160;&#123;
{freeFormFields.map(f => (
<Tooltip
key={f.id}
open={copiedValue === f.id}
title={"Copied!"}
placement="top-center"
>
<span
className="vm-legend-item-info__free-fields"
key={f.key}
onClick={createHandlerCopy(f.freeField, f.id)}
>
{f.freeField}
</span>
</Tooltip>
))}
&#125;
</div>
</div>
);
};
export default LegendItem;

View File

@@ -0,0 +1,16 @@
import { LegendItemType } from "../../../../utils/uplot/types";
export const getFreeFields = (legend: LegendItemType) => {
const keys = Object.keys(legend.freeFormFields).filter(f => f !== "__name__");
return keys.map(f => {
const freeField = `${f}="${legend.freeFormFields[f]}"`;
const id = `${legend.label}.${freeField}`;
return {
id,
freeField,
key: f
};
});
};

View File

@@ -0,0 +1,51 @@
@use "src/styles/variables" as *;
.vm-legend-item {
display: grid;
grid-template-columns: auto auto;
grid-gap: $padding-small;
align-items: start;
justify-content: start;
padding: $padding-small $padding-large $padding-small $padding-small;
background-color: $color-background-block;
cursor: pointer;
transition: 0.2s ease;
&:hover {
background-color: rgba(0, 0, 0, 0.1);
}
&_hide {
text-decoration: line-through;
opacity: 0.5;
}
&__marker {
width: 14px;
height: 14px;
box-sizing: border-box;
transition: 0.2s ease;
border-radius: 2px;
}
&-info {
font-weight: normal;
&__label {
}
&__free-fields {
padding: 3px;
cursor: pointer;
&:hover {
text-decoration: underline;
}
&:not(:last-child):after {
content: ",";
}
}
}
}

View File

@@ -0,0 +1,30 @@
@use "src/styles/variables" as *;
.vm-legend {
position: relative;
display: flex;
flex-wrap: wrap;
margin-top: $padding-medium;
cursor: default;
&-group {
min-width: 23%;
margin: 0 $padding-global $padding-global 0;
&-title {
display: flex;
align-items: center;
padding: 0 $padding-small $padding-small;
margin-bottom: 1px;
border-bottom: $border-divider;
&__count {
font-weight: bold;
margin-right: $padding-small;
}
&__query {
}
}
}
}

View File

@@ -0,0 +1,272 @@
import React, { FC, useCallback, useEffect, useMemo, useRef, useState } from "preact/compat";
import uPlot, {
AlignedData as uPlotData,
Options as uPlotOptions,
Series as uPlotSeries,
Range,
Scales,
Scale,
} from "uplot";
import { defaultOptions } from "../../../utils/uplot/helpers";
import { dragChart } from "../../../utils/uplot/events";
import { getAxes, getMinMaxBuffer } from "../../../utils/uplot/axes";
import { MetricResult } from "../../../api/types";
import { limitsDurations } from "../../../utils/time";
import throttle from "lodash.throttle";
import useResize from "../../../hooks/useResize";
import { TimeParams } from "../../../types";
import { YaxisState } from "../../../state/graph/reducer";
import "uplot/dist/uPlot.min.css";
import "./style.scss";
import classNames from "classnames";
import ChartTooltip, { ChartTooltipProps } from "../ChartTooltip/ChartTooltip";
export interface LineChartProps {
metrics: MetricResult[];
data: uPlotData;
period: TimeParams;
yaxis: YaxisState;
series: uPlotSeries[];
unit?: string;
setPeriod: ({ from, to }: {from: Date, to: Date}) => void;
container: HTMLDivElement | null
}
enum typeChartUpdate {xRange = "xRange", yRange = "yRange", data = "data"}
const LineChart: FC<LineChartProps> = ({
data,
series,
metrics = [],
period,
yaxis,
unit,
setPeriod,
container
}) => {
const uPlotRef = useRef<HTMLDivElement>(null);
const [isPanning, setPanning] = useState(false);
const [xRange, setXRange] = useState({ min: period.start, max: period.end });
const [uPlotInst, setUPlotInst] = useState<uPlot>();
const layoutSize = useResize(container);
const [showTooltip, setShowTooltip] = useState(false);
const [tooltipIdx, setTooltipIdx] = useState({ seriesIdx: -1, dataIdx: -1 });
const [tooltipOffset, setTooltipOffset] = useState({ left: 0, top: 0 });
const [stickyTooltips, setStickyToolTips] = useState<ChartTooltipProps[]>([]);
const tooltipId = useMemo(() => `${tooltipIdx.seriesIdx}_${tooltipIdx.dataIdx}`, [tooltipIdx]);
const setScale = ({ min, max }: { min: number, max: number }): void => {
setPeriod({ from: new Date(min * 1000), to: new Date(max * 1000) });
};
const throttledSetScale = useCallback(throttle(setScale, 500), []);
const setPlotScale = ({ u, min, max }: { u: uPlot, min: number, max: number }) => {
const delta = (max - min) * 1000;
if ((delta < limitsDurations.min) || (delta > limitsDurations.max)) return;
u.setScale("x", { min, max });
setXRange({ min, max });
throttledSetScale({ min, max });
};
const onReadyChart = (u: uPlot) => {
const factor = 0.9;
setTooltipOffset({
left: parseFloat(u.over.style.left),
top: parseFloat(u.over.style.top)
});
u.over.addEventListener("mousedown", e => {
const { ctrlKey, metaKey, button } = e;
const leftClick = button === 0;
const leftClickWithMeta = leftClick && (ctrlKey || metaKey);
if (leftClickWithMeta) {
// drag pan
dragChart({ u, e, setPanning, setPlotScale, factor });
}
});
u.over.addEventListener("wheel", e => {
if (!e.ctrlKey && !e.metaKey) return;
e.preventDefault();
const { width } = u.over.getBoundingClientRect();
const zoomPos = u.cursor.left && u.cursor.left > 0 ? u.cursor.left : 0;
const xVal = u.posToVal(zoomPos, "x");
const oxRange = (u.scales.x.max || 0) - (u.scales.x.min || 0);
const nxRange = e.deltaY < 0 ? oxRange * factor : oxRange / factor;
const min = xVal - (zoomPos / width) * nxRange;
const max = min + nxRange;
u.batch(() => setPlotScale({ u, min, max }));
});
};
const handleKeyDown = (e: KeyboardEvent) => {
const { target, ctrlKey, metaKey, key } = e;
const isInput = target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement;
if (!uPlotInst || isInput) return;
const minus = key === "-";
const plus = key === "+" || key === "=";
if ((minus || plus) && !(ctrlKey || metaKey)) {
e.preventDefault();
const factor = (xRange.max - xRange.min) / 10 * (plus ? 1 : -1);
setPlotScale({
u: uPlotInst,
min: xRange.min + factor,
max: xRange.max - factor
});
}
};
const handleClick = () => {
const id = `${tooltipIdx.seriesIdx}_${tooltipIdx.dataIdx}`;
const props = {
id,
unit,
series,
metrics,
tooltipIdx,
tooltipOffset,
};
if (!stickyTooltips.find(t => t.id === id)) {
const tooltipProps = JSON.parse(JSON.stringify(props));
setStickyToolTips(prev => [...prev, tooltipProps]);
}
};
const handleUnStick = (id:string) => {
setStickyToolTips(prev => prev.filter(t => t.id !== id));
};
const setCursor = (u: uPlot) => {
const dataIdx = u.cursor.idx ?? -1;
setTooltipIdx(prev => ({ ...prev, dataIdx }));
};
const seriesFocus = (u: uPlot, sidx: (number | null)) => {
const seriesIdx = sidx ?? -1;
setTooltipIdx(prev => ({ ...prev, seriesIdx }));
};
const getRangeX = (): Range.MinMax => [xRange.min, xRange.max];
const getRangeY = (u: uPlot, min = 0, max = 1, axis: string): Range.MinMax => {
if (yaxis.limits.enable) return yaxis.limits.range[axis];
return getMinMaxBuffer(min, max);
};
const getScales = (): Scales => {
const scales: { [key: string]: { range: Scale.Range } } = { x: { range: getRangeX } };
const ranges = Object.keys(yaxis.limits.range);
(ranges.length ? ranges : ["1"]).forEach(axis => {
scales[axis] = { range: (u: uPlot, min = 0, max = 1) => getRangeY(u, min, max, axis) };
});
return scales;
};
const options: uPlotOptions = {
...defaultOptions,
series,
axes: getAxes( [{}, { scale: "1" }], unit),
scales: { ...getScales() },
width: layoutSize.width || 400,
plugins: [{ hooks: { ready: onReadyChart, setCursor, setSeries: seriesFocus } }],
hooks: {
setSelect: [
(u) => {
const min = u.posToVal(u.select.left, "x");
const max = u.posToVal(u.select.left + u.select.width, "x");
setPlotScale({ u, min, max });
}
]
}
};
const updateChart = (type: typeChartUpdate): void => {
if (!uPlotInst) return;
switch (type) {
case typeChartUpdate.xRange:
uPlotInst.scales.x.range = getRangeX;
break;
case typeChartUpdate.yRange:
Object.keys(yaxis.limits.range).forEach(axis => {
if (!uPlotInst.scales[axis]) return;
uPlotInst.scales[axis].range = (u: uPlot, min = 0, max = 1) => getRangeY(u, min, max, axis);
});
break;
case typeChartUpdate.data:
uPlotInst.setData(data);
break;
}
if (!isPanning) uPlotInst.redraw();
};
useEffect(() => setXRange({ min: period.start, max: period.end }), [period]);
useEffect(() => {
setStickyToolTips([]);
setTooltipIdx({ seriesIdx: -1, dataIdx: -1 });
if (!uPlotRef.current) return;
const u = new uPlot(options, data, uPlotRef.current);
setUPlotInst(u);
setXRange({ min: period.start, max: period.end });
return u.destroy;
}, [uPlotRef.current, series, layoutSize]);
useEffect(() => {
window.addEventListener("keydown", handleKeyDown);
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, [xRange]);
useEffect(() => updateChart(typeChartUpdate.data), [data]);
useEffect(() => updateChart(typeChartUpdate.xRange), [xRange]);
useEffect(() => updateChart(typeChartUpdate.yRange), [yaxis]);
useEffect(() => {
const show = tooltipIdx.dataIdx !== -1 && tooltipIdx.seriesIdx !== -1;
setShowTooltip(show);
if (show) window.addEventListener("click", handleClick);
return () => {
window.removeEventListener("click", handleClick);
};
}, [tooltipIdx, stickyTooltips]);
return (
<div
className={classNames({
"vm-line-chart": true,
"vm-line-chart_panning": isPanning
})}
>
<div
className="vm-line-chart__u-plot"
ref={uPlotRef}
/>
{uPlotInst && showTooltip && (
<ChartTooltip
unit={unit}
u={uPlotInst}
series={series}
metrics={metrics}
tooltipIdx={tooltipIdx}
tooltipOffset={tooltipOffset}
id={tooltipId}
/>
)}
{uPlotInst && stickyTooltips.map(t => (
<ChartTooltip
{...t}
isSticky
u={uPlotInst}
key={t.id}
onClose={handleUnStick}
/>
))}
</div>
);
};
export default LineChart;

View File

@@ -0,0 +1,14 @@
@use "src/styles/variables" as *;
.vm-line-chart {
height: 500px;
pointer-events: auto;
&_panning {
pointer-events: none;
}
&__u-plot {
position: relative;
}
}

View File

@@ -0,0 +1,71 @@
import React, { FC } from "preact/compat";
import StepConfigurator from "../StepConfigurator/StepConfigurator";
import { useGraphDispatch } from "../../../state/graph/GraphStateContext";
import { getAppModeParams } from "../../../utils/app-mode";
import TenantsConfiguration from "../TenantsConfiguration/TenantsConfiguration";
import { useCustomPanelDispatch, useCustomPanelState } from "../../../state/customPanel/CustomPanelStateContext";
import { useTimeState } from "../../../state/time/TimeStateContext";
import { useQueryDispatch, useQueryState } from "../../../state/query/QueryStateContext";
import "./style.scss";
import Switch from "../../Main/Switch/Switch";
const AdditionalSettings: FC = () => {
const graphDispatch = useGraphDispatch();
const { inputTenantID } = getAppModeParams();
const { autocomplete } = useQueryState();
const queryDispatch = useQueryDispatch();
const { nocache, isTracingEnabled } = useCustomPanelState();
const customPanelDispatch = useCustomPanelDispatch();
const { period: { step } } = useTimeState();
const onChangeCache = () => {
customPanelDispatch({ type: "TOGGLE_NO_CACHE" });
};
const onChangeQueryTracing = () => {
customPanelDispatch({ type: "TOGGLE_QUERY_TRACING" });
};
const onChangeAutocomplete = () => {
queryDispatch({ type: "TOGGLE_AUTOCOMPLETE" });
};
const onChangeStep = (value: number) => {
graphDispatch({ type: "SET_CUSTOM_STEP", payload: value });
};
return <div className="vm-additional-settings">
<Switch
label={"Autocomplete"}
value={autocomplete}
onChange={onChangeAutocomplete}
/>
<Switch
label={"Disable cache"}
value={nocache}
onChange={onChangeCache}
/>
<Switch
label={"Trace query"}
value={isTracingEnabled}
onChange={onChangeQueryTracing}
/>
<div className="vm-additional-settings__input">
<StepConfigurator
defaultStep={step}
setStep={onChangeStep}
/>
</div>
{!!inputTenantID && (
<div className="vm-additional-settings__input">
<TenantsConfiguration/>
</div>
)}
</div>;
};
export default AdditionalSettings;

View File

@@ -0,0 +1,14 @@
@use "src/styles/variables" as *;
.vm-additional-settings {
display: inline-flex;
align-items: center;
justify-content: flex-start;
flex-wrap: wrap;
gap: 24px;
&__input {
flex-basis: 160px;
margin-bottom: -6px;
}
}

View File

@@ -0,0 +1,48 @@
import React, { FC, useMemo, useRef } from "preact/compat";
import { useCardinalityState, useCardinalityDispatch } from "../../../state/cardinality/CardinalityStateContext";
import dayjs from "dayjs";
import Button from "../../Main/Button/Button";
import { CalendarIcon } from "../../Main/Icons";
import Tooltip from "../../Main/Tooltip/Tooltip";
import { getAppModeEnable } from "../../../utils/app-mode";
import { DATE_FORMAT } from "../../../constants/date";
import DatePicker from "../../Main/DatePicker/DatePicker";
const CardinalityDatePicker: FC = () => {
const appModeEnable = getAppModeEnable();
const buttonRef = useRef<HTMLDivElement>(null);
const { date } = useCardinalityState();
const cardinalityDispatch = useCardinalityDispatch();
const dateFormatted = useMemo(() => dayjs(date).format(DATE_FORMAT), [date]);
const handleChangeDate = (val: string) => {
cardinalityDispatch({ type: "SET_DATE", payload: val });
};
return (
<div>
<div ref={buttonRef}>
<Tooltip title="Date control">
<Button
className={appModeEnable ? "" : "vm-header-button"}
variant="contained"
color="primary"
startIcon={<CalendarIcon/>}
>
{dateFormatted}
</Button>
</Tooltip>
</div>
<DatePicker
date={date || ""}
format={DATE_FORMAT}
onChange={handleChangeDate}
targetRef={buttonRef}
/>
</div>
);
};
export default CardinalityDatePicker;

View File

@@ -0,0 +1,94 @@
import React, { FC, useState } from "preact/compat";
import ServerConfigurator from "./ServerConfigurator/ServerConfigurator";
import { useAppDispatch, useAppState } from "../../../state/common/StateContext";
import { SettingsIcon } from "../../Main/Icons";
import Button from "../../Main/Button/Button";
import Modal from "../../Main/Modal/Modal";
import "./style.scss";
import Tooltip from "../../Main/Tooltip/Tooltip";
import LimitsConfigurator from "./LimitsConfigurator/LimitsConfigurator";
import { SeriesLimits } from "../../../types";
import { useCustomPanelDispatch, useCustomPanelState } from "../../../state/customPanel/CustomPanelStateContext";
import { getAppModeEnable } from "../../../utils/app-mode";
import classNames from "classnames";
const title = "Settings";
const GlobalSettings: FC = () => {
const appModeEnable = getAppModeEnable();
const { serverUrl: stateServerUrl } = useAppState();
const { seriesLimits } = useCustomPanelState();
const dispatch = useAppDispatch();
const customPanelDispatch = useCustomPanelDispatch();
const [serverUrl, setServerUrl] = useState(stateServerUrl);
const [limits, setLimits] = useState<SeriesLimits>(seriesLimits);
const [open, setOpen] = useState(false);
const handleOpen = () => setOpen(true);
const handleClose = () => setOpen(false);
const handlerApply = () => {
dispatch({ type: "SET_SERVER", payload: serverUrl });
customPanelDispatch({ type: "SET_SERIES_LIMITS", payload: limits });
handleClose();
};
return <>
<Tooltip title={title}>
<Button
className={classNames({
"vm-header-button": !appModeEnable
})}
variant="contained"
color="primary"
startIcon={<SettingsIcon/>}
onClick={handleOpen}
/>
</Tooltip>
{open && (
<Modal
title={title}
onClose={handleClose}
>
<div className="vm-server-configurator">
{!appModeEnable && (
<div className="vm-server-configurator__input">
<ServerConfigurator
serverUrl={serverUrl}
onChange={setServerUrl}
onEnter={handlerApply}
/>
</div>
)}
<div className="vm-server-configurator__input">
<LimitsConfigurator
limits={limits}
onChange={setLimits}
onEnter={handlerApply}
/>
</div>
<div className="vm-server-configurator__footer">
<Button
variant="outlined"
color="error"
onClick={handleClose}
>
Cancel
</Button>
<Button
variant="contained"
onClick={handlerApply}
>
apply
</Button>
</div>
</div>
</Modal>
)}
</>;
};
export default GlobalSettings;

View File

@@ -0,0 +1,88 @@
import React, { FC, useState } from "preact/compat";
import { DisplayType, ErrorTypes, SeriesLimits } from "../../../../types";
import TextField from "../../../Main/TextField/TextField";
import Tooltip from "../../../Main/Tooltip/Tooltip";
import { InfoIcon, RestartIcon } from "../../../Main/Icons";
import Button from "../../../Main/Button/Button";
import { DEFAULT_MAX_SERIES } from "../../../../constants/graph";
import "./style.scss";
export interface ServerConfiguratorProps {
limits: SeriesLimits
onChange: (limits: SeriesLimits) => void
onEnter: () => void
}
const fields: {label: string, type: DisplayType}[] = [
{ label: "Graph", type: "chart" },
{ label: "JSON", type: "code" },
{ label: "Table", type: "table" }
];
const LimitsConfigurator: FC<ServerConfiguratorProps> = ({ limits, onChange , onEnter }) => {
const [error, setError] = useState({
table: "",
chart: "",
code: ""
});
const handleChange = (val: string, type: DisplayType) => {
const value = val || "";
setError(prev => ({ ...prev, [type]: +value < 0 ? ErrorTypes.positiveNumber : "" }));
onChange({
...limits,
[type]: !value ? Infinity : value
});
};
const handleReset = () => {
onChange(DEFAULT_MAX_SERIES);
};
const createChangeHandler = (type: DisplayType) => (val: string) => {
handleChange(val, type);
};
return (
<div className="vm-limits-configurator">
<div className="vm-limits-configurator-title">
Series limits by tabs
<Tooltip title="To disable limits set to 0">
<Button
variant="text"
color="primary"
size="small"
startIcon={<InfoIcon/>}
/>
</Tooltip>
<div className="vm-limits-configurator-title__reset">
<Button
variant="text"
color="primary"
size="small"
startIcon={<RestartIcon/>}
onClick={handleReset}
>
Reset
</Button>
</div>
</div>
<div className="vm-limits-configurator__inputs">
{fields.map(f => (
<TextField
key={f.type}
label={f.label}
value={limits[f.type]}
error={error[f.type]}
onChange={createChangeHandler(f.type)}
onEnter={onEnter}
type="number"
/>
))}
</div>
</div>
);
};
export default LimitsConfigurator;

View File

@@ -0,0 +1,28 @@
@use "src/styles/variables" as *;
.vm-limits-configurator {
&-title {
display: flex;
align-items: center;
justify-content: flex-start;
font-size: $font-size;
font-weight: bold;
margin-bottom: $padding-global;
&__reset {
display: flex;
align-items: center;
justify-content: flex-end;
flex-grow: 1;
}
}
&__inputs {
display: grid;
grid-template-columns: repeat(3, 1fr);
align-items: center;
justify-content: space-between;
gap: $padding-global;
}
}

View File

@@ -0,0 +1,36 @@
import React, { FC, useState } from "preact/compat";
import { ErrorTypes } from "../../../../types";
import TextField from "../../../Main/TextField/TextField";
import { isValidHttpUrl } from "../../../../utils/url";
export interface ServerConfiguratorProps {
serverUrl: string
onChange: (url: string) => void
onEnter: () => void
}
const ServerConfigurator: FC<ServerConfiguratorProps> = ({ serverUrl, onChange , onEnter }) => {
const [error, setError] = useState("");
const onChangeServer = (val: string) => {
const value = val || "";
onChange(value);
setError("");
if (!value) setError(ErrorTypes.emptyServer);
if (!isValidHttpUrl(value)) setError(ErrorTypes.validServer);
};
return (
<TextField
autofocus
label="Server URL"
value={serverUrl}
error={error}
onChange={onChangeServer}
onEnter={onEnter}
/>
);
};
export default ServerConfigurator;

View File

@@ -0,0 +1,22 @@
@use "src/styles/variables" as *;
.vm-server-configurator {
display: grid;
align-items: center;
gap: $padding-medium;
width: 600px;
&__input {
}
&__footer {
display: inline-grid;
grid-template-columns: repeat(2, 1fr);
align-items: center;
justify-content: flex-end;
gap: $padding-small;
margin-left: auto;
margin-right: 0;
}
}

View File

@@ -0,0 +1,62 @@
import React, { FC, useCallback, useMemo } from "preact/compat";
import debounce from "lodash.debounce";
import { AxisRange, YaxisState } from "../../../../state/graph/reducer";
import "./style.scss";
import TextField from "../../../Main/TextField/TextField";
import Switch from "../../../Main/Switch/Switch";
interface AxesLimitsConfiguratorProps {
yaxis: YaxisState,
setYaxisLimits: (limits: AxisRange) => void,
toggleEnableLimits: () => void
}
const AxesLimitsConfigurator: FC<AxesLimitsConfiguratorProps> = ({ yaxis, setYaxisLimits, toggleEnableLimits }) => {
const axes = useMemo(() => Object.keys(yaxis.limits.range), [yaxis.limits.range]);
const onChangeLimit = (value: string, axis: string, index: number) => {
const newLimits = yaxis.limits.range;
newLimits[axis][index] = +value;
if (newLimits[axis][0] === newLimits[axis][1] || newLimits[axis][0] > newLimits[axis][1]) return;
setYaxisLimits(newLimits);
};
const debouncedOnChangeLimit = useCallback(debounce(onChangeLimit, 500), [yaxis.limits.range]);
const createHandlerOnchangeAxis = (axis: string, index: number) => (val: string) => {
debouncedOnChangeLimit(val, axis, index);
};
return <div className="vm-axes-limits">
<Switch
value={yaxis.limits.enable}
onChange={toggleEnableLimits}
label="Fix the limits for y-axis"
/>
<div className="vm-axes-limits-list">
{axes.map(axis => (
<div
className="vm-axes-limits-list__inputs"
key={axis}
>
<TextField
label={`Min ${axis}`}
type="number"
disabled={!yaxis.limits.enable}
value={yaxis.limits.range[axis][0]}
onChange={createHandlerOnchangeAxis(axis, 0)}
/>
<TextField
label={`Max ${axis}`}
type="number"
disabled={!yaxis.limits.enable}
value={yaxis.limits.range[axis][1]}
onChange={createHandlerOnchangeAxis(axis, 1)}
/>
</div>
))}
</div>
</div>;
};
export default AxesLimitsConfigurator;

View File

@@ -0,0 +1,20 @@
@use "src/styles/variables" as *;
.vm-axes-limits {
display: grid;
align-items: center;
gap: $padding-global;
max-width: 300px;
&-list {
display: grid;
align-items: center;
gap: $padding-global;
&__inputs {
display: grid;
grid-template-columns: repeat(2, 120px);
gap: $padding-small;
}
}
}

View File

@@ -0,0 +1,77 @@
import React, { FC, useRef, useState } from "preact/compat";
import AxesLimitsConfigurator from "./AxesLimitsConfigurator/AxesLimitsConfigurator";
import { AxisRange, YaxisState } from "../../../state/graph/reducer";
import { CloseIcon, SettingsIcon } from "../../Main/Icons";
import Button from "../../Main/Button/Button";
import useClickOutside from "../../../hooks/useClickOutside";
import Popper from "../../Main/Popper/Popper";
import "./style.scss";
import Tooltip from "../../Main/Tooltip/Tooltip";
const title = "Axes settings";
interface GraphSettingsProps {
yaxis: YaxisState,
setYaxisLimits: (limits: AxisRange) => void,
toggleEnableLimits: () => void
}
const GraphSettings: FC<GraphSettingsProps> = ({ yaxis, setYaxisLimits, toggleEnableLimits }) => {
const popperRef = useRef<HTMLDivElement>(null);
const [openPopper, setOpenPopper] = useState(false);
const buttonRef = useRef<HTMLDivElement>(null);
useClickOutside(popperRef, () => setOpenPopper(false), buttonRef);
const toggleOpen = () => {
setOpenPopper(prev => !prev);
};
const handleClose = () => {
setOpenPopper(false);
};
return (
<div className="vm-graph-settings">
<Tooltip title={title}>
<div ref={buttonRef}>
<Button
variant="text"
startIcon={<SettingsIcon/>}
onClick={toggleOpen}
/>
</div>
</Tooltip>
<Popper
open={openPopper}
buttonRef={buttonRef}
placement="bottom-right"
onClose={handleClose}
>
<div
className="vm-graph-settings-popper"
ref={popperRef}
>
<div className="vm-popper-header">
<h3 className="vm-popper-header__title">
{title}
</h3>
<Button
size="small"
startIcon={<CloseIcon/>}
onClick={handleClose}
/>
</div>
<div className="vm-graph-settings-popper__body">
<AxesLimitsConfigurator
yaxis={yaxis}
setYaxisLimits={setYaxisLimits}
toggleEnableLimits={toggleEnableLimits}
/>
</div>
</div>
</Popper>
</div>
);
};
export default GraphSettings;

View File

@@ -0,0 +1,15 @@
@use "src/styles/variables" as *;
.vm-graph-settings {
&-popper {
display: grid;
gap: $padding-global;
padding: 0 0 $padding-global;
&__body {
display: grid;
gap: $padding-small;
padding: 0 $padding-global;
}
}
}

View File

@@ -0,0 +1,92 @@
import React, { FC, useRef } from "preact/compat";
import { KeyboardEvent } from "react";
import { ErrorTypes } from "../../../types";
import TextField from "../../Main/TextField/TextField";
import Autocomplete from "../../Main/Autocomplete/Autocomplete";
import "./style.scss";
export interface QueryEditorProps {
onChange: (query: string) => void;
onEnter: () => void;
onArrowUp: () => void;
onArrowDown: () => void;
value: string;
oneLiner?: boolean;
autocomplete: boolean;
error?: ErrorTypes | string;
options: string[];
label: string;
disabled?: boolean
}
const QueryEditor: FC<QueryEditorProps> = ({
value,
onChange,
onEnter,
onArrowUp,
onArrowDown,
autocomplete,
error,
options,
label,
disabled = false
}) => {
const autocompleteAnchorEl = useRef<HTMLDivElement>(null);
const handleSelect = (val: string) => {
onChange(val);
};
const handleKeyDown = (e: KeyboardEvent) => {
const { key, ctrlKey, metaKey, shiftKey } = e;
const ctrlMetaKey = ctrlKey || metaKey;
const arrowUp = key === "ArrowUp";
const arrowDown = key === "ArrowDown";
const enter = key === "Enter";
// prev value from history
if (arrowUp && ctrlMetaKey) {
e.preventDefault();
onArrowUp();
}
// next value from history
if (arrowDown && ctrlMetaKey) {
e.preventDefault();
onArrowDown();
}
// execute query
if (enter && !shiftKey) {
onEnter();
}
};
return <div
className="vm-query-editor"
ref={autocompleteAnchorEl}
>
<TextField
value={value}
label={label}
type={"textarea"}
autofocus={!!value}
error={error}
onKeyDown={handleKeyDown}
onChange={onChange}
disabled={disabled}
/>
{autocomplete && (
<Autocomplete
value={value}
options={options}
anchor={autocompleteAnchorEl}
onSelect={handleSelect}
/>
)}
</div>;
};
export default QueryEditor;

View File

@@ -0,0 +1,9 @@
@use "src/styles/variables" as *;
.vm-query-editor {
&-autocomplete {
max-height: 300px;
overflow: auto;
}
}

View File

@@ -0,0 +1,67 @@
import React, { FC, useCallback, useState } from "preact/compat";
import { useEffect } from "react";
import debounce from "lodash.debounce";
import { RestartIcon } from "../../Main/Icons";
import TextField from "../../Main/TextField/TextField";
import Button from "../../Main/Button/Button";
import Tooltip from "../../Main/Tooltip/Tooltip";
interface StepConfiguratorProps {
defaultStep?: number,
setStep: (step: number) => void,
}
const StepConfigurator: FC<StepConfiguratorProps> = ({ defaultStep, setStep }) => {
const [customStep, setCustomStep] = useState(defaultStep);
const [error, setError] = useState("");
const handleApply = (step: number) => setStep(step || 1);
const debouncedHandleApply = useCallback(debounce(handleApply, 700), []);
const onChangeStep = (val: string) => {
const value = +val;
if (!value) return;
handleSetStep(value);
};
const handleSetStep = (value: number) => {
if (value > 0) {
setCustomStep(value);
debouncedHandleApply(value);
setError("");
} else {
setError("step is out of allowed range");
}
};
const handleReset = () => {
handleSetStep(defaultStep || 1);
};
useEffect(() => {
if (defaultStep) handleSetStep(defaultStep);
}, [defaultStep]);
return (
<TextField
label="Step value"
type="number"
value={customStep}
error={error}
onChange={onChangeStep}
endIcon={(
<Tooltip title="Reset step to default">
<Button
variant={"text"}
size={"small"}
startIcon={<RestartIcon/>}
onClick={handleReset}
/>
</Tooltip>
)}
/>
);
};
export default StepConfigurator;

View File

@@ -0,0 +1,58 @@
import React, { FC, useState, useEffect, useCallback } from "preact/compat";
import { useAppDispatch, useAppState } from "../../../state/common/StateContext";
import debounce from "lodash.debounce";
import { getAppModeParams } from "../../../utils/app-mode";
import { useTimeDispatch } from "../../../state/time/TimeStateContext";
import { InfoIcon } from "../../Main/Icons";
import TextField from "../../Main/TextField/TextField";
import Button from "../../Main/Button/Button";
import Tooltip from "../../Main/Tooltip/Tooltip";
const TenantsConfiguration: FC = () => {
const { serverURL } = getAppModeParams();
const { tenantId: tenantIdState } = useAppState();
const dispatch = useAppDispatch();
const timeDispatch = useTimeDispatch();
const [tenantId, setTenantId] = useState<string | number>(tenantIdState || 0);
const handleApply = (value: string | number) => {
const tenantId = Number(value);
dispatch({ type: "SET_TENANT_ID", payload: tenantId });
if (serverURL) {
const updateServerUrl = serverURL.replace(/(\/select\/)([\d]+)(\/prometheus)/gmi, `$1${tenantId}$3`);
dispatch({ type: "SET_SERVER", payload: updateServerUrl });
timeDispatch({ type: "RUN_QUERY" });
}
};
const debouncedHandleApply = useCallback(debounce(handleApply, 700), []);
const handleChange = (value: string) => {
setTenantId(value);
debouncedHandleApply(value);
};
useEffect(() => {
if (tenantId === tenantIdState) return;
setTenantId(tenantIdState);
}, [tenantIdState]);
return <TextField
label="Tenant ID"
type="number"
value={tenantId}
onChange={handleChange}
endIcon={(
<Tooltip title={"Define tenant id if you need request to another storage"}>
<Button
variant={"text"}
size={"small"}
startIcon={<InfoIcon/>}
/>
</Tooltip>
)}
/>;
};
export default TenantsConfiguration;

View File

@@ -0,0 +1,143 @@
import React, { FC, useEffect, useRef, useState } from "preact/compat";
import { useTimeDispatch } from "../../../../state/time/TimeStateContext";
import { getAppModeEnable } from "../../../../utils/app-mode";
import Button from "../../../Main/Button/Button";
import { ArrowDownIcon, RefreshIcon } from "../../../Main/Icons";
import Popper from "../../../Main/Popper/Popper";
import "./style.scss";
import classNames from "classnames";
import Tooltip from "../../../Main/Tooltip/Tooltip";
interface AutoRefreshOption {
seconds: number
title: string
}
const delayOptions: AutoRefreshOption[] = [
{ seconds: 0, title: "Off" },
{ seconds: 1, title: "1s" },
{ seconds: 2, title: "2s" },
{ seconds: 5, title: "5s" },
{ seconds: 10, title: "10s" },
{ seconds: 30, title: "30s" },
{ seconds: 60, title: "1m" },
{ seconds: 300, title: "5m" },
{ seconds: 900, title: "15m" },
{ seconds: 1800, title: "30m" },
{ seconds: 3600, title: "1h" },
{ seconds: 7200, title: "2h" }
];
export const ExecutionControls: FC = () => {
const dispatch = useTimeDispatch();
const appModeEnable = getAppModeEnable();
const [autoRefresh, setAutoRefresh] = useState(false);
const [selectedDelay, setSelectedDelay] = useState<AutoRefreshOption>(delayOptions[0]);
const handleChange = (d: AutoRefreshOption) => {
if ((autoRefresh && !d.seconds) || (!autoRefresh && d.seconds)) {
setAutoRefresh(prev => !prev);
}
setSelectedDelay(d);
setOpenOptions(false);
};
const handleUpdate = () => {
dispatch({ type: "RUN_QUERY" });
};
useEffect(() => {
const delay = selectedDelay.seconds;
let timer: number;
if (autoRefresh) {
timer = setInterval(() => {
dispatch({ type: "RUN_QUERY" });
}, delay * 1000) as unknown as number;
} else {
setSelectedDelay(delayOptions[0]);
}
return () => {
timer && clearInterval(timer);
};
}, [selectedDelay, autoRefresh]);
const [openOptions, setOpenOptions] = useState(false);
const optionsButtonRef = useRef<HTMLDivElement>(null);
const toggleOpenOptions = () => {
setOpenOptions(prev => !prev);
};
const handleCloseOptions = () => {
setOpenOptions(false);
};
const createHandlerChange = (d: AutoRefreshOption) => () => {
handleChange(d);
};
return <>
<div className="vm-execution-controls">
<div
className={classNames({
"vm-execution-controls-buttons": true,
"vm-header-button": !appModeEnable
})}
>
<Tooltip title="Refresh dashboard">
<Button
variant="contained"
color="primary"
onClick={handleUpdate}
startIcon={<RefreshIcon/>}
/>
</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>
</div>
</div>
<Popper
open={openOptions}
placement="bottom-right"
onClose={handleCloseOptions}
buttonRef={optionsButtonRef}
>
<div className="vm-execution-controls-list">
{delayOptions.map(d => (
<div
className={classNames({
"vm-list__item": true,
"vm-list__item_active": d.seconds === selectedDelay.seconds
})}
key={d.seconds}
onClick={createHandlerChange(d)}
>
{d.title}
</div>
))}
</div>
</Popper>
</>;
};

View File

@@ -0,0 +1,32 @@
@use "src/styles/variables" as *;
@use "src/components/Main/Button/style" as *;
.vm-execution-controls {
&-buttons {
display: flex;
justify-content: space-between;
border-radius: calc($button-radius + 1px);
min-width: 107px;
&__arrow {
display: flex;
align-items: center;
justify-content: center;
transform: rotate(0);
transition: transform 200ms ease-in-out;
&_open {
transform: rotate(180deg);
}
}
}
&-list {
width: 124px;
max-height: 208px;
overflow: auto;
padding: $padding-small 0;
font-size: $font-size;
}
}

View File

@@ -0,0 +1,35 @@
import React, { FC } from "preact/compat";
import { relativeTimeOptions } from "../../../../utils/time";
import "./style.scss";
import classNames from "classnames";
interface TimeDurationSelector {
setDuration: ({ duration, until, id }: {duration: string, until: Date, id: string}) => void;
relativeTime: string;
}
const TimeDurationSelector: FC<TimeDurationSelector> = ({ relativeTime, setDuration }) => {
const createHandlerClick = (value: { duration: string, until: Date, id: string }) => () => {
setDuration(value);
};
return (
<div className="vm-time-duration">
{relativeTimeOptions.map(({ id, duration, until, title }) => (
<div
className={classNames({
"vm-list__item": true,
"vm-list__item_active": id === relativeTime
})}
key={id}
onClick={createHandlerClick({ duration, until: until(), id })}
>
{title || duration}
</div>
))}
</div>
);
};
export default TimeDurationSelector;

View File

@@ -0,0 +1,7 @@
@use "src/styles/variables" as *;
.vm-time-duration {
max-height: 168px;
overflow: auto;
font-size: $font-size;
}

View File

@@ -0,0 +1,192 @@
import React, { FC, useEffect, useState, useMemo, useRef } from "preact/compat";
import { dateFromSeconds, formatDateForNativeInput } from "../../../../utils/time";
import TimeDurationSelector from "../TimeDurationSelector/TimeDurationSelector";
import dayjs from "dayjs";
import { getAppModeEnable } from "../../../../utils/app-mode";
import { useTimeDispatch, useTimeState } from "../../../../state/time/TimeStateContext";
import { AlarmIcon, CalendarIcon, ClockIcon } from "../../../Main/Icons";
import Button from "../../../Main/Button/Button";
import Popper from "../../../Main/Popper/Popper";
import Tooltip from "../../../Main/Tooltip/Tooltip";
import { DATE_TIME_FORMAT } from "../../../../constants/date";
import useResize from "../../../../hooks/useResize";
import DatePicker from "../../../Main/DatePicker/DatePicker";
import "./style.scss";
import useClickOutside from "../../../../hooks/useClickOutside";
export const TimeSelector: FC = () => {
const wrapperRef = useRef<HTMLDivElement>(null);
const documentSize = useResize(document.body);
const displayFullDate = useMemo(() => documentSize.width > 1120, [documentSize]);
const [until, setUntil] = useState<string>();
const [from, setFrom] = useState<string>();
const formFormat = useMemo(() => dayjs(from).format(DATE_TIME_FORMAT), [from]);
const untilFormat = useMemo(() => dayjs(until).format(DATE_TIME_FORMAT), [until]);
const { period: { end, start }, relativeTime } = useTimeState();
const dispatch = useTimeDispatch();
const appModeEnable = getAppModeEnable();
useEffect(() => {
setUntil(formatDateForNativeInput(dateFromSeconds(end)));
}, [end]);
useEffect(() => {
setFrom(formatDateForNativeInput(dateFromSeconds(start)));
}, [start]);
const setDuration = ({ duration, until, id }: {duration: string, until: Date, id: string}) => {
dispatch({ type: "SET_RELATIVE_TIME", payload: { duration, until, id } });
setOpenOptions(false);
};
const formatRange = useMemo(() => {
const startFormat = dayjs(dateFromSeconds(start)).format(DATE_TIME_FORMAT);
const endFormat = dayjs(dateFromSeconds(end)).format(DATE_TIME_FORMAT);
return {
start: startFormat,
end: endFormat
};
}, [start, end]);
const dateTitle = useMemo(() => {
const isRelativeTime = relativeTime && relativeTime !== "none";
return isRelativeTime ? relativeTime.replace(/_/g, " ") : `${formatRange.start} - ${formatRange.end}`;
}, [relativeTime, formatRange]);
const fromRef = useRef<HTMLDivElement>(null);
const untilRef = useRef<HTMLDivElement>(null);
const fromPickerRef = useRef<HTMLDivElement>(null);
const untilPickerRef = useRef<HTMLDivElement>(null);
const [openOptions, setOpenOptions] = useState(false);
const buttonRef = useRef<HTMLDivElement>(null);
const setTimeAndClosePicker = () => {
if (from && until) {
dispatch({ type: "SET_PERIOD", payload: { from: new Date(from), to: new Date(until) } });
}
setOpenOptions(false);
};
const handleFromChange = (from: string) => setFrom(from);
const handleUntilChange = (until: string) => setUntil(until);
const onApplyClick = () => setTimeAndClosePicker();
const onSwitchToNow = () => dispatch({ type: "RUN_QUERY_TO_NOW" });
const onCancelClick = () => {
setUntil(formatDateForNativeInput(dateFromSeconds(end)));
setFrom(formatDateForNativeInput(dateFromSeconds(start)));
setOpenOptions(false);
};
const toggleOpenOptions = () => {
setOpenOptions(prev => !prev);
};
const handleCloseOptions = () => {
setOpenOptions(false);
};
useClickOutside(wrapperRef, (e) => {
const target = e.target as HTMLElement;
const isFromButton = fromRef?.current && fromRef.current.contains(target);
const isUntilButton = untilRef?.current && untilRef.current.contains(target);
const isFromPicker = fromPickerRef?.current && fromPickerRef?.current?.contains(target);
const isUntilPicker = untilPickerRef?.current && untilPickerRef?.current?.contains(target);
if (isFromButton || isUntilButton || isFromPicker || isUntilPicker) return;
handleCloseOptions();
});
return <>
<div ref={buttonRef}>
<Tooltip title="Time range controls">
<Button
className={appModeEnable ? "" : "vm-header-button"}
variant="contained"
color="primary"
startIcon={<ClockIcon/>}
onClick={toggleOpenOptions}
>
{displayFullDate && <span>{dateTitle}</span>}
</Button>
</Tooltip>
</div>
<Popper
open={openOptions}
buttonRef={buttonRef}
placement="bottom-right"
onClose={handleCloseOptions}
clickOutside={false}
>
<div
className="vm-time-selector"
ref={wrapperRef}
>
<div className="vm-time-selector-left">
<div className="vm-time-selector-left-inputs">
<div
className="vm-time-selector-left-inputs__date"
ref={fromRef}
>
<label>From:</label>
<span>{formFormat}</span>
<CalendarIcon/>
<DatePicker
ref={fromPickerRef}
date={from || ""}
onChange={handleFromChange}
targetRef={fromRef}
timepicker={true}
/>
</div>
<div
className="vm-time-selector-left-inputs__date"
ref={untilRef}
>
<label>To:</label>
<span>{untilFormat}</span>
<CalendarIcon/>
<DatePicker
ref={untilPickerRef}
date={until || ""}
onChange={handleUntilChange}
targetRef={untilRef}
timepicker={true}
/>
</div>
</div>
<Button
variant="text"
startIcon={<AlarmIcon />}
onClick={onSwitchToNow}
>
switch to now
</Button>
<div className="vm-time-selector-left__controls">
<Button
color="error"
variant="outlined"
onClick={onCancelClick}
>
Cancel
</Button>
<Button
color="primary"
onClick={onApplyClick}
>
Apply
</Button>
</div>
</div>
<TimeDurationSelector
relativeTime={relativeTime || ""}
setDuration={setDuration}
/>
</div>
</Popper>
</>;
};

View File

@@ -0,0 +1,61 @@
@use "src/styles/variables" as *;
.vm-time-selector {
display: grid;
grid-template-columns: repeat(2, 230px);
padding: $padding-global 0;
&-left {
display: flex;
flex-direction: column;
gap: $padding-small;
border-right: $border-divider;
padding: 0 $padding-global;
&-inputs {
flex-grow: 1;
display: grid;
align-items: flex-start;
justify-content: stretch;
&__date {
display: grid;
grid-template-columns: 1fr 14px;
gap: $padding-small;
align-items: center;
justify-content: center;
padding-bottom: $padding-small;
margin-bottom: $padding-global;
border-bottom: $border-divider;
cursor: pointer;
transition: color 200ms ease-in-out, border-bottom-color 300ms ease;
&:hover {
border-bottom-color: $color-primary;
}
&:hover svg,
&:hover {
color: $color-primary;
}
label {
grid-column: 1/3;
font-size: $font-size-small;
color: $color-text-secondary;
}
svg {
color: $color-text-secondary;
transition: color 200ms ease-in-out;
}
}
}
&__controls {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: $padding-small;
}
}
}

View File

@@ -1,205 +0,0 @@
/* eslint max-lines: ["error", {"max": 300}] */
import React, {useState} from "preact/compat";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import Checkbox from "@mui/material/Checkbox";
import DialogActions from "@mui/material/DialogActions";
import DialogContent from "@mui/material/DialogContent";
import DialogContentText from "@mui/material/DialogContentText";
import FormControl from "@mui/material/FormControl";
import FormControlLabel from "@mui/material/FormControlLabel";
import FormHelperText from "@mui/material/FormHelperText";
import Input from "@mui/material/Input";
import InputAdornment from "@mui/material/InputAdornment";
import InputLabel from "@mui/material/InputLabel";
import Tab from "@mui/material/Tab";
import Tabs from "@mui/material/Tabs";
import TextField from "@mui/material/TextField";
import Typography from "@mui/material/Typography";
import DialogTitle from "@mui/material/DialogTitle";
import Dialog from "@mui/material/Dialog";
import TabPanel from "./AuthTabPanel";
import PersonIcon from "@mui/icons-material/Person";
import LockIcon from "@mui/icons-material/Lock";
import {useAuthDispatch, useAuthState} from "../../../../state/auth/AuthStateContext";
import {AUTH_METHOD, WithCheckbox} from "../../../../state/auth/reducer";
import {ChangeEvent, ClipboardEvent} from "react";
// TODO: make generic when creating second dialog
export interface DialogProps {
open: boolean;
onClose: () => void;
}
export interface AuthTab {
title: string;
id: AUTH_METHOD;
}
const BEARER_PREFIX = "Bearer ";
const tabs: AuthTab[] = [
{title: "No auth", id: "NO_AUTH"},
{title: "Basic Auth", id: "BASIC_AUTH"},
{title: "Bearer Token", id: "BEARER_AUTH"}
];
export const AuthDialog: React.FC<DialogProps> = (props) => {
const {onClose, open} = props;
const {saveAuthLocally, basicData, bearerData, authMethod} = useAuthState();
const dispatch = useAuthDispatch();
const [authCheckbox, setAuthCheckbox] = useState(saveAuthLocally);
const [basicValue, setBasicValue] = useState(basicData || {password: "", login: ""});
const [bearerValue, setBearerValue] = useState(bearerData?.token || BEARER_PREFIX);
const [tabIndex, setTabIndex] = useState(tabs.findIndex(el => el.id === authMethod) || 0);
const handleChange = (event: unknown, newValue: number) => {
setTabIndex(newValue);
};
const handleBearerChange = (event: ChangeEvent<HTMLInputElement>) => {
const newVal = event.target.value;
if (newVal.startsWith(BEARER_PREFIX)) {
setBearerValue(newVal);
} else {
setBearerValue(BEARER_PREFIX);
}
};
const handleClose = () => {
onClose();
};
const onBearerPaste = (e: ClipboardEvent) => {
// if you're pasting token word Bearer will be added automagically
const newVal = e.clipboardData.getData("text/plain");
if (newVal.startsWith(BEARER_PREFIX)) {
setBearerValue(newVal);
} else {
setBearerValue(BEARER_PREFIX + newVal);
}
e.preventDefault();
};
const handleApply = () => {
// TODO: handle validation/required fields
switch (tabIndex) {
case 0:
dispatch({type: "SET_NO_AUTH", payload: {checkbox: authCheckbox} as WithCheckbox});
break;
case 1:
dispatch({type: "SET_BASIC_AUTH", payload: { checkbox: authCheckbox, value: basicValue}});
break;
case 2:
dispatch({type: "SET_BEARER_AUTH", payload: {checkbox: authCheckbox, value: {token: bearerValue}}});
break;
}
handleClose();
};
return (
<Dialog onClose={handleClose} aria-labelledby="simple-dialog-title" open={open}>
<DialogTitle id="simple-dialog-title">Request Auth Settings</DialogTitle>
<DialogContent>
<DialogContentText>
This affects Authorization header sent to the server you specify. Not shown in URL and can be optionally stored on a client side
</DialogContentText>
<Tabs
value={tabIndex}
onChange={handleChange}
indicatorColor="primary"
textColor="primary"
>
{
tabs.map(t => <Tab key={t.id} label={t.title} />)
}
</Tabs>
<Box p={0} display="flex" flexDirection="column" sx={{height: "200px"}}>
<Box flexGrow={1}>
<TabPanel value={tabIndex} index={0}>
<Typography style={{fontStyle: "italic"}}>
No Authorization Header
</Typography>
</TabPanel>
<TabPanel value={tabIndex} index={1}>
<FormControl margin="dense" fullWidth={true}>
<InputLabel htmlFor="basic-login">User</InputLabel>
<Input
id="basic-login"
startAdornment={
<InputAdornment position="start">
<PersonIcon />
</InputAdornment>
}
required
onChange={e => setBasicValue(prev => ({...prev, login: e.target.value || ""}))}
value={basicValue?.login || ""}
/>
</FormControl>
<FormControl margin="dense" fullWidth={true}>
<InputLabel htmlFor="basic-pass">Password</InputLabel>
<Input
id="basic-pass"
// type="password" // Basic auth is not super secure in any case :)
startAdornment={
<InputAdornment position="start">
<LockIcon />
</InputAdornment>
}
onChange={e => setBasicValue(prev => ({...prev, password: e.target.value || ""}))}
value={basicValue?.password || ""}
/>
</FormControl>
</TabPanel>
<TabPanel value={tabIndex} index={2}>
<TextField
id="bearer-auth"
label="Bearer token"
multiline
fullWidth={true}
value={bearerValue}
onChange={handleBearerChange}
InputProps={{
onPaste: onBearerPaste
}}
maxRows={6}
/>
</TabPanel>
</Box>
<FormControl>
<FormControlLabel
control={
<Checkbox
checked={authCheckbox}
onChange={() => setAuthCheckbox(prev => !prev)}
name="checkedB"
color="primary"
/>
}
label="Persist Auth Data Locally"
/>
<FormHelperText>
{authCheckbox ? "Auth Data and the Selected method will be saved to LocalStorage" : "Auth Data won't be saved. All previously saved Auth Data will be removed"}
</FormHelperText>
</FormControl>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={handleApply} color="primary">
Apply
</Button>
</DialogActions>
</Dialog>
);
};

View File

@@ -1,31 +0,0 @@
import React from "preact/compat";
import Box from "@mui/material/Box";
import {ReactNode} from "react";
interface TabPanelProps {
children?: ReactNode;
index: number;
value: number;
}
const AuthTabPanel: React.FC<TabPanelProps> = (props) => {
const { children, value, index, ...other } = props;
return (
<div
role="tabpanel"
hidden={value !== index}
id={`auth-config-tabpanel-${index}`}
aria-labelledby={`auth-config-tab-${index}`}
{...other}
>
{value === index && (
<Box py={2}>
{children}
</Box>
)}
</div>
);
};
export default AuthTabPanel;

View File

@@ -1,40 +0,0 @@
import React, {FC} from "preact/compat";
import TableChartIcon from "@mui/icons-material/TableChart";
import ShowChartIcon from "@mui/icons-material/ShowChart";
import CodeIcon from "@mui/icons-material/Code";
import Tabs from "@mui/material/Tabs";
import Tab from "@mui/material/Tab";
import {useAppDispatch, useAppState} from "../../../state/common/StateContext";
import {SyntheticEvent} from "react";
export type DisplayType = "table" | "chart" | "code";
export const displayTypeTabs = [
{value: "chart", icon: <ShowChartIcon/>, label: "Graph", prometheusCode: 0},
{value: "code", icon: <CodeIcon/>, label: "JSON"},
{value: "table", icon: <TableChartIcon/>, label: "Table", prometheusCode: 1}
];
export const DisplayTypeSwitch: FC = () => {
const {displayType} = useAppState();
const dispatch = useAppDispatch();
const handleChange = (event: SyntheticEvent, newValue: DisplayType) => {
dispatch({type: "SET_DISPLAY_TYPE", payload: newValue ?? displayType});
};
return <Tabs
value={displayType}
onChange={handleChange}
sx={{minHeight: "0", marginBottom: "-1px"}}
>
{displayTypeTabs.map(t =>
<Tab key={t.value}
icon={t.icon}
iconPosition="start"
label={t.label} value={t.value}
sx={{minHeight: "41px"}}
/>)}
</Tabs>;
};

View File

@@ -1,48 +0,0 @@
import React, {FC, useCallback, useMemo} from "preact/compat";
import {ChangeEvent} from "react";
import Box from "@mui/material/Box";
import FormControlLabel from "@mui/material/FormControlLabel";
import TextField from "@mui/material/TextField";
import debounce from "lodash.debounce";
import BasicSwitch from "../../../../theme/switch";
import {AxisRange, YaxisState} from "../../../../state/graph/reducer";
interface AxesLimitsConfiguratorProps {
yaxis: YaxisState,
setYaxisLimits: (limits: AxisRange) => void,
toggleEnableLimits: () => void
}
const AxesLimitsConfigurator: FC<AxesLimitsConfiguratorProps> = ({yaxis, setYaxisLimits, toggleEnableLimits}) => {
const axes = useMemo(() => Object.keys(yaxis.limits.range), [yaxis.limits.range]);
const onChangeLimit = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>, axis: string, index: number) => {
const newLimits = yaxis.limits.range;
newLimits[axis][index] = +e.target.value;
if (newLimits[axis][0] === newLimits[axis][1] || newLimits[axis][0] > newLimits[axis][1]) return;
setYaxisLimits(newLimits);
};
const debouncedOnChangeLimit = useCallback(debounce(onChangeLimit, 500), [yaxis.limits.range]);
return <Box display="grid" alignItems="center" gap={2}>
<FormControlLabel
control={<BasicSwitch checked={yaxis.limits.enable} onChange={toggleEnableLimits}/>}
label="Fix the limits for y-axis"
/>
<Box display="grid" alignItems="center" gap={2}>
{axes.map(axis => <Box display="grid" gridTemplateColumns="120px 120px" gap={1} key={axis}>
<TextField label={`Min ${axis}`} type="number" size="small" variant="outlined"
disabled={!yaxis.limits.enable}
defaultValue={yaxis.limits.range[axis][0]}
onChange={(e) => debouncedOnChangeLimit(e, axis, 0)}/>
<TextField label={`Max ${axis}`} type="number" size="small" variant="outlined"
disabled={!yaxis.limits.enable}
defaultValue={yaxis.limits.range[axis][1]}
onChange={(e) => debouncedOnChangeLimit(e, axis, 1)} />
</Box>)}
</Box>
</Box>;
};
export default AxesLimitsConfigurator;

View File

@@ -1,80 +0,0 @@
import SettingsIcon from "@mui/icons-material/Settings";
import React, {FC, useState} from "preact/compat";
import AxesLimitsConfigurator from "./AxesLimitsConfigurator";
import Box from "@mui/material/Box";
import IconButton from "@mui/material/IconButton";
import Paper from "@mui/material/Paper";
import Popper from "@mui/material/Popper";
import Tooltip from "@mui/material/Tooltip";
import Typography from "@mui/material/Typography";
import CloseIcon from "@mui/icons-material/Close";
import ClickAwayListener from "@mui/material/ClickAwayListener";
import {AxisRange, YaxisState} from "../../../../state/graph/reducer";
const classes = {
popover: {
display: "grid",
gridGap: "16px",
padding: "0 0 25px",
},
popoverHeader: {
display: "flex",
alignItems: "center",
justifyContent: "space-between",
backgroundColor: "primary.main",
padding: "6px 6px 6px 12px",
borderRadius: "4px 4px 0 0",
color: "primary.contrastText",
},
popoverBody: {
display: "grid",
gridGap: "6px",
padding: "0 14px",
}
};
const title = "Axes Settings";
interface GraphSettingsProps {
yaxis: YaxisState,
setYaxisLimits: (limits: AxisRange) => void,
toggleEnableLimits: () => void
}
const GraphSettings: FC<GraphSettingsProps> = ({yaxis, setYaxisLimits, toggleEnableLimits}) => {
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
const open = Boolean(anchorEl);
return <Box>
<Tooltip title={title}>
<IconButton onClick={(e) => setAnchorEl(e.currentTarget)}>
<SettingsIcon/>
</IconButton>
</Tooltip>
<Popper
open={open}
anchorEl={anchorEl}
placement="left-start"
modifiers={[{name: "offset", options: {offset: [0, 6]}}]}>
<ClickAwayListener onClickAway={() => setAnchorEl(null)}>
<Paper elevation={3} sx={classes.popover}>
<Box id="handle" sx={classes.popoverHeader}>
<Typography variant="body1"><b>{title}</b></Typography>
<IconButton size="small" onClick={() => setAnchorEl(null)}>
<CloseIcon style={{color: "white"}}/>
</IconButton>
</Box>
<Box sx={classes.popoverBody}>
<AxesLimitsConfigurator
yaxis={yaxis}
setYaxisLimits={setYaxisLimits}
toggleEnableLimits={toggleEnableLimits}
/>
</Box>
</Paper>
</ClickAwayListener>
</Popper>
</Box>;
};
export default GraphSettings;

View File

@@ -1,62 +0,0 @@
import React, {FC} from "preact/compat";
import Box from "@mui/material/Box";
import FormControlLabel from "@mui/material/FormControlLabel";
import {saveToStorage} from "../../../../utils/storage";
import {useAppDispatch, useAppState} from "../../../../state/common/StateContext";
import BasicSwitch from "../../../../theme/switch";
import StepConfigurator from "./StepConfigurator";
import {useGraphDispatch} from "../../../../state/graph/GraphStateContext";
import {getAppModeParams} from "../../../../utils/app-mode";
import TenantsConfiguration from "../Settings/TenantsConfiguration";
const AdditionalSettings: FC = () => {
const graphDispatch = useGraphDispatch();
const {inputTenantID} = getAppModeParams();
const {queryControls: {autocomplete, nocache, isTracingEnabled}, time: {period: {step}}} = useAppState();
const dispatch = useAppDispatch();
const onChangeAutocomplete = () => {
dispatch({type: "TOGGLE_AUTOCOMPLETE"});
saveToStorage("AUTOCOMPLETE", !autocomplete);
};
const onChangeCache = () => {
dispatch({type: "NO_CACHE"});
saveToStorage("NO_CACHE", !nocache);
};
const onChangeQueryTracing = () => {
dispatch({type: "TOGGLE_QUERY_TRACING"});
saveToStorage("QUERY_TRACING", !isTracingEnabled);
};
return <Box display="flex" alignItems="center" flexWrap="wrap" gap={2}>
<Box>
<FormControlLabel label="Autocomplete" sx={{m: 0}}
control={<BasicSwitch checked={autocomplete} onChange={onChangeAutocomplete}/>}
/>
</Box>
<Box>
<FormControlLabel label="Disable cache" sx={{m: 0}}
control={<BasicSwitch checked={nocache} onChange={onChangeCache}/>}
/>
</Box>
<Box>
<FormControlLabel label="Trace query" sx={{m: 0}}
control={<BasicSwitch checked={isTracingEnabled} onChange={onChangeQueryTracing} />}
/>
</Box>
<Box ml={2}>
<StepConfigurator defaultStep={step}
setStep={(value) => {
graphDispatch({type: "SET_CUSTOM_STEP", payload: value});
}}
/>
</Box>
{!!inputTenantID && <Box ml={2}><TenantsConfiguration/></Box>}
</Box>;
};
export default AdditionalSettings;

View File

@@ -1,110 +0,0 @@
import React, {FC, useState, useEffect} from "preact/compat";
import Box from "@mui/material/Box";
import IconButton from "@mui/material/IconButton";
import Tooltip from "@mui/material/Tooltip";
import QueryEditor from "./QueryEditor";
import {useAppDispatch, useAppState} from "../../../../state/common/StateContext";
import DeleteIcon from "@mui/icons-material/Delete";
import AddIcon from "@mui/icons-material/Add";
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
import AdditionalSettings from "./AdditionalSettings";
import {ErrorTypes} from "../../../../types";
import Button from "@mui/material/Button";
import Typography from "@mui/material/Typography";
import usePrevious from "../../../../hooks/usePrevious";
import {MAX_QUERY_FIELDS} from "../../../../config";
export interface QueryConfiguratorProps {
error?: ErrorTypes | string;
queryOptions: string[]
}
const QueryConfigurator: FC<QueryConfiguratorProps> = ({error, queryOptions}) => {
const {query, queryHistory, queryControls: {autocomplete}} = useAppState();
const [stateQuery, setStateQuery] = useState(query || []);
const prevStateQuery = usePrevious(stateQuery) as (undefined | string[]);
const dispatch = useAppDispatch();
const updateHistory = () => {
dispatch({
type: "SET_QUERY_HISTORY", payload: stateQuery.map((q, i) => {
const h = queryHistory[i] || {values: []};
const queryEqual = q === h.values[h.values.length - 1];
return {
index: h.values.length - Number(queryEqual),
values: !queryEqual && q ? [...h.values, q] : h.values
};
})
});
};
const onRunQuery = () => {
updateHistory();
dispatch({type: "SET_QUERY", payload: stateQuery});
dispatch({type: "RUN_QUERY"});
};
const onAddQuery = () => {
setStateQuery(prev => [...prev, ""]);
};
const onRemoveQuery = (index: number) => {
setStateQuery(prev => prev.filter((q, i) => i !== index));
};
const onSetQuery = (value: string, index: number) => {
setStateQuery(prev => prev.map((q, i) => i === index ? value : q));
};
const setHistoryIndex = (step: number, indexQuery: number) => {
const {index, values} = queryHistory[indexQuery];
const newIndexHistory = index + step;
if (newIndexHistory < 0 || newIndexHistory >= values.length) return;
onSetQuery(values[newIndexHistory] || "", indexQuery);
dispatch({
type: "SET_QUERY_HISTORY_BY_INDEX",
payload: {value: {values, index: newIndexHistory}, queryNumber: indexQuery}
});
};
useEffect(() => {
if (prevStateQuery && (stateQuery.length < prevStateQuery.filter(q => q).length)) {
onRunQuery();
}
}, [stateQuery]);
return <Box>
<Box>
{stateQuery.map((q, i) =>
<Box key={i} display="grid" gridTemplateColumns="1fr auto" gap="4px" width="100%" position="relative"
mb={i === stateQuery.length - 1 ? 0 : 2}>
<QueryEditor
query={stateQuery[i]} index={i} autocomplete={autocomplete} queryOptions={queryOptions}
error={error} setHistoryIndex={setHistoryIndex} runQuery={onRunQuery} setQuery={onSetQuery}
label={`Query ${i + 1}`} size={"small"}/>
{stateQuery.length > 1 && <Tooltip title="Remove Query">
<IconButton onClick={() => onRemoveQuery(i)} sx={{height: "33px", width: "33px", padding: 0}} color={"error"}>
<DeleteIcon fontSize={"small"}/>
</IconButton>
</Tooltip>}
</Box>)}
</Box>
<Box mt={3} display="grid" gridTemplateColumns="1fr auto" alignItems="start" gap={4}>
<AdditionalSettings/>
<Box display="grid" gridTemplateColumns="repeat(2, auto)" gap={1}>
{stateQuery.length < MAX_QUERY_FIELDS && (
<Button variant="outlined" onClick={onAddQuery} startIcon={<AddIcon/>}>
<Typography lineHeight={"20px"} fontWeight="500">Add Query</Typography>
</Button>
)}
<Button variant="contained" onClick={onRunQuery} startIcon={<PlayArrowIcon/>}>
<Typography lineHeight={"20px"} fontWeight="500">Execute Query</Typography>
</Button>
</Box>
</Box>
</Box>;
};
export default QueryConfigurator;

View File

@@ -1,142 +0,0 @@
import React, {FC, useEffect, useMemo, useRef, useState} from "preact/compat";
import {KeyboardEvent} from "react";
import {ErrorTypes} from "../../../../types";
import Popper from "@mui/material/Popper";
import TextField from "@mui/material/TextField";
import Box from "@mui/material/Box";
import Paper from "@mui/material/Paper";
import MenuItem from "@mui/material/MenuItem";
import MenuList from "@mui/material/MenuList";
import ClickAwayListener from "@mui/material/ClickAwayListener";
export interface QueryEditorProps {
setHistoryIndex: (step: number, index: number) => void;
setQuery: (query: string, index: number) => void;
runQuery: () => void;
query: string;
index: number;
oneLiner?: boolean;
autocomplete: boolean;
error?: ErrorTypes | string;
queryOptions: string[];
label: string;
size?: "small" | "medium" | undefined;
}
const QueryEditor: FC<QueryEditorProps> = ({
index,
query,
setHistoryIndex,
setQuery,
runQuery,
autocomplete,
error,
queryOptions,
label,
size = "medium"
}) => {
const [focusField, setFocusField] = useState(false);
const [focusOption, setFocusOption] = useState(-1);
const autocompleteAnchorEl = useRef<HTMLDivElement>(null);
const wrapperEl = useRef<HTMLUListElement>(null);
const [openAutocomplete, setOpenAutocomplete] = useState(false);
useEffect(() => {
if (!focusField) return;
const words = (query.match(/[a-zA-Z_:.][a-zA-Z0-9_:.]*/gm) || []).length;
setOpenAutocomplete(!(!autocomplete || query.length < 2 || words > 1));
},
[autocomplete, query]);
const actualOptions = useMemo(() => {
setFocusOption(0);
if (!openAutocomplete) return [];
try {
const regexp = new RegExp(String(query), "i");
const options = queryOptions.filter((item) => regexp.test(item) && (item !== query));
return options.sort((a,b) => (a.match(regexp)?.index || 0) - (b.match(regexp)?.index || 0));
} catch (e) {
return [];
}
}, [autocomplete, query, queryOptions]);
const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {
const {key, ctrlKey, metaKey, shiftKey} = e;
const ctrlMetaKey = ctrlKey || metaKey;
const arrowUp = key === "ArrowUp";
const arrowDown = key === "ArrowDown";
const enter = key === "Enter";
const hasAutocomplete = openAutocomplete && actualOptions.length;
if (((arrowUp || arrowDown) && (hasAutocomplete || ctrlMetaKey)) || (enter && (hasAutocomplete || ctrlMetaKey || !shiftKey))) {
e.preventDefault();
}
// ArrowUp
if (arrowUp && hasAutocomplete && !ctrlMetaKey) {
setFocusOption((prev) => prev === 0 ? 0 : prev - 1);
} else if (arrowUp && ctrlMetaKey) {
setHistoryIndex(-1, index);
}
// ArrowDown
if (arrowDown && hasAutocomplete && !ctrlMetaKey) {
setFocusOption((prev) => prev >= actualOptions.length - 1 ? actualOptions.length - 1 : prev + 1);
} else if (arrowDown && ctrlMetaKey) {
setHistoryIndex(1, index);
}
// Enter
if (enter && hasAutocomplete && !shiftKey && !ctrlMetaKey) {
setQuery(actualOptions[focusOption], index);
} else if (enter && !shiftKey) {
runQuery();
}
};
useEffect(() => {
if (!wrapperEl.current) return;
const target = wrapperEl.current.childNodes[focusOption] as HTMLElement;
if (target?.scrollIntoView) target.scrollIntoView({block: "center"});
}, [focusOption]);
return <Box ref={autocompleteAnchorEl}>
<TextField
defaultValue={query}
fullWidth
label={label}
multiline
focused={!!query}
error={!!error}
onFocus={() => setFocusField(true)}
onKeyDown={handleKeyDown}
onChange={(e) => setQuery(e.target.value, index)}
size={size}
/>
<Popper open={openAutocomplete} anchorEl={autocompleteAnchorEl.current} placement="bottom-start" sx={{zIndex: 3}}>
<ClickAwayListener onClickAway={() => setOpenAutocomplete(false)}>
<Paper elevation={3} sx={{ maxHeight: 300, overflow: "auto" }}>
<MenuList ref={wrapperEl} dense>
{actualOptions.map((item, i) =>
<MenuItem
id={`$autocomplete$${item}`}
key={item}
sx={{bgcolor: `rgba(0, 0, 0, ${i === focusOption ? 0.12 : 0})`}}
onClick={() => {
setQuery(item, index);
setOpenAutocomplete(false);
}}
>
{item}
</MenuItem>)}
</MenuList>
</Paper>
</ClickAwayListener>
</Popper>
</Box>;
};
export default QueryEditor;

View File

@@ -1,67 +0,0 @@
import React, {FC, useCallback, useState} from "preact/compat";
import {ChangeEvent, useEffect} from "react";
import TextField from "@mui/material/TextField";
import debounce from "lodash.debounce";
import InputAdornment from "@mui/material/InputAdornment";
import Tooltip from "@mui/material/Tooltip";
import RestartAltIcon from "@mui/icons-material/RestartAlt";
import IconButton from "@mui/material/IconButton";
interface StepConfiguratorProps {
defaultStep?: number,
setStep: (step: number) => void,
}
const StepConfigurator: FC<StepConfiguratorProps> = ({defaultStep, setStep}) => {
const [customStep, setCustomStep] = useState(defaultStep);
const [error, setError] = useState(false);
const handleApply = (step: number) => setStep(step || 1);
const debouncedHandleApply = useCallback(debounce(handleApply, 700), []);
const onChangeStep = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const value = +e.target.value;
if (!value) return;
handleSetStep(value);
};
const handleSetStep = (value: number) => {
if (value > 0) {
setCustomStep(value);
debouncedHandleApply(value);
setError(false);
} else {
setError(true);
}
};
useEffect(() => {
if (defaultStep) handleSetStep(defaultStep);
}, [defaultStep]);
return <TextField
label="Step value"
type="number"
size="small"
variant="outlined"
value={customStep}
error={error}
helperText={error ? "step is out of allowed range" : " "}
onChange={onChangeStep}
InputProps={{
inputProps: {min: 0},
endAdornment: (
<InputAdornment position="start" sx={{mr: -0.5, cursor: "pointer"}}>
<Tooltip title={"Reset step to default"}>
<IconButton size={"small"} onClick={() => handleSetStep(defaultStep || 1)}>
<RestartAltIcon fontSize={"small"} />
</IconButton>
</Tooltip>
</InputAdornment>
),
}}
/>;
};
export default StepConfigurator;

View File

@@ -1,80 +0,0 @@
import React, {FC, useState} from "preact/compat";
import Tooltip from "@mui/material/Tooltip";
import SettingsIcon from "@mui/icons-material/Settings";
import Button from "@mui/material/Button";
import Box from "@mui/material/Box";
import Modal from "@mui/material/Modal";
import ServerConfigurator from "./ServerConfigurator";
import Typography from "@mui/material/Typography";
import CloseIcon from "@mui/icons-material/Close";
import IconButton from "@mui/material/IconButton";
import {useAppDispatch, useAppState} from "../../../../state/common/StateContext";
const modalStyle = {
position: "absolute" as const,
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
bgcolor: "background.paper",
p: 3,
borderRadius: "4px",
width: "80%",
maxWidth: "800px"
};
const title = "Setting Server URL";
const GlobalSettings: FC = () => {
const {serverUrl} = useAppState();
const dispatch = useAppDispatch();
const [changedServerUrl, setChangedServerUrl] = useState(serverUrl);
const setServer = (url?: string) => {
dispatch({type: "SET_SERVER", payload: url || changedServerUrl});
handleClose();
};
const [open, setOpen] = useState(false);
const handleOpen = () => setOpen(true);
const handleClose = () => setOpen(false);
return <>
<Tooltip title={title}>
<Button variant="contained" color="primary"
sx={{
color: "white",
border: "1px solid rgba(0, 0, 0, 0.2)",
minWidth: "34px",
padding: "6px 8px",
boxShadow: "none",
}}
startIcon={<SettingsIcon style={{marginRight: "-8px", marginLeft: "4px"}}/>}
onClick={handleOpen}>
</Button>
</Tooltip>
<Modal open={open} onClose={handleClose}>
<Box sx={modalStyle}>
<Box display="grid" gridTemplateColumns="1fr auto" alignItems="center" mb={4}>
<Typography id="modal-modal-title" variant="h6" component="h2">
{title}
</Typography>
<IconButton size="small" onClick={handleClose}>
<CloseIcon/>
</IconButton>
</Box>
<ServerConfigurator setServer={setChangedServerUrl} onEnter={setServer}/>
<Box display="grid" gridTemplateColumns="auto auto" gap={1} justifyContent="end" mt={4}>
<Button variant="outlined" onClick={handleClose}>
Cancel
</Button>
<Button variant="contained" onClick={() => setServer()}>
apply
</Button>
</Box>
</Box>
</Modal>
</>;
};
export default GlobalSettings;

View File

@@ -1,44 +0,0 @@
import React, {FC, useState} from "preact/compat";
import TextField from "@mui/material/TextField";
import {useAppState} from "../../../../state/common/StateContext";
import {ErrorTypes} from "../../../../types";
import {ChangeEvent, KeyboardEvent} from "react";
export interface ServerConfiguratorProps {
error?: ErrorTypes | string;
setServer: (url: string) => void
onEnter: (url: string) => void
}
const ServerConfigurator: FC<ServerConfiguratorProps> = ({error, setServer, onEnter}) => {
const {serverUrl} = useAppState();
const [changedServerUrl, setChangedServerUrl] = useState(serverUrl);
const onChangeServer = (e: ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
const value = e.target.value || "";
setChangedServerUrl(value);
setServer(value);
};
const onKeyDown = (e: KeyboardEvent) => {
if (e.key === "Enter") {
e.preventDefault();
onEnter(changedServerUrl);
}
};
return <TextField
autoFocus
fullWidth
variant="outlined"
label="Server URL"
value={changedServerUrl || ""}
error={error === ErrorTypes.validServer || error === ErrorTypes.emptyServer}
inputProps={{style: {fontFamily: "Monospace"}}}
onChange={onChangeServer}
onKeyDown={onKeyDown}
/>;
};
export default ServerConfigurator;

View File

@@ -1,60 +0,0 @@
import React, {FC, useState, useEffect, useCallback} from "preact/compat";
import TextField from "@mui/material/TextField";
import InputAdornment from "@mui/material/InputAdornment";
import Tooltip from "@mui/material/Tooltip";
import InfoIcon from "@mui/icons-material/Info";
import {useAppDispatch, useAppState} from "../../../../state/common/StateContext";
import {ChangeEvent} from "react";
import debounce from "lodash.debounce";
import {getAppModeParams} from "../../../../utils/app-mode";
const TenantsConfiguration: FC = () => {
const {serverURL} = getAppModeParams();
const {tenantId: tenantIdState} = useAppState();
const dispatch = useAppDispatch();
const [tenantId, setTenantId] = useState<string | number>(tenantIdState || 0);
const handleApply = (value: string | number) => {
const tenantId = Number(value);
dispatch({type: "SET_TENANT_ID", payload: tenantId});
if (serverURL) {
const updateServerUrl = serverURL.replace(/(\/select\/)([\d]+)(\/prometheus)/gmi, `$1${tenantId}$3`);
dispatch({type: "SET_SERVER", payload: updateServerUrl});
dispatch({type: "RUN_QUERY"});
}
};
const debouncedHandleApply = useCallback(debounce(handleApply, 700), []);
const handleChange = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
setTenantId(e.target.value);
debouncedHandleApply(e.target.value);
};
useEffect(() => {
if (tenantId === tenantIdState) return;
setTenantId(tenantIdState);
}, [tenantIdState]);
return <TextField
label="Tenant ID"
type="number"
size="small"
variant="outlined"
value={tenantId}
onChange={handleChange}
InputProps={{
inputProps: {min: 0},
startAdornment: (
<InputAdornment position="start">
<Tooltip title={"Define tenant id if you need request to another storage"}>
<InfoIcon fontSize={"small"} />
</Tooltip>
</InputAdornment>
),
}}
/>;
};
export default TenantsConfiguration;

View File

@@ -1,125 +0,0 @@
import React, {FC, useEffect, useState} from "preact/compat";
import Tooltip from "@mui/material/Tooltip";
import {useAppDispatch, useAppState} from "../../../../state/common/StateContext";
import Button from "@mui/material/Button";
import Popper from "@mui/material/Popper";
import Paper from "@mui/material/Paper";
import ClickAwayListener from "@mui/material/ClickAwayListener";
import AutorenewIcon from "@mui/icons-material/Autorenew";
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
import List from "@mui/material/List";
import ListItem from "@mui/material/ListItem";
import ListItemText from "@mui/material/ListItemText";
import {useLocation} from "react-router-dom";
import {getAppModeEnable} from "../../../../utils/app-mode";
import Box from "@mui/material/Box";
interface AutoRefreshOption {
seconds: number
title: string
}
const delayOptions: AutoRefreshOption[] = [
{seconds: 0, title: "Off"},
{seconds: 1, title: "1s"},
{seconds: 2, title: "2s"},
{seconds: 5, title: "5s"},
{seconds: 10, title: "10s"},
{seconds: 30, title: "30s"},
{seconds: 60, title: "1m"},
{seconds: 300, title: "5m"},
{seconds: 900, title: "15m"},
{seconds: 1800, title: "30m"},
{seconds: 3600, title: "1h"},
{seconds: 7200, title: "2h"}
];
export const ExecutionControls: FC = () => {
const dispatch = useAppDispatch();
const appModeEnable = getAppModeEnable();
const {queryControls: {autoRefresh}} = useAppState();
const location = useLocation();
useEffect(() => {
if (autoRefresh) dispatch({type: "TOGGLE_AUTOREFRESH"});
}, [location]);
const [selectedDelay, setSelectedDelay] = useState<AutoRefreshOption>(delayOptions[0]);
const handleChange = (d: AutoRefreshOption) => {
if ((autoRefresh && !d.seconds) || (!autoRefresh && d.seconds)) {
dispatch({type: "TOGGLE_AUTOREFRESH"});
}
setSelectedDelay(d);
setAnchorEl(null);
};
const handleUpdate = () => {
dispatch({type: "RUN_QUERY"});
};
useEffect(() => {
const delay = selectedDelay.seconds;
let timer: number;
if (autoRefresh) {
timer = setInterval(() => {
dispatch({type: "RUN_QUERY"});
}, delay * 1000) as unknown as number;
} else {
setSelectedDelay(delayOptions[0]);
}
return () => {
timer && clearInterval(timer);
};
}, [selectedDelay, autoRefresh]);
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
const open = Boolean(anchorEl);
return <>
<Box sx={{
minWidth: "110px",
color: "white",
border: appModeEnable ? "none" : "1px solid rgba(0, 0, 0, 0.2)",
justifyContent: "space-between",
boxShadow: "none",
borderRadius: "4px",
display: "grid",
gridTemplateColumns: "auto 1fr"
}}>
<Tooltip title="Refresh dashboard">
<Button variant="contained" color="primary"
sx={{color: "white", minWidth: "34px", boxShadow: "none", borderRadius: "3px 0 0 3px", p: "6px 6px"}}
startIcon={<AutorenewIcon fontSize={"small"} style={{marginRight: "-8px", marginLeft: "4px"}}/>}
onClick={handleUpdate}
>
</Button>
</Tooltip>
<Tooltip title="Auto-refresh control">
<Button variant="contained" color="primary" sx={{boxShadow: "none", borderRadius: "0 3px 3px 0"}} fullWidth
endIcon={<KeyboardArrowDownIcon sx={{transform: open ? "rotate(180deg)" : "none"}}/>}
onClick={(e) => setAnchorEl(e.currentTarget)}
>
{selectedDelay.title}
</Button>
</Tooltip>
</Box>
<Popper
open={open}
anchorEl={anchorEl}
placement="bottom-end"
modifiers={[{name: "offset", options: {offset: [0, 6]}}]}>
<ClickAwayListener onClickAway={() => setAnchorEl(null)}>
<Paper elevation={3}>
<List style={{minWidth: "110px", maxHeight: "208px", overflow: "auto", padding: "20px 0"}}>
{delayOptions.map(d =>
<ListItem key={d.seconds} button onClick={() => handleChange(d)}>
<ListItemText primary={d.title}/>
</ListItem>)}
</List>
</Paper>
</ClickAwayListener></Popper>
</>;
};

View File

@@ -1,21 +0,0 @@
import React, {FC} from "preact/compat";
import List from "@mui/material/List";
import ListItemButton from "@mui/material/ListItemButton";
import ListItemText from "@mui/material/ListItemText";
import {relativeTimeOptions} from "../../../../utils/time";
interface TimeDurationSelector {
setDuration: ({duration, until, id}: {duration: string, until: Date, id: string}) => void;
}
const TimeDurationSelector: FC<TimeDurationSelector> = ({setDuration}) => {
return <List style={{maxHeight: "168px", overflow: "auto", paddingRight: "15px"}}>
{relativeTimeOptions.map(({id, duration, until, title}) =>
<ListItemButton key={id} onClick={() => setDuration({duration, until: until(), id})}>
<ListItemText primary={title || duration}/>
</ListItemButton>)}
</List>;
};
export default TimeDurationSelector;

View File

@@ -1,173 +0,0 @@
import React, {FC, useEffect, useState, useMemo} from "preact/compat";
import {KeyboardEvent} from "react";
import {useAppDispatch, useAppState} from "../../../../state/common/StateContext";
import {dateFromSeconds, formatDateForNativeInput} from "../../../../utils/time";
import TimeDurationSelector from "./TimeDurationSelector";
import dayjs from "dayjs";
import QueryBuilderIcon from "@mui/icons-material/QueryBuilder";
import Box from "@mui/material/Box";
import TextField from "@mui/material/TextField";
import DateTimePicker from "@mui/lab/DateTimePicker";
import Button from "@mui/material/Button";
import Popper from "@mui/material/Popper";
import Paper from "@mui/material/Paper";
import Divider from "@mui/material/Divider";
import ClickAwayListener from "@mui/material/ClickAwayListener";
import Tooltip from "@mui/material/Tooltip";
import AlarmAdd from "@mui/icons-material/AlarmAdd";
import useMediaQuery from "@mui/material/useMediaQuery";
import {getAppModeEnable} from "../../../../utils/app-mode";
const formatDate = "YYYY-MM-DD HH:mm:ss";
const classes = {
container: {
display: "grid",
gridTemplateColumns: "200px auto 200px",
gridGap: "10px",
padding: "20px",
},
timeControls: {
display: "grid",
gridTemplateRows: "auto 1fr auto",
gridGap: "16px 0",
},
datePickerItem: {
minWidth: "200px",
},
};
export const TimeSelector: FC = () => {
const displayFullDate = useMediaQuery("(min-width: 1120px)");
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
const [until, setUntil] = useState<string>();
const [from, setFrom] = useState<string>();
const {time: {period: {end, start}, relativeTime}} = useAppState();
const dispatch = useAppDispatch();
const appModeEnable = getAppModeEnable();
useEffect(() => {
setUntil(formatDateForNativeInput(dateFromSeconds(end)));
}, [end]);
useEffect(() => {
setFrom(formatDateForNativeInput(dateFromSeconds(start)));
}, [start]);
const setDuration = ({duration, until, id}: {duration: string, until: Date, id: string}) => {
dispatch({type: "SET_RELATIVE_TIME", payload: {duration, until, id}});
setAnchorEl(null);
};
const formatRange = useMemo(() => {
const startFormat = dayjs(dateFromSeconds(start)).format(formatDate);
const endFormat = dayjs(dateFromSeconds(end)).format(formatDate);
return {
start: startFormat,
end: endFormat
};
}, [start, end]);
const open = Boolean(anchorEl);
const setTimeAndClosePicker = () => {
if (from && until) {
dispatch({type: "SET_PERIOD", payload: {from: new Date(from), to: new Date(until)}});
}
setAnchorEl(null);
};
const onFromChange = (from: dayjs.Dayjs | null) => setFrom(from?.format(formatDate));
const onUntilChange = (until: dayjs.Dayjs | null) => setUntil(until?.format(formatDate));
const onApplyClick = () => setTimeAndClosePicker();
const onSwitchToNow = () => dispatch({type: "RUN_QUERY_TO_NOW"});
const onCancelClick = () => {
setUntil(formatDateForNativeInput(dateFromSeconds(end)));
setFrom(formatDateForNativeInput(dateFromSeconds(start)));
setAnchorEl(null);
};
const onKeyDown = (e: KeyboardEvent) => {
if (e.key === "Enter" || e.keyCode === 13) {
setTimeAndClosePicker();
}
};
return <>
<Tooltip title="Time range controls">
<Button variant="contained" color="primary"
sx={{
color: "white",
border: appModeEnable ? "none" : "1px solid rgba(0, 0, 0, 0.2)",
boxShadow: "none",
minWidth: "34px",
padding: displayFullDate ? "" : "6px 8px",
}}
startIcon={<QueryBuilderIcon style={displayFullDate ? {} : {marginRight: "-8px", marginLeft: "4px"}}/>}
onClick={(e) => setAnchorEl(e.currentTarget)}>
{displayFullDate && <span>
{relativeTime && relativeTime !== "none"
? relativeTime.replace(/_/g, " ")
: `${formatRange.start} - ${formatRange.end}`}
</span>}
</Button>
</Tooltip>
<Popper
open={open}
anchorEl={anchorEl}
placement="bottom-end"
modifiers={[{name: "offset", options: {offset: [0, 6]}}]}
sx={{zIndex: 3, position: "relative"}}
>
<ClickAwayListener onClickAway={() => setAnchorEl(null)}>
<Paper elevation={3}>
<Box sx={classes.container}>
<Box sx={classes.timeControls}>
<Box sx={classes.datePickerItem}>
<DateTimePicker
label="From"
ampm={false}
value={from}
onChange={onFromChange}
onError={console.log}
inputFormat={formatDate}
mask="____-__-__ __:__:__"
renderInput={(params) => <TextField {...params} variant="standard" onKeyDown={onKeyDown}/>}
maxDate={dayjs(until)}
PopperProps={{disablePortal: true}}/>
</Box>
<Box sx={classes.datePickerItem}>
<DateTimePicker
label="To"
ampm={false}
value={until}
onChange={onUntilChange}
onError={console.log}
inputFormat={formatDate}
mask="____-__-__ __:__:__"
renderInput={(params) => <TextField {...params} variant="standard" onKeyDown={onKeyDown}/>}
PopperProps={{disablePortal: true}}/>
</Box>
<Box display="grid" gridTemplateColumns="auto 1fr" gap={1}>
<Button variant="outlined" onClick={onCancelClick}>
Cancel
</Button>
<Button variant="outlined" onClick={onApplyClick} color={"success"}>
Apply
</Button>
<Button startIcon={<AlarmAdd />} onClick={onSwitchToNow}>
switch to now
</Button>
</Box>
</Box>
{/*setup duration*/}
<Divider orientation="vertical" flexItem />
<Box>
<TimeDurationSelector setDuration={setDuration}/>
</Box>
</Box>
</Paper>
</ClickAwayListener>
</Popper>
</>;
};

View File

@@ -1,111 +0,0 @@
import React, {FC, useState, useEffect} from "preact/compat";
import Alert from "@mui/material/Alert";
import Box from "@mui/material/Box";
import GraphView from "./Views/GraphView";
import TableView from "./Views/TableView";
import {useAppDispatch, useAppState} from "../../state/common/StateContext";
import QueryConfigurator from "./Configurator/Query/QueryConfigurator";
import {useFetchQuery} from "../../hooks/useFetchQuery";
import JsonView from "./Views/JsonView";
import {DisplayTypeSwitch} from "./Configurator/DisplayTypeSwitch";
import GraphSettings from "./Configurator/Graph/GraphSettings";
import {useGraphDispatch, useGraphState} from "../../state/graph/GraphStateContext";
import {AxisRange} from "../../state/graph/reducer";
import Spinner from "../common/Spinner";
import {useFetchQueryOptions} from "../../hooks/useFetchQueryOptions";
import TracingsView from "./Views/TracingsView";
import Trace from "./Trace/Trace";
import TableSettings from "../Table/TableSettings";
const CustomPanel: FC = () => {
const [displayColumns, setDisplayColumns] = useState<string[]>();
const [tracesState, setTracesState] = useState<Trace[]>([]);
const {displayType, time: {period}, query, queryControls: {isTracingEnabled}} = useAppState();
const { customStep, yaxis } = useGraphState();
const dispatch = useAppDispatch();
const graphDispatch = useGraphDispatch();
const setYaxisLimits = (limits: AxisRange) => {
graphDispatch({type: "SET_YAXIS_LIMITS", payload: limits});
};
const toggleEnableLimits = () => {
graphDispatch({type: "TOGGLE_ENABLE_YAXIS_LIMITS"});
};
const setPeriod = ({from, to}: {from: Date, to: Date}) => {
dispatch({type: "SET_PERIOD", payload: {from, to}});
};
const {queryOptions} = useFetchQueryOptions();
const {isLoading, liveData, graphData, error, warning, traces} = useFetchQuery({
visible: true,
customStep
});
const handleTraceDelete = (trace: Trace) => {
const updatedTraces = tracesState.filter((data) => data.idValue !== trace.idValue);
setTracesState([...updatedTraces]);
};
useEffect(() => {
if (traces) {
setTracesState([...tracesState, ...traces]);
}
}, [traces]);
useEffect(() => {
setTracesState([]);
}, [displayType]);
return (
<Box p={4} display="grid" gridTemplateRows="auto 1fr" style={{minHeight: "calc(100vh - 64px)"}}>
<Box boxShadow="rgba(99, 99, 99, 0.2) 0px 2px 8px 0px;" p={4} pb={2} m={-4} mb={2}>
<QueryConfigurator error={error} queryOptions={queryOptions}/>
</Box>
<Box height="100%">
{isLoading && <Spinner isLoading={isLoading} height={"500px"}/>}
{<Box height={"100%"} bgcolor={"#fff"}>
<Box display="grid" gridTemplateColumns="1fr auto" alignItems="center" mb={2}
borderBottom={1} borderColor="divider">
<DisplayTypeSwitch/>
<Box display={"flex"}>
{displayType === "chart" && <GraphSettings
yaxis={yaxis}
setYaxisLimits={setYaxisLimits}
toggleEnableLimits={toggleEnableLimits}
/>}
{displayType === "table" && <TableSettings
data={liveData || []}
defaultColumns={displayColumns}
onChange={setDisplayColumns}
/>}
</Box>
</Box>
{error && <Alert color="error" severity="error" sx={{whiteSpace: "pre-wrap", mt: 2}}>{error}</Alert>}
{warning && <Alert color="warning" severity="warning" sx={{whiteSpace: "pre-wrap", my: 2}}>{warning}</Alert>}
{graphData && period && (displayType === "chart") && <>
{isTracingEnabled && <TracingsView
traces={tracesState}
onDeleteClick={handleTraceDelete}
/>}
<GraphView data={graphData} period={period} customStep={customStep} query={query} yaxis={yaxis}
setYaxisLimits={setYaxisLimits} setPeriod={setPeriod}/>
</>}
{liveData && (displayType === "code") && <JsonView data={liveData}/>}
{liveData && (displayType === "table") && <>
{isTracingEnabled && <TracingsView
traces={tracesState}
onDeleteClick={handleTraceDelete}
/>}
<TableView data={liveData} displayColumns={displayColumns}/>
</>}
</Box>}
</Box>
</Box>
);
};
export default CustomPanel;

View File

@@ -1,69 +0,0 @@
import React, {FC, useState} from "preact/compat";
import Box from "@mui/material/Box";
import ListItem from "@mui/material/ListItem";
import ListItemText from "@mui/material/ListItemText";
import ListItemButton from "@mui/material/ListItemButton";
import ListItemIcon from "@mui/material/ListItemIcon";
import ExpandLess from "@mui/icons-material/ExpandLess";
import AddCircleRoundedIcon from "@mui/icons-material/AddCircleRounded";
import Collapse from "@mui/material/Collapse";
import List from "@mui/material/List";
import {BorderLinearProgressWithLabel} from "../../BorderLineProgress/BorderLinearProgress";
import Trace from "../Trace/Trace";
interface RecursiveProps {
trace: Trace;
totalMsec: number;
}
interface OpenLevels {
[x: number]: boolean
}
const NestedNav: FC<RecursiveProps> = ({ trace, totalMsec}) => {
const [openLevels, setOpenLevels] = useState({} as OpenLevels);
const handleListClick = (level: number) => () => {
setOpenLevels((prevState:OpenLevels) => {
return {...prevState, [level]: !prevState[level]};
});
};
const hasChildren = trace.children && trace.children.length;
const progress = trace.duration / totalMsec * 100;
return (
<Box sx={{ bgcolor: "rgba(201, 227, 246, 0.4)" }}>
<ListItem onClick={handleListClick(trace.idValue)} sx={!hasChildren ? {p:0, pl: 7} : {p:0}}>
<ListItemButton alignItems={"flex-start"} sx={{ pt: 0, pb: 0}} style={{ userSelect: "text" }} disableRipple>
{hasChildren ? <ListItemIcon>
{openLevels[trace.idValue] ?
<ExpandLess fontSize={"large"} color={"info"} /> :
<AddCircleRoundedIcon fontSize={"large"} color={"info"} />}
</ListItemIcon>: null}
<Box display="flex" flexDirection="column" flexGrow={0.5} sx={{ ml: 4, mr: 4, width: "100%" }}>
<ListItemText>
<BorderLinearProgressWithLabel variant="determinate" value={progress} />
</ListItemText>
<ListItemText
primary={trace.message}
secondary={`duration: ${trace.duration} ms`}
/>
</Box>
</ListItemButton>
</ListItem>
<>
<Collapse in={openLevels[trace.idValue]} timeout="auto" unmountOnExit>
<List component="div" disablePadding sx={{ pl: 4 }}>
{hasChildren ?
trace.children.map((trace) => <NestedNav
key={trace.duration}
trace={trace}
totalMsec={totalMsec}
/>) : null}
</List>
</Collapse>
</>
</Box>
);
};
export default NestedNav;

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