mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2025-01-10 15:14:09 +00:00
Merge branch 'public-single-node' into pmm-6401-read-prometheus-data-files
This commit is contained in:
commit
be94882ada
252 changed files with 4517 additions and 1340 deletions
16
README.md
16
README.md
|
@ -105,6 +105,7 @@ Case studies:
|
||||||
* [Brandwatch](https://docs.victoriametrics.com/CaseStudies.html#brandwatch)
|
* [Brandwatch](https://docs.victoriametrics.com/CaseStudies.html#brandwatch)
|
||||||
* [CERN](https://docs.victoriametrics.com/CaseStudies.html#cern)
|
* [CERN](https://docs.victoriametrics.com/CaseStudies.html#cern)
|
||||||
* [COLOPL](https://docs.victoriametrics.com/CaseStudies.html#colopl)
|
* [COLOPL](https://docs.victoriametrics.com/CaseStudies.html#colopl)
|
||||||
|
* [Dig Security](https://docs.victoriametrics.com/CaseStudies.html#dig-security)
|
||||||
* [Fly.io](https://docs.victoriametrics.com/CaseStudies.html#flyio)
|
* [Fly.io](https://docs.victoriametrics.com/CaseStudies.html#flyio)
|
||||||
* [German Research Center for Artificial Intelligence](https://docs.victoriametrics.com/CaseStudies.html#german-research-center-for-artificial-intelligence)
|
* [German Research Center for Artificial Intelligence](https://docs.victoriametrics.com/CaseStudies.html#german-research-center-for-artificial-intelligence)
|
||||||
* [Grammarly](https://docs.victoriametrics.com/CaseStudies.html#grammarly)
|
* [Grammarly](https://docs.victoriametrics.com/CaseStudies.html#grammarly)
|
||||||
|
@ -2227,7 +2228,7 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li
|
||||||
-insert.maxQueueDuration duration
|
-insert.maxQueueDuration duration
|
||||||
The maximum duration to wait in the queue when -maxConcurrentInserts concurrent insert requests are executed (default 1m0s)
|
The maximum duration to wait in the queue when -maxConcurrentInserts concurrent insert requests are executed (default 1m0s)
|
||||||
-internStringMaxLen int
|
-internStringMaxLen int
|
||||||
The maximum length for strings to intern. Lower limit may save memory at the cost of higher CPU usage. See https://en.wikipedia.org/wiki/String_interning (default 300)
|
The maximum length for strings to intern. Lower limit may save memory at the cost of higher CPU usage. See https://en.wikipedia.org/wiki/String_interning (default 500)
|
||||||
-logNewSeries
|
-logNewSeries
|
||||||
Whether to log new series. This option is for debug purposes only. It can lead to performance issues when big number of new series are ingested into VictoriaMetrics
|
Whether to log new series. This option is for debug purposes only. It can lead to performance issues when big number of new series are ingested into VictoriaMetrics
|
||||||
-loggerDisableTimestamps
|
-loggerDisableTimestamps
|
||||||
|
@ -2263,11 +2264,11 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li
|
||||||
-metricsAuthKey string
|
-metricsAuthKey string
|
||||||
Auth key for /metrics endpoint. It must be passed via authKey query arg. It overrides httpAuth.* settings
|
Auth key for /metrics endpoint. It must be passed via authKey query arg. It overrides httpAuth.* settings
|
||||||
-opentsdbHTTPListenAddr string
|
-opentsdbHTTPListenAddr string
|
||||||
TCP address to listen for OpentTSDB HTTP put requests. Usually :4242 must be set. Doesn't work if empty. See also -opentsdbHTTPListenAddr.useProxyProtocol
|
TCP address to listen for OpenTSDB HTTP put requests. Usually :4242 must be set. Doesn't work if empty. See also -opentsdbHTTPListenAddr.useProxyProtocol
|
||||||
-opentsdbHTTPListenAddr.useProxyProtocol
|
-opentsdbHTTPListenAddr.useProxyProtocol
|
||||||
Whether to use proxy protocol for connections accepted at -opentsdbHTTPListenAddr . See https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt
|
Whether to use proxy protocol for connections accepted at -opentsdbHTTPListenAddr . See https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt
|
||||||
-opentsdbListenAddr string
|
-opentsdbListenAddr string
|
||||||
TCP and UDP address to listen for OpentTSDB metrics. Telnet put messages and HTTP /api/put messages are simultaneously served on TCP port. Usually :4242 must be set. Doesn't work if empty. See also -opentsdbListenAddr.useProxyProtocol
|
TCP and UDP address to listen for OpenTSDB metrics. Telnet put messages and HTTP /api/put messages are simultaneously served on TCP port. Usually :4242 must be set. Doesn't work if empty. See also -opentsdbListenAddr.useProxyProtocol
|
||||||
-opentsdbListenAddr.useProxyProtocol
|
-opentsdbListenAddr.useProxyProtocol
|
||||||
Whether to use proxy protocol for connections accepted at -opentsdbListenAddr . See https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt
|
Whether to use proxy protocol for connections accepted at -opentsdbListenAddr . See https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt
|
||||||
-opentsdbTrimTimestamp duration
|
-opentsdbTrimTimestamp duration
|
||||||
|
@ -2337,6 +2338,8 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li
|
||||||
How frequently to reload the full state from Kubernetes API server (default 30m0s)
|
How frequently to reload the full state from Kubernetes API server (default 30m0s)
|
||||||
-promscrape.kubernetesSDCheckInterval duration
|
-promscrape.kubernetesSDCheckInterval duration
|
||||||
Interval for checking for changes in Kubernetes API server. This works only if kubernetes_sd_configs is configured in '-promscrape.config' file. See https://docs.victoriametrics.com/sd_configs.html#kubernetes_sd_configs for details (default 30s)
|
Interval for checking for changes in Kubernetes API server. This works only if kubernetes_sd_configs is configured in '-promscrape.config' file. See https://docs.victoriametrics.com/sd_configs.html#kubernetes_sd_configs for details (default 30s)
|
||||||
|
-promscrape.kumaSDCheckInterval duration
|
||||||
|
Interval for checking for changes in kuma service discovery. This works only if kuma_sd_configs is configured in '-promscrape.config' file. See https://docs.victoriametrics.com/sd_configs.html#kuma_sd_configs for details (default 30s)
|
||||||
-promscrape.maxDroppedTargets int
|
-promscrape.maxDroppedTargets int
|
||||||
The maximum number of droppedTargets to show at /api/v1/targets page. Increase this value if your setup drops more scrape targets during relabeling and you need investigating labels for all the dropped targets. Note that the increased number of tracked dropped targets may result in increased memory usage (default 1000)
|
The maximum number of droppedTargets to show at /api/v1/targets page. Increase this value if your setup drops more scrape targets during relabeling and you need investigating labels for all the dropped targets. Note that the increased number of tracked dropped targets may result in increased memory usage (default 1000)
|
||||||
-promscrape.maxResponseHeadersSize size
|
-promscrape.maxResponseHeadersSize size
|
||||||
|
@ -2398,8 +2401,11 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li
|
||||||
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)
|
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
|
-search.latencyOffset duration
|
||||||
The time when data points become visible in query results after the collection. It can be overridden on per-query basis via latency_offset arg. Too small value can result in incomplete last points for query results (default 30s)
|
The time when data points become visible in query results after the collection. It can be overridden on per-query basis via latency_offset arg. Too small value can result in incomplete last points for query results (default 30s)
|
||||||
|
-search.logQueryMemoryUsage size
|
||||||
|
Log queries, which require more memory than specified by this flag. This may help detecting and optimizing heavy queries. Query logging is disabled by default. See also -search.logSlowQueryDuration and -search.maxMemoryPerQuery
|
||||||
|
Supports the following optional suffixes for size values: KB, MB, GB, TB, KiB, MiB, GiB, TiB (default 0)
|
||||||
-search.logSlowQueryDuration duration
|
-search.logSlowQueryDuration duration
|
||||||
Log queries with execution time exceeding this value. Zero disables slow query logging (default 5s)
|
Log queries with execution time exceeding this value. Zero disables slow query logging. See also -search.logQueryMemoryUsage (default 5s)
|
||||||
-search.maxConcurrentRequests int
|
-search.maxConcurrentRequests int
|
||||||
The maximum number of concurrent search requests. It shouldn't be high, since a single request can saturate all the CPU cores, while many concurrently executed requests may require high amounts of memory. See also -search.maxQueueDuration and -search.maxMemoryPerQuery (default 8)
|
The maximum number of concurrent search requests. It shouldn't be high, since a single request can saturate all the CPU cores, while many concurrently executed requests may require high amounts of memory. See also -search.maxQueueDuration and -search.maxMemoryPerQuery (default 8)
|
||||||
-search.maxExportDuration duration
|
-search.maxExportDuration duration
|
||||||
|
@ -2413,7 +2419,7 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li
|
||||||
-search.maxLookback duration
|
-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
|
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
|
-search.maxMemoryPerQuery size
|
||||||
The maximum amounts of memory a single query may consume. Queries requiring more memory are rejected. The total memory limit for concurrently executed queries can be estimated as -search.maxMemoryPerQuery multiplied by -search.maxConcurrentRequests
|
The maximum amounts of memory a single query may consume. Queries requiring more memory are rejected. The total memory limit for concurrently executed queries can be estimated as -search.maxMemoryPerQuery multiplied by -search.maxConcurrentRequests . See also -search.logQueryMemoryUsage
|
||||||
Supports the following optional suffixes for size values: KB, MB, GB, TB, KiB, MiB, GiB, TiB (default 0)
|
Supports the following optional suffixes for size values: KB, MB, GB, TB, KiB, MiB, GiB, TiB (default 0)
|
||||||
-search.maxPointsPerTimeseries int
|
-search.maxPointsPerTimeseries int
|
||||||
The maximum points per a single timeseries returned from /api/v1/query_range. This option doesn't limit the number of scanned raw samples in the database. The main purpose of this option is to limit the number of per-series points returned to graphing UI such as VMUI or Grafana. There is no sense in setting this limit to values bigger than the horizontal resolution of the graph (default 30000)
|
The maximum points per a single timeseries returned from /api/v1/query_range. This option doesn't limit the number of scanned raw samples in the database. The main purpose of this option is to limit the number of per-series points returned to graphing UI such as VMUI or Grafana. There is no sense in setting this limit to values bigger than the horizontal resolution of the graph (default 30000)
|
||||||
|
|
|
@ -103,7 +103,7 @@ func main() {
|
||||||
|
|
||||||
func requestHandler(w http.ResponseWriter, r *http.Request) bool {
|
func requestHandler(w http.ResponseWriter, r *http.Request) bool {
|
||||||
if r.URL.Path == "/" {
|
if r.URL.Path == "/" {
|
||||||
if r.Method != "GET" {
|
if r.Method != http.MethodGet {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
w.Header().Add("Content-Type", "text/html; charset=utf-8")
|
w.Header().Add("Content-Type", "text/html; charset=utf-8")
|
||||||
|
|
|
@ -181,30 +181,25 @@ There is also support for multitenant writes. See [these docs](#multitenancy).
|
||||||
|
|
||||||
## VictoriaMetrics remote write protocol
|
## VictoriaMetrics remote write protocol
|
||||||
|
|
||||||
By default `vmagent` uses Prometheus remote_write protocol for sending the data to the configured `-remoteWrite.url`.
|
`vmagent` supports sending data to the configured `-remoteWrite.url` either via Prometheus remote write protocol
|
||||||
This allows sending data to [any Prometheus-compatible remote storage](https://prometheus.io/docs/operating/integrations/#remote-endpoints-and-storage).
|
or via VictoriaMetrics remote write protocol.
|
||||||
|
|
||||||
The Prometheus remote_write protocol may require big amounts of network bandwidth under high load.
|
VictoriaMetrics remote write protocol provides the following benefits comparing to Prometheus remote write protocol:
|
||||||
This may result in high network egress costs when the configured remote storage is located in remote datacenter or availability zone.
|
|
||||||
This also may result in the increased disk IO at `vmagent` when it writes to disk the pending data, which must be sent to remote storage.
|
|
||||||
In this case the `vmagent` can be instructed to use VictoriaMetrics remote write protocol.
|
|
||||||
This allows reducing egress network bandwidth costs while reducing disk read/write IO at `vmagent` side under high load.
|
|
||||||
The `-remoteWrite.useVMProto=true` command-line flag instructs `vmagent` to send the data to the corresponding `-remoteWrite.url`
|
|
||||||
via VictoriaMetrics remote write protocol.
|
|
||||||
|
|
||||||
While all the [recently released](https://docs.victoriametrics.com/CHANGELOG.html) VictoriaMetrics components support
|
- Reduced network bandwidth usage by 2x-5x. This allows saving network bandwidth usage costs when `vmagent` and
|
||||||
the VictoriaMetrics remote write protocol, third-party systems and old versions of VictoriaMetrics components may miss the support of this protocol.
|
the configured remote storage systems are located in different datacenters, availability zones or regions.
|
||||||
|
|
||||||
The `-remoteWrite.useVMProto` command-line flag can be set independently per each configured `-remoteWrite.url`.
|
- Reduced disk read/write IO and disk space usage at `vmagent` when the remote storage is temporarily unavailable.
|
||||||
For example, the following command instructs `vmagent` to send the data to `https://victoriametrics/api/v1/write` via VictoriaMetrics remote write protocol,
|
In this case `vmagent` buffers the incoming data to disk using the VictoriaMetrics remote write format.
|
||||||
while sending the data to `https://prom-compatible-storage/write` via Prometheus remote write protocol:
|
This reduces disk read/write IO and disk space usage by 2x-5x comparing to Prometheus remote write format.
|
||||||
|
|
||||||
```
|
`vmagent` automatically uses VictoriaMetrics remote write protocol when it sends data to VictoriaMetrics components such as other `vmagent` instances,
|
||||||
./vmagent -remoteWrite.url=https://victoriametrics/api/v1/write \
|
[single-node VictoriaMetrics](https://docs.victoriametrics.com/Single-server-VictoriaMetrics.html)
|
||||||
-remoteWrite.useVMProto=true \
|
or `vminsert` at [cluster version](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html).
|
||||||
-remoteWrite.url=https://prom-compatible-storage/write \
|
|
||||||
-remoteWrite.useVMProto=false
|
`vmagent` automatically switches to Prometheus remote write protocol when it sends data to old versions of VictoriaMetrics components
|
||||||
```
|
or to other Prometheus-compatible remote storage systems. It is possible to force switch to Prometheus remote write protocol
|
||||||
|
by specifying `-remoteWrite.forcePromProto` command-line flag for the corresponding `-remoteWrite.url`.
|
||||||
|
|
||||||
## Multitenancy
|
## Multitenancy
|
||||||
|
|
||||||
|
@ -1299,11 +1294,11 @@ See the docs at https://docs.victoriametrics.com/vmagent.html .
|
||||||
-metricsAuthKey string
|
-metricsAuthKey string
|
||||||
Auth key for /metrics endpoint. It must be passed via authKey query arg. It overrides httpAuth.* settings
|
Auth key for /metrics endpoint. It must be passed via authKey query arg. It overrides httpAuth.* settings
|
||||||
-opentsdbHTTPListenAddr string
|
-opentsdbHTTPListenAddr string
|
||||||
TCP address to listen for OpentTSDB HTTP put requests. Usually :4242 must be set. Doesn't work if empty. See also -opentsdbHTTPListenAddr.useProxyProtocol
|
TCP address to listen for OpenTSDB HTTP put requests. Usually :4242 must be set. Doesn't work if empty. See also -opentsdbHTTPListenAddr.useProxyProtocol
|
||||||
-opentsdbHTTPListenAddr.useProxyProtocol
|
-opentsdbHTTPListenAddr.useProxyProtocol
|
||||||
Whether to use proxy protocol for connections accepted at -opentsdbHTTPListenAddr . See https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt
|
Whether to use proxy protocol for connections accepted at -opentsdbHTTPListenAddr . See https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt
|
||||||
-opentsdbListenAddr string
|
-opentsdbListenAddr string
|
||||||
TCP and UDP address to listen for OpentTSDB metrics. Telnet put messages and HTTP /api/put messages are simultaneously served on TCP port. Usually :4242 must be set. Doesn't work if empty. See also -opentsdbListenAddr.useProxyProtocol
|
TCP and UDP address to listen for OpenTSDB metrics. Telnet put messages and HTTP /api/put messages are simultaneously served on TCP port. Usually :4242 must be set. Doesn't work if empty. See also -opentsdbListenAddr.useProxyProtocol
|
||||||
-opentsdbListenAddr.useProxyProtocol
|
-opentsdbListenAddr.useProxyProtocol
|
||||||
Whether to use proxy protocol for connections accepted at -opentsdbListenAddr . See https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt
|
Whether to use proxy protocol for connections accepted at -opentsdbListenAddr . See https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt
|
||||||
-opentsdbTrimTimestamp duration
|
-opentsdbTrimTimestamp duration
|
||||||
|
@ -1371,6 +1366,8 @@ See the docs at https://docs.victoriametrics.com/vmagent.html .
|
||||||
How frequently to reload the full state from Kubernetes API server (default 30m0s)
|
How frequently to reload the full state from Kubernetes API server (default 30m0s)
|
||||||
-promscrape.kubernetesSDCheckInterval duration
|
-promscrape.kubernetesSDCheckInterval duration
|
||||||
Interval for checking for changes in Kubernetes API server. This works only if kubernetes_sd_configs is configured in '-promscrape.config' file. See https://docs.victoriametrics.com/sd_configs.html#kubernetes_sd_configs for details (default 30s)
|
Interval for checking for changes in Kubernetes API server. This works only if kubernetes_sd_configs is configured in '-promscrape.config' file. See https://docs.victoriametrics.com/sd_configs.html#kubernetes_sd_configs for details (default 30s)
|
||||||
|
-promscrape.kumaSDCheckInterval duration
|
||||||
|
Interval for checking for changes in kuma service discovery. This works only if kuma_sd_configs is configured in '-promscrape.config' file. See https://docs.victoriametrics.com/sd_configs.html#kuma_sd_configs for details (default 30s)
|
||||||
-promscrape.maxDroppedTargets int
|
-promscrape.maxDroppedTargets int
|
||||||
The maximum number of droppedTargets to show at /api/v1/targets page. Increase this value if your setup drops more scrape targets during relabeling and you need investigating labels for all the dropped targets. Note that the increased number of tracked dropped targets may result in increased memory usage (default 1000)
|
The maximum number of droppedTargets to show at /api/v1/targets page. Increase this value if your setup drops more scrape targets during relabeling and you need investigating labels for all the dropped targets. Note that the increased number of tracked dropped targets may result in increased memory usage (default 1000)
|
||||||
-promscrape.maxResponseHeadersSize size
|
-promscrape.maxResponseHeadersSize size
|
||||||
|
@ -1451,6 +1448,9 @@ See the docs at https://docs.victoriametrics.com/vmagent.html .
|
||||||
Supports an array of values separated by comma or specified via multiple flags.
|
Supports an array of values separated by comma or specified via multiple flags.
|
||||||
-remoteWrite.flushInterval duration
|
-remoteWrite.flushInterval duration
|
||||||
Interval for flushing the data to remote storage. This option takes effect only when less than 10K data points per second are pushed to -remoteWrite.url (default 1s)
|
Interval for flushing the data to remote storage. This option takes effect only when less than 10K data points per second are pushed to -remoteWrite.url (default 1s)
|
||||||
|
-remoteWrite.forcePromProto array
|
||||||
|
Whether to force Prometheus remote write protocol for sending data to the corresponding -remoteWrite.url . See https://docs.victoriametrics.com/vmagent.html#victoriametrics-remote-write-protocol
|
||||||
|
Supports array of values separated by comma or specified via multiple flags.
|
||||||
-remoteWrite.headers array
|
-remoteWrite.headers array
|
||||||
Optional HTTP headers to send with each request to the corresponding -remoteWrite.url. For example, -remoteWrite.headers='My-Auth:foobar' would send 'My-Auth: foobar' HTTP header with every request to the corresponding -remoteWrite.url. Multiple headers must be delimited by '^^': -remoteWrite.headers='header1:value1^^header2:value2'
|
Optional HTTP headers to send with each request to the corresponding -remoteWrite.url. For example, -remoteWrite.headers='My-Auth:foobar' would send 'My-Auth: foobar' HTTP header with every request to the corresponding -remoteWrite.url. Multiple headers must be delimited by '^^': -remoteWrite.headers='header1:value1^^header2:value2'
|
||||||
Supports an array of values separated by comma or specified via multiple flags.
|
Supports an array of values separated by comma or specified via multiple flags.
|
||||||
|
@ -1536,14 +1536,11 @@ See the docs at https://docs.victoriametrics.com/vmagent.html .
|
||||||
-remoteWrite.tmpDataPath string
|
-remoteWrite.tmpDataPath string
|
||||||
Path to directory where temporary data for remote write component is stored. See also -remoteWrite.maxDiskUsagePerURL (default "vmagent-remotewrite-data")
|
Path to directory where temporary data for remote write component is stored. See also -remoteWrite.maxDiskUsagePerURL (default "vmagent-remotewrite-data")
|
||||||
-remoteWrite.url array
|
-remoteWrite.url array
|
||||||
Remote storage URL to write data to. It must support Prometheus remote_write protocol. Example url: http://<victoriametrics-host>:8428/api/v1/write . It is recommended setting -remoteWrite.useVMProto command-line option when VictoriaMetrics is used as a remote storage in order to save network bandwidth. See https://docs.victoriametrics.com/vmagent.html#victoriametrics-remote-write-protocol . Pass multiple -remoteWrite.url options in order to replicate the collected data to multiple remote storage systems. See also -remoteWrite.multitenantURL
|
Remote storage URL to write data to. It must support either VictoriaMetrics remote write protocol or Prometheus remote_write protocol. Example url: http://<victoriametrics-host>:8428/api/v1/write . Pass multiple -remoteWrite.url options in order to replicate the collected data to multiple remote storage systems. See also -remoteWrite.multitenantURL
|
||||||
Supports an array of values separated by comma or specified via multiple flags.
|
Supports an array of values separated by comma or specified via multiple flags.
|
||||||
-remoteWrite.urlRelabelConfig array
|
-remoteWrite.urlRelabelConfig array
|
||||||
Optional path to relabel configs for the corresponding -remoteWrite.url. See also -remoteWrite.relabelConfig. The path can point either to local file or to http url. See https://docs.victoriametrics.com/vmagent.html#relabeling
|
Optional path to relabel configs for the corresponding -remoteWrite.url. See also -remoteWrite.relabelConfig. The path can point either to local file or to http url. See https://docs.victoriametrics.com/vmagent.html#relabeling
|
||||||
Supports an array of values separated by comma or specified via multiple flags.
|
Supports an array of values separated by comma or specified via multiple flags.
|
||||||
-remoteWrite.useVMProto array
|
|
||||||
Whether to use VictoriaMetrics protocol for sending the data to the given -remoteWrite.url in order to reduce network bandwidth usage and disk read/write IO under high load. See https://docs.victoriametrics.com/vmagent.html#victoriametrics-remote-write-protocol
|
|
||||||
Supports array of values separated by comma or specified via multiple flags.
|
|
||||||
-sortLabels
|
-sortLabels
|
||||||
Whether to sort labels for incoming samples before writing them to all the configured remote storage systems. This may be needed for reducing memory usage at remote storage when the order of labels in incoming samples is random. For example, if m{k1="v1",k2="v2"} may be sent as m{k2="v2",k1="v1"}Enabled sorting for labels can slow down ingestion performance a bit
|
Whether to sort labels for incoming samples before writing them to all the configured remote storage systems. This may be needed for reducing memory usage at remote storage when the order of labels in incoming samples is random. For example, if m{k1="v1",k2="v2"} may be sent as m{k2="v2",k1="v1"}Enabled sorting for labels can slow down ingestion performance a bit
|
||||||
-tls
|
-tls
|
||||||
|
|
|
@ -56,12 +56,12 @@ var (
|
||||||
"See also -graphiteListenAddr.useProxyProtocol")
|
"See also -graphiteListenAddr.useProxyProtocol")
|
||||||
graphiteUseProxyProtocol = flag.Bool("graphiteListenAddr.useProxyProtocol", false, "Whether to use proxy protocol for connections accepted at -graphiteListenAddr . "+
|
graphiteUseProxyProtocol = flag.Bool("graphiteListenAddr.useProxyProtocol", false, "Whether to use proxy protocol for connections accepted at -graphiteListenAddr . "+
|
||||||
"See https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt")
|
"See https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt")
|
||||||
opentsdbListenAddr = flag.String("opentsdbListenAddr", "", "TCP and UDP address to listen for OpentTSDB metrics. "+
|
opentsdbListenAddr = flag.String("opentsdbListenAddr", "", "TCP and UDP address to listen for OpenTSDB metrics. "+
|
||||||
"Telnet put messages and HTTP /api/put messages are simultaneously served on TCP port. "+
|
"Telnet put messages and HTTP /api/put messages are simultaneously served on TCP port. "+
|
||||||
"Usually :4242 must be set. Doesn't work if empty. See also -opentsdbListenAddr.useProxyProtocol")
|
"Usually :4242 must be set. Doesn't work if empty. See also -opentsdbListenAddr.useProxyProtocol")
|
||||||
opentsdbUseProxyProtocol = flag.Bool("opentsdbListenAddr.useProxyProtocol", false, "Whether to use proxy protocol for connections accepted at -opentsdbListenAddr . "+
|
opentsdbUseProxyProtocol = flag.Bool("opentsdbListenAddr.useProxyProtocol", false, "Whether to use proxy protocol for connections accepted at -opentsdbListenAddr . "+
|
||||||
"See https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt")
|
"See https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt")
|
||||||
opentsdbHTTPListenAddr = flag.String("opentsdbHTTPListenAddr", "", "TCP address to listen for OpentTSDB HTTP put requests. Usually :4242 must be set. Doesn't work if empty. "+
|
opentsdbHTTPListenAddr = flag.String("opentsdbHTTPListenAddr", "", "TCP address to listen for OpenTSDB HTTP put requests. Usually :4242 must be set. Doesn't work if empty. "+
|
||||||
"See also -opentsdbHTTPListenAddr.useProxyProtocol")
|
"See also -opentsdbHTTPListenAddr.useProxyProtocol")
|
||||||
opentsdbHTTPUseProxyProtocol = flag.Bool("opentsdbHTTPListenAddr.useProxyProtocol", false, "Whether to use proxy protocol for connections accepted "+
|
opentsdbHTTPUseProxyProtocol = flag.Bool("opentsdbHTTPListenAddr.useProxyProtocol", false, "Whether to use proxy protocol for connections accepted "+
|
||||||
"at -opentsdbHTTPListenAddr . See https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt")
|
"at -opentsdbHTTPListenAddr . See https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt")
|
||||||
|
@ -208,7 +208,7 @@ func getAuthTokenFromPath(path string) (*auth.Token, error) {
|
||||||
|
|
||||||
func requestHandler(w http.ResponseWriter, r *http.Request) bool {
|
func requestHandler(w http.ResponseWriter, r *http.Request) bool {
|
||||||
if r.URL.Path == "/" {
|
if r.URL.Path == "/" {
|
||||||
if r.Method != "GET" {
|
if r.Method != http.MethodGet {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
w.Header().Add("Content-Type", "text/html; charset=utf-8")
|
w.Header().Add("Content-Type", "text/html; charset=utf-8")
|
||||||
|
@ -253,6 +253,9 @@ func requestHandler(w http.ResponseWriter, r *http.Request) bool {
|
||||||
}
|
}
|
||||||
switch path {
|
switch path {
|
||||||
case "/prometheus/api/v1/write", "/api/v1/write":
|
case "/prometheus/api/v1/write", "/api/v1/write":
|
||||||
|
if common.HandleVMProtoServerHandshake(w, r) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
prometheusWriteRequests.Inc()
|
prometheusWriteRequests.Inc()
|
||||||
if err := promremotewrite.InsertHandler(nil, r); err != nil {
|
if err := promremotewrite.InsertHandler(nil, r); err != nil {
|
||||||
prometheusWriteErrors.Inc()
|
prometheusWriteErrors.Inc()
|
||||||
|
|
|
@ -23,7 +23,7 @@ var (
|
||||||
func TestInsertHandler(t *testing.T) {
|
func TestInsertHandler(t *testing.T) {
|
||||||
setUp()
|
setUp()
|
||||||
defer tearDown()
|
defer tearDown()
|
||||||
req := httptest.NewRequest("POST", "/insert/0/api/v1/import/prometheus", bytes.NewBufferString(`{"foo":"bar"}
|
req := httptest.NewRequest(http.MethodPost, "/insert/0/api/v1/import/prometheus", bytes.NewBufferString(`{"foo":"bar"}
|
||||||
go_memstats_alloc_bytes_total 1`))
|
go_memstats_alloc_bytes_total 1`))
|
||||||
if err := InsertHandler(nil, req); err != nil {
|
if err := InsertHandler(nil, req); err != nil {
|
||||||
t.Errorf("unxepected error %s", err)
|
t.Errorf("unxepected error %s", err)
|
||||||
|
|
|
@ -123,6 +123,10 @@ func newHTTPClient(argIdx int, remoteWriteURL, sanitizedURL string, fq *persiste
|
||||||
}
|
}
|
||||||
tr.Proxy = http.ProxyURL(pu)
|
tr.Proxy = http.ProxyURL(pu)
|
||||||
}
|
}
|
||||||
|
hc := &http.Client{
|
||||||
|
Transport: tr,
|
||||||
|
Timeout: sendTimeout.GetOptionalArgOrDefault(argIdx, time.Minute),
|
||||||
|
}
|
||||||
c := &client{
|
c := &client{
|
||||||
sanitizedURL: sanitizedURL,
|
sanitizedURL: sanitizedURL,
|
||||||
remoteWriteURL: remoteWriteURL,
|
remoteWriteURL: remoteWriteURL,
|
||||||
|
@ -130,11 +134,8 @@ func newHTTPClient(argIdx int, remoteWriteURL, sanitizedURL string, fq *persiste
|
||||||
authCfg: authCfg,
|
authCfg: authCfg,
|
||||||
awsCfg: awsCfg,
|
awsCfg: awsCfg,
|
||||||
fq: fq,
|
fq: fq,
|
||||||
hc: &http.Client{
|
hc: hc,
|
||||||
Transport: tr,
|
stopCh: make(chan struct{}),
|
||||||
Timeout: sendTimeout.GetOptionalArgOrDefault(argIdx, time.Minute),
|
|
||||||
},
|
|
||||||
stopCh: make(chan struct{}),
|
|
||||||
}
|
}
|
||||||
c.sendBlock = c.sendBlockHTTP
|
c.sendBlock = c.sendBlockHTTP
|
||||||
return c
|
return c
|
||||||
|
@ -309,7 +310,7 @@ func (c *client) sendBlockHTTP(block []byte) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
again:
|
again:
|
||||||
req, err := http.NewRequest("POST", c.remoteWriteURL, bytes.NewBuffer(block))
|
req, err := http.NewRequest(http.MethodPost, c.remoteWriteURL, bytes.NewBuffer(block))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Panicf("BUG: unexpected error from http.NewRequest(%q): %s", c.sanitizedURL, err)
|
logger.Panicf("BUG: unexpected error from http.NewRequest(%q): %s", c.sanitizedURL, err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package remotewrite
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
|
||||||
|
@ -21,7 +22,7 @@ func TestPushWriteRequest(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func testPushWriteRequest(t *testing.T, rowsCount, expectedBlockLenProm, expectedBlockLenVM int) {
|
func testPushWriteRequest(t *testing.T, rowsCount, expectedBlockLenProm, expectedBlockLenVM int) {
|
||||||
f := func(isVMRemoteWrite bool, expectedBlockLen int) {
|
f := func(isVMRemoteWrite bool, expectedBlockLen int, tolerancePrc float64) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
wr := newTestWriteRequest(rowsCount, 20)
|
wr := newTestWriteRequest(rowsCount, 20)
|
||||||
pushBlockLen := 0
|
pushBlockLen := 0
|
||||||
|
@ -32,17 +33,17 @@ func testPushWriteRequest(t *testing.T, rowsCount, expectedBlockLenProm, expecte
|
||||||
pushBlockLen = len(block)
|
pushBlockLen = len(block)
|
||||||
}
|
}
|
||||||
pushWriteRequest(wr, pushBlock, isVMRemoteWrite)
|
pushWriteRequest(wr, pushBlock, isVMRemoteWrite)
|
||||||
if pushBlockLen != expectedBlockLen {
|
if math.Abs(float64(pushBlockLen-expectedBlockLen)/float64(expectedBlockLen)*100) > tolerancePrc {
|
||||||
t.Fatalf("unexpected block len for rowsCount=%d, isVMRemoteWrite=%v; got %d bytes; expecting %d bytes",
|
t.Fatalf("unexpected block len for rowsCount=%d, isVMRemoteWrite=%v; got %d bytes; expecting %d bytes +- %.0f%%",
|
||||||
rowsCount, isVMRemoteWrite, pushBlockLen, expectedBlockLen)
|
rowsCount, isVMRemoteWrite, pushBlockLen, expectedBlockLen, tolerancePrc)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check Prometheus remote write
|
// Check Prometheus remote write
|
||||||
f(false, expectedBlockLenProm)
|
f(false, expectedBlockLenProm, 0)
|
||||||
|
|
||||||
// Check VictoriaMetrics remote write
|
// Check VictoriaMetrics remote write
|
||||||
f(true, expectedBlockLenVM)
|
f(true, expectedBlockLenVM, 15)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newTestWriteRequest(seriesCount, labelsCount int) *prompbmarshal.WriteRequest {
|
func newTestWriteRequest(seriesCount, labelsCount int) *prompbmarshal.WriteRequest {
|
||||||
|
|
|
@ -21,6 +21,7 @@ import (
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/procutil"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/procutil"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promrelabel"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promrelabel"
|
||||||
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/common"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/streamaggr"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/streamaggr"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/tenantmetrics"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/tenantmetrics"
|
||||||
"github.com/VictoriaMetrics/metrics"
|
"github.com/VictoriaMetrics/metrics"
|
||||||
|
@ -28,17 +29,14 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
remoteWriteURLs = flagutil.NewArrayString("remoteWrite.url", "Remote storage URL to write data to. It must support Prometheus remote_write protocol. "+
|
remoteWriteURLs = flagutil.NewArrayString("remoteWrite.url", "Remote storage URL to write data to. It must support either VictoriaMetrics remote write protocol "+
|
||||||
"Example url: http://<victoriametrics-host>:8428/api/v1/write . "+
|
"or Prometheus remote_write protocol. Example url: http://<victoriametrics-host>:8428/api/v1/write . "+
|
||||||
"It is recommended setting -remoteWrite.useVMProto command-line option when VictoriaMetrics is used as a remote storage in order to save network bandwidth. "+
|
|
||||||
"See https://docs.victoriametrics.com/vmagent.html#victoriametrics-remote-write-protocol . "+
|
|
||||||
"Pass multiple -remoteWrite.url options in order to replicate the collected data to multiple remote storage systems. See also -remoteWrite.multitenantURL")
|
"Pass multiple -remoteWrite.url options in order to replicate the collected data to multiple remote storage systems. See also -remoteWrite.multitenantURL")
|
||||||
remoteWriteMultitenantURLs = flagutil.NewArrayString("remoteWrite.multitenantURL", "Base path for multitenant remote storage URL to write data to. "+
|
remoteWriteMultitenantURLs = flagutil.NewArrayString("remoteWrite.multitenantURL", "Base path for multitenant remote storage URL to write data to. "+
|
||||||
"See https://docs.victoriametrics.com/vmagent.html#multitenancy for details. Example url: http://<vminsert>:8480 . "+
|
"See https://docs.victoriametrics.com/vmagent.html#multitenancy for details. Example url: http://<vminsert>:8480 . "+
|
||||||
"Pass multiple -remoteWrite.multitenantURL flags in order to replicate data to multiple remote storage systems. See also -remoteWrite.url")
|
"Pass multiple -remoteWrite.multitenantURL flags in order to replicate data to multiple remote storage systems. See also -remoteWrite.url")
|
||||||
useVMProto = flagutil.NewArrayBool("remoteWrite.useVMProto", "Whether to use VictoriaMetrics protocol for sending the data to the given -remoteWrite.url "+
|
forcePromProto = flagutil.NewArrayBool("remoteWrite.forcePromProto", "Whether to force Prometheus remote write protocol for sending data "+
|
||||||
"in order to reduce network bandwidth usage and disk read/write IO under high load. "+
|
"to the corresponding -remoteWrite.url . See https://docs.victoriametrics.com/vmagent.html#victoriametrics-remote-write-protocol")
|
||||||
"See https://docs.victoriametrics.com/vmagent.html#victoriametrics-remote-write-protocol")
|
|
||||||
tmpDataPath = flag.String("remoteWrite.tmpDataPath", "vmagent-remotewrite-data", "Path to directory where temporary data for remote write component is stored. "+
|
tmpDataPath = flag.String("remoteWrite.tmpDataPath", "vmagent-remotewrite-data", "Path to directory where temporary data for remote write component is stored. "+
|
||||||
"See also -remoteWrite.maxDiskUsagePerURL")
|
"See also -remoteWrite.maxDiskUsagePerURL")
|
||||||
queues = flag.Int("remoteWrite.queues", cgroup.AvailableCPUs()*2, "The number of concurrent queues to each -remoteWrite.url. Set more queues if default number of queues "+
|
queues = flag.Int("remoteWrite.queues", cgroup.AvailableCPUs()*2, "The number of concurrent queues to each -remoteWrite.url. Set more queues if default number of queues "+
|
||||||
|
@ -480,7 +478,18 @@ func newRemoteWriteCtx(argIdx int, at *auth.Token, remoteWriteURL *url.URL, maxI
|
||||||
_ = metrics.GetOrCreateGauge(fmt.Sprintf(`vmagent_remotewrite_pending_inmemory_blocks{path=%q, url=%q}`, queuePath, sanitizedURL), func() float64 {
|
_ = metrics.GetOrCreateGauge(fmt.Sprintf(`vmagent_remotewrite_pending_inmemory_blocks{path=%q, url=%q}`, queuePath, sanitizedURL), func() float64 {
|
||||||
return float64(fq.GetInmemoryQueueLen())
|
return float64(fq.GetInmemoryQueueLen())
|
||||||
})
|
})
|
||||||
isVMRemoteWrite := useVMProto.GetOptionalArg(argIdx)
|
|
||||||
|
// Auto-detect whether the remote storage supports VictoriaMetrics remote write protocol.
|
||||||
|
isVMRemoteWrite := false
|
||||||
|
usePromProto := forcePromProto.GetOptionalArg(argIdx)
|
||||||
|
if !usePromProto {
|
||||||
|
isVMRemoteWrite = common.HandleVMProtoClientHandshake(remoteWriteURL)
|
||||||
|
if !isVMRemoteWrite {
|
||||||
|
logger.Infof("the remote storage at %q doesn't support VictoriaMetrics remote write protocol. Switching to Prometheus remote write protocol. "+
|
||||||
|
"See https://docs.victoriametrics.com/vmagent.html#victoriametrics-remote-write-protocol", sanitizedURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var c *client
|
var c *client
|
||||||
switch remoteWriteURL.Scheme {
|
switch remoteWriteURL.Scheme {
|
||||||
case "http", "https":
|
case "http", "https":
|
||||||
|
|
|
@ -621,6 +621,8 @@ can read the same rules configuration as normal, evaluate them on the given time
|
||||||
results via remote write to the configured storage. vmalert supports any PromQL/MetricsQL compatible
|
results via remote write to the configured storage. vmalert supports any PromQL/MetricsQL compatible
|
||||||
data source for backfilling.
|
data source for backfilling.
|
||||||
|
|
||||||
|
See a blogpost about [Rules backfilling via vmalert](https://victoriametrics.com/blog/rules-replay/).
|
||||||
|
|
||||||
### How it works
|
### How it works
|
||||||
|
|
||||||
In `replay` mode vmalert works as a cli-tool and exits immediately after work is done.
|
In `replay` mode vmalert works as a cli-tool and exits immediately after work is done.
|
||||||
|
|
|
@ -174,7 +174,7 @@ func (s *VMStorage) do(ctx context.Context, req *http.Request) (*http.Response,
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *VMStorage) newRequestPOST() (*http.Request, error) {
|
func (s *VMStorage) newRequestPOST() (*http.Request, error) {
|
||||||
req, err := http.NewRequest("POST", s.datasourceURL, nil)
|
req, err := http.NewRequest(http.MethodPost, s.datasourceURL, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,7 +64,7 @@ func (am *AlertManager) send(ctx context.Context, alerts []Alert) error {
|
||||||
b := &bytes.Buffer{}
|
b := &bytes.Buffer{}
|
||||||
writeamRequest(b, alerts, am.argFunc, am.relabelConfigs)
|
writeamRequest(b, alerts, am.argFunc, am.relabelConfigs)
|
||||||
|
|
||||||
req, err := http.NewRequest("POST", am.addr, b)
|
req, err := http.NewRequest(http.MethodPost, am.addr, b)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -225,7 +225,7 @@ func (c *Client) flush(ctx context.Context, wr *prompbmarshal.WriteRequest) {
|
||||||
|
|
||||||
func (c *Client) send(ctx context.Context, data []byte) error {
|
func (c *Client) send(ctx context.Context, data []byte) error {
|
||||||
r := bytes.NewReader(data)
|
r := bytes.NewReader(data)
|
||||||
req, err := http.NewRequest("POST", c.addr, r)
|
req, err := http.NewRequest(http.MethodPost, c.addr, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create new HTTP request: %w", err)
|
return fmt.Errorf("failed to create new HTTP request: %w", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,7 +57,7 @@ func (rh *requestHandler) handler(w http.ResponseWriter, r *http.Request) bool {
|
||||||
|
|
||||||
switch r.URL.Path {
|
switch r.URL.Path {
|
||||||
case "/", "/vmalert", "/vmalert/":
|
case "/", "/vmalert", "/vmalert/":
|
||||||
if r.Method != "GET" {
|
if r.Method != http.MethodGet {
|
||||||
httpserver.Errorf(w, r, "path %q supports only GET method", r.URL.Path)
|
httpserver.Errorf(w, r, "path %q supports only GET method", r.URL.Path)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
|
@ -169,7 +169,7 @@ func tryProcessingRequest(w http.ResponseWriter, r *http.Request, targetURL *url
|
||||||
if err != nil {
|
if err != nil {
|
||||||
remoteAddr := httpserver.GetQuotedRemoteAddr(r)
|
remoteAddr := httpserver.GetQuotedRemoteAddr(r)
|
||||||
requestURI := httpserver.GetRequestURI(r)
|
requestURI := httpserver.GetRequestURI(r)
|
||||||
if r.Method == "POST" || r.Method == "PUT" {
|
if r.Method == http.MethodPost || r.Method == http.MethodPut {
|
||||||
// It is impossible to retry POST and PUT requests,
|
// It is impossible to retry POST and PUT requests,
|
||||||
// since we already proxied the request body to the backend.
|
// since we already proxied the request body to the backend.
|
||||||
err = &httpserver.ErrorWithStatusCode{
|
err = &httpserver.ErrorWithStatusCode{
|
||||||
|
|
60
app/vmctl/backoff/backoff.go
Normal file
60
app/vmctl/backoff/backoff.go
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
package backoff
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
backoffRetries = 5
|
||||||
|
backoffFactor = 1.7
|
||||||
|
backoffMinDuration = time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
// retryableFunc describes call back which will repeat on errors
|
||||||
|
type retryableFunc func() error
|
||||||
|
|
||||||
|
var ErrBadRequest = errors.New("bad request")
|
||||||
|
|
||||||
|
// Backoff describes object with backoff policy params
|
||||||
|
type Backoff struct {
|
||||||
|
retries int
|
||||||
|
factor float64
|
||||||
|
minDuration time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// New initialize backoff object
|
||||||
|
func New() *Backoff {
|
||||||
|
return &Backoff{
|
||||||
|
retries: backoffRetries,
|
||||||
|
factor: backoffFactor,
|
||||||
|
minDuration: backoffMinDuration,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retry process retries until all attempts are completed
|
||||||
|
func (b *Backoff) Retry(ctx context.Context, cb retryableFunc) (uint64, error) {
|
||||||
|
var attempt uint64
|
||||||
|
for i := 0; i < b.retries; i++ {
|
||||||
|
// @TODO we should use context to cancel retries
|
||||||
|
err := cb()
|
||||||
|
if err == nil {
|
||||||
|
return attempt, nil
|
||||||
|
}
|
||||||
|
if errors.Is(err, ErrBadRequest) {
|
||||||
|
logger.Errorf("unrecoverable error: %s", err)
|
||||||
|
return attempt, err // fail fast if not recoverable
|
||||||
|
}
|
||||||
|
attempt++
|
||||||
|
backoff := float64(b.minDuration) * math.Pow(b.factor, float64(i))
|
||||||
|
dur := time.Duration(backoff)
|
||||||
|
logger.Errorf("got error: %s on attempt: %d; will retry in %v", err, attempt, dur)
|
||||||
|
time.Sleep(time.Duration(backoff))
|
||||||
|
}
|
||||||
|
return attempt, fmt.Errorf("execution failed after %d retry attempts", b.retries)
|
||||||
|
}
|
96
app/vmctl/backoff/backoff_test.go
Normal file
96
app/vmctl/backoff/backoff_test.go
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
package backoff
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRetry_Do(t *testing.T) {
|
||||||
|
counter := 0
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
backoffRetries int
|
||||||
|
backoffFactor float64
|
||||||
|
backoffMinDuration time.Duration
|
||||||
|
retryableFunc retryableFunc
|
||||||
|
ctx context.Context
|
||||||
|
withCancel bool
|
||||||
|
want uint64
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "return bad request",
|
||||||
|
retryableFunc: func() error {
|
||||||
|
return ErrBadRequest
|
||||||
|
},
|
||||||
|
ctx: context.Background(),
|
||||||
|
want: 0,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty retries values",
|
||||||
|
retryableFunc: func() error {
|
||||||
|
time.Sleep(time.Millisecond * 100)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
ctx: context.Background(),
|
||||||
|
want: 0,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "only one retry test",
|
||||||
|
backoffRetries: 5,
|
||||||
|
backoffFactor: 1.7,
|
||||||
|
backoffMinDuration: time.Millisecond * 10,
|
||||||
|
retryableFunc: func() error {
|
||||||
|
t := time.NewTicker(time.Millisecond * 5)
|
||||||
|
defer t.Stop()
|
||||||
|
for range t.C {
|
||||||
|
counter++
|
||||||
|
if counter%2 == 0 {
|
||||||
|
return fmt.Errorf("got some error")
|
||||||
|
}
|
||||||
|
if counter%3 == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
ctx: context.Background(),
|
||||||
|
want: 1,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "all retries failed test",
|
||||||
|
backoffRetries: 5,
|
||||||
|
backoffFactor: 0.1,
|
||||||
|
backoffMinDuration: time.Millisecond * 10,
|
||||||
|
retryableFunc: func() error {
|
||||||
|
t := time.NewTicker(time.Millisecond * 5)
|
||||||
|
defer t.Stop()
|
||||||
|
for range t.C {
|
||||||
|
return fmt.Errorf("got some error")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
ctx: context.Background(),
|
||||||
|
want: 5,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
r := New()
|
||||||
|
got, err := r.Retry(tt.ctx, tt.retryableFunc)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("Retry() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("Retry() got = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -65,7 +65,7 @@ func main() {
|
||||||
// disable progress bars since openTSDB implementation
|
// disable progress bars since openTSDB implementation
|
||||||
// does not use progress bar pool
|
// does not use progress bar pool
|
||||||
vmCfg.DisableProgressBar = true
|
vmCfg.DisableProgressBar = true
|
||||||
importer, err := vm.NewImporter(vmCfg)
|
importer, err := vm.NewImporter(ctx, vmCfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create VM importer: %s", err)
|
return fmt.Errorf("failed to create VM importer: %s", err)
|
||||||
}
|
}
|
||||||
|
@ -100,7 +100,7 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
vmCfg := initConfigVM(c)
|
vmCfg := initConfigVM(c)
|
||||||
importer, err = vm.NewImporter(vmCfg)
|
importer, err = vm.NewImporter(ctx, vmCfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create VM importer: %s", err)
|
return fmt.Errorf("failed to create VM importer: %s", err)
|
||||||
}
|
}
|
||||||
|
@ -137,7 +137,7 @@ func main() {
|
||||||
|
|
||||||
vmCfg := initConfigVM(c)
|
vmCfg := initConfigVM(c)
|
||||||
|
|
||||||
importer, err := vm.NewImporter(vmCfg)
|
importer, err := vm.NewImporter(ctx, vmCfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create VM importer: %s", err)
|
return fmt.Errorf("failed to create VM importer: %s", err)
|
||||||
}
|
}
|
||||||
|
@ -163,7 +163,7 @@ func main() {
|
||||||
fmt.Println("Prometheus import mode")
|
fmt.Println("Prometheus import mode")
|
||||||
|
|
||||||
vmCfg := initConfigVM(c)
|
vmCfg := initConfigVM(c)
|
||||||
importer, err = vm.NewImporter(vmCfg)
|
importer, err = vm.NewImporter(ctx, vmCfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create VM importer: %s", err)
|
return fmt.Errorf("failed to create VM importer: %s", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
@ -58,7 +59,7 @@ func Test_prometheusProcessor_run(t *testing.T) {
|
||||||
return client
|
return client
|
||||||
},
|
},
|
||||||
im: func(vmCfg vm.Config) *vm.Importer {
|
im: func(vmCfg vm.Config) *vm.Importer {
|
||||||
importer, err := vm.NewImporter(vmCfg)
|
importer, err := vm.NewImporter(context.Background(), vmCfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("error init importer: %s", err)
|
t.Fatalf("error init importer: %s", err)
|
||||||
}
|
}
|
||||||
|
@ -95,7 +96,7 @@ func Test_prometheusProcessor_run(t *testing.T) {
|
||||||
return client
|
return client
|
||||||
},
|
},
|
||||||
im: func(vmCfg vm.Config) *vm.Importer {
|
im: func(vmCfg vm.Config) *vm.Importer {
|
||||||
importer, err := vm.NewImporter(vmCfg)
|
importer, err := vm.NewImporter(context.Background(), vmCfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("error init importer: %s", err)
|
t.Fatalf("error init importer: %s", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -110,6 +110,7 @@ func TestRemoteRead(t *testing.T) {
|
||||||
|
|
||||||
for _, tt := range testCases {
|
for _, tt := range testCases {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
remoteReadServer := remote_read_integration.NewRemoteReadServer(t)
|
remoteReadServer := remote_read_integration.NewRemoteReadServer(t)
|
||||||
defer remoteReadServer.Close()
|
defer remoteReadServer.Close()
|
||||||
remoteWriteServer := remote_read_integration.NewRemoteWriteServer(t)
|
remoteWriteServer := remote_read_integration.NewRemoteWriteServer(t)
|
||||||
|
@ -139,7 +140,7 @@ func TestRemoteRead(t *testing.T) {
|
||||||
|
|
||||||
tt.vmCfg.Addr = remoteWriteServer.URL()
|
tt.vmCfg.Addr = remoteWriteServer.URL()
|
||||||
|
|
||||||
importer, err := vm.NewImporter(tt.vmCfg)
|
importer, err := vm.NewImporter(ctx, tt.vmCfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create VM importer: %s", err)
|
t.Fatalf("failed to create VM importer: %s", err)
|
||||||
}
|
}
|
||||||
|
@ -156,7 +157,6 @@ func TestRemoteRead(t *testing.T) {
|
||||||
cc: 1,
|
cc: 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
err = rmp.run(ctx, true, false)
|
err = rmp.run(ctx, true, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to run remote read processor: %s", err)
|
t.Fatalf("failed to run remote read processor: %s", err)
|
||||||
|
@ -263,6 +263,7 @@ func TestSteamRemoteRead(t *testing.T) {
|
||||||
|
|
||||||
for _, tt := range testCases {
|
for _, tt := range testCases {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
remoteReadServer := remote_read_integration.NewRemoteReadStreamServer(t)
|
remoteReadServer := remote_read_integration.NewRemoteReadStreamServer(t)
|
||||||
defer remoteReadServer.Close()
|
defer remoteReadServer.Close()
|
||||||
remoteWriteServer := remote_read_integration.NewRemoteWriteServer(t)
|
remoteWriteServer := remote_read_integration.NewRemoteWriteServer(t)
|
||||||
|
@ -292,7 +293,7 @@ func TestSteamRemoteRead(t *testing.T) {
|
||||||
|
|
||||||
tt.vmCfg.Addr = remoteWriteServer.URL()
|
tt.vmCfg.Addr = remoteWriteServer.URL()
|
||||||
|
|
||||||
importer, err := vm.NewImporter(tt.vmCfg)
|
importer, err := vm.NewImporter(ctx, tt.vmCfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create VM importer: %s", err)
|
t.Fatalf("failed to create VM importer: %s", err)
|
||||||
}
|
}
|
||||||
|
@ -309,7 +310,6 @@ func TestSteamRemoteRead(t *testing.T) {
|
||||||
cc: 1,
|
cc: 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
err = rmp.run(ctx, true, false)
|
err = rmp.run(ctx, true, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to run remote read processor: %s", err)
|
t.Fatalf("failed to run remote read processor: %s", err)
|
||||||
|
|
|
@ -157,7 +157,7 @@ func (c *Client) do(req *http.Request) (*http.Response, error) {
|
||||||
// Ping checks the health of the read source
|
// Ping checks the health of the read source
|
||||||
func (c *Client) Ping() error {
|
func (c *Client) Ping() error {
|
||||||
url := c.addr + healthPath
|
url := c.addr + healthPath
|
||||||
req, err := http.NewRequest("GET", url, nil)
|
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot create request to %q: %s", url, err)
|
return fmt.Errorf("cannot create request to %q: %s", url, err)
|
||||||
}
|
}
|
||||||
|
@ -174,7 +174,7 @@ func (c *Client) Ping() error {
|
||||||
func (c *Client) fetch(ctx context.Context, data []byte, streamCb StreamCallback) error {
|
func (c *Client) fetch(ctx context.Context, data []byte, streamCb StreamCallback) error {
|
||||||
r := bytes.NewReader(data)
|
r := bytes.NewReader(data)
|
||||||
url := c.addr + remoteReadPath
|
url := c.addr + remoteReadPath
|
||||||
req, err := http.NewRequest("POST", url, r)
|
req, err := http.NewRequest(http.MethodPost, url, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create new HTTP request: %w", err)
|
return fmt.Errorf("failed to create new HTTP request: %w", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,15 +3,16 @@ package vm
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"compress/gzip"
|
"compress/gzip"
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"math"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/backoff"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/barpool"
|
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/barpool"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/limiter"
|
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/limiter"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/decimal"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/decimal"
|
||||||
|
@ -75,7 +76,8 @@ type Importer struct {
|
||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
once sync.Once
|
once sync.Once
|
||||||
|
|
||||||
s *stats
|
s *stats
|
||||||
|
backoff *backoff.Backoff
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResetStats resets im stats.
|
// ResetStats resets im stats.
|
||||||
|
@ -107,7 +109,7 @@ func AddExtraLabelsToImportPath(path string, extraLabels []string) (string, erro
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewImporter creates new Importer for the given cfg.
|
// NewImporter creates new Importer for the given cfg.
|
||||||
func NewImporter(cfg Config) (*Importer, error) {
|
func NewImporter(ctx context.Context, cfg Config) (*Importer, error) {
|
||||||
if cfg.Concurrency < 1 {
|
if cfg.Concurrency < 1 {
|
||||||
return nil, fmt.Errorf("concurrency can't be lower than 1")
|
return nil, fmt.Errorf("concurrency can't be lower than 1")
|
||||||
}
|
}
|
||||||
|
@ -136,6 +138,7 @@ func NewImporter(cfg Config) (*Importer, error) {
|
||||||
close: make(chan struct{}),
|
close: make(chan struct{}),
|
||||||
input: make(chan *TimeSeries, cfg.Concurrency*4),
|
input: make(chan *TimeSeries, cfg.Concurrency*4),
|
||||||
errors: make(chan *ImportError, cfg.Concurrency),
|
errors: make(chan *ImportError, cfg.Concurrency),
|
||||||
|
backoff: backoff.New(),
|
||||||
}
|
}
|
||||||
if err := im.Ping(); err != nil {
|
if err := im.Ping(); err != nil {
|
||||||
return nil, fmt.Errorf("ping to %q failed: %s", addr, err)
|
return nil, fmt.Errorf("ping to %q failed: %s", addr, err)
|
||||||
|
@ -154,7 +157,7 @@ func NewImporter(cfg Config) (*Importer, error) {
|
||||||
}
|
}
|
||||||
go func(bar *pb.ProgressBar) {
|
go func(bar *pb.ProgressBar) {
|
||||||
defer im.wg.Done()
|
defer im.wg.Done()
|
||||||
im.startWorker(bar, cfg.BatchSize, cfg.SignificantFigures, cfg.RoundDigits)
|
im.startWorker(ctx, bar, cfg.BatchSize, cfg.SignificantFigures, cfg.RoundDigits)
|
||||||
}(bar)
|
}(bar)
|
||||||
}
|
}
|
||||||
im.ResetStats()
|
im.ResetStats()
|
||||||
|
@ -205,7 +208,7 @@ func (im *Importer) Close() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (im *Importer) startWorker(bar *pb.ProgressBar, batchSize, significantFigures, roundDigits int) {
|
func (im *Importer) startWorker(ctx context.Context, bar *pb.ProgressBar, batchSize, significantFigures, roundDigits int) {
|
||||||
var batch []*TimeSeries
|
var batch []*TimeSeries
|
||||||
var dataPoints int
|
var dataPoints int
|
||||||
var waitForBatch time.Time
|
var waitForBatch time.Time
|
||||||
|
@ -219,7 +222,9 @@ func (im *Importer) startWorker(bar *pb.ProgressBar, batchSize, significantFigur
|
||||||
exitErr := &ImportError{
|
exitErr := &ImportError{
|
||||||
Batch: batch,
|
Batch: batch,
|
||||||
}
|
}
|
||||||
if err := im.Import(batch); err != nil {
|
retryableFunc := func() error { return im.Import(batch) }
|
||||||
|
_, err := im.backoff.Retry(ctx, retryableFunc)
|
||||||
|
if err != nil {
|
||||||
exitErr.Err = err
|
exitErr.Err = err
|
||||||
}
|
}
|
||||||
im.errors <- exitErr
|
im.errors <- exitErr
|
||||||
|
@ -249,7 +254,7 @@ func (im *Importer) startWorker(bar *pb.ProgressBar, batchSize, significantFigur
|
||||||
im.s.idleDuration += time.Since(waitForBatch)
|
im.s.idleDuration += time.Since(waitForBatch)
|
||||||
im.s.Unlock()
|
im.s.Unlock()
|
||||||
|
|
||||||
if err := im.flush(batch); err != nil {
|
if err := im.flush(ctx, batch); err != nil {
|
||||||
im.errors <- &ImportError{
|
im.errors <- &ImportError{
|
||||||
Batch: batch,
|
Batch: batch,
|
||||||
Err: err,
|
Err: err,
|
||||||
|
@ -264,36 +269,22 @@ func (im *Importer) startWorker(bar *pb.ProgressBar, batchSize, significantFigur
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
func (im *Importer) flush(ctx context.Context, b []*TimeSeries) error {
|
||||||
// TODO: make configurable
|
retryableFunc := func() error { return im.Import(b) }
|
||||||
backoffRetries = 5
|
attempts, err := im.backoff.Retry(ctx, retryableFunc)
|
||||||
backoffFactor = 1.7
|
if err != nil {
|
||||||
backoffMinDuration = time.Second
|
return fmt.Errorf("import failed with %d retries: %s", attempts, err)
|
||||||
)
|
|
||||||
|
|
||||||
func (im *Importer) flush(b []*TimeSeries) error {
|
|
||||||
var err error
|
|
||||||
for i := 0; i < backoffRetries; i++ {
|
|
||||||
err = im.Import(b)
|
|
||||||
if err == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if errors.Is(err, ErrBadRequest) {
|
|
||||||
return err // fail fast if not recoverable
|
|
||||||
}
|
|
||||||
im.s.Lock()
|
|
||||||
im.s.retries++
|
|
||||||
im.s.Unlock()
|
|
||||||
backoff := float64(backoffMinDuration) * math.Pow(backoffFactor, float64(i))
|
|
||||||
time.Sleep(time.Duration(backoff))
|
|
||||||
}
|
}
|
||||||
return fmt.Errorf("import failed with %d retries: %s", backoffRetries, err)
|
im.s.Lock()
|
||||||
|
im.s.retries = attempts
|
||||||
|
im.s.Unlock()
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ping sends a ping to im.addr.
|
// Ping sends a ping to im.addr.
|
||||||
func (im *Importer) Ping() error {
|
func (im *Importer) Ping() error {
|
||||||
url := fmt.Sprintf("%s/health", im.addr)
|
url := fmt.Sprintf("%s/health", im.addr)
|
||||||
req, err := http.NewRequest("GET", url, nil)
|
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot create request to %q: %s", im.addr, err)
|
return fmt.Errorf("cannot create request to %q: %s", im.addr, err)
|
||||||
}
|
}
|
||||||
|
@ -317,7 +308,7 @@ func (im *Importer) Import(tsBatch []*TimeSeries) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
pr, pw := io.Pipe()
|
pr, pw := io.Pipe()
|
||||||
req, err := http.NewRequest("POST", im.importPath, pr)
|
req, err := http.NewRequest(http.MethodPost, im.importPath, pr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot create request to %q: %s", im.addr, err)
|
return fmt.Errorf("cannot create request to %q: %s", im.addr, err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -147,7 +147,7 @@ func (p *vmNativeProcessor) runSingle(ctx context.Context, f filter, srcURL, dst
|
||||||
sync := make(chan struct{})
|
sync := make(chan struct{})
|
||||||
go func() {
|
go func() {
|
||||||
defer func() { close(sync) }()
|
defer func() { close(sync) }()
|
||||||
req, err := http.NewRequestWithContext(ctx, "POST", dstURL, pr)
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, dstURL, pr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("cannot create import request to %q: %s", p.dst.addr, err)
|
log.Fatalf("cannot create import request to %q: %s", p.dst.addr, err)
|
||||||
}
|
}
|
||||||
|
@ -198,7 +198,7 @@ func (p *vmNativeProcessor) runSingle(ctx context.Context, f filter, srcURL, dst
|
||||||
|
|
||||||
func (p *vmNativeProcessor) getSourceTenants(ctx context.Context, f filter) ([]string, error) {
|
func (p *vmNativeProcessor) getSourceTenants(ctx context.Context, f filter) ([]string, error) {
|
||||||
u := fmt.Sprintf("%s/%s", p.src.addr, nativeTenantsAddr)
|
u := fmt.Sprintf("%s/%s", p.src.addr, nativeTenantsAddr)
|
||||||
req, err := http.NewRequestWithContext(ctx, "GET", u, nil)
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("cannot create request to %q: %s", u, err)
|
return nil, fmt.Errorf("cannot create request to %q: %s", u, err)
|
||||||
}
|
}
|
||||||
|
@ -232,7 +232,7 @@ func (p *vmNativeProcessor) getSourceTenants(ctx context.Context, f filter) ([]s
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *vmNativeProcessor) exportPipe(ctx context.Context, url string, f filter) (io.ReadCloser, error) {
|
func (p *vmNativeProcessor) exportPipe(ctx context.Context, url string, f filter) (io.ReadCloser, error) {
|
||||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("cannot create request to %q: %s", p.src.addr, err)
|
return nil, fmt.Errorf("cannot create request to %q: %s", p.src.addr, err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,13 +48,13 @@ var (
|
||||||
"See also -influxListenAddr.useProxyProtocol")
|
"See also -influxListenAddr.useProxyProtocol")
|
||||||
influxUseProxyProtocol = flag.Bool("influxListenAddr.useProxyProtocol", false, "Whether to use proxy protocol for connections accepted at -influxListenAddr . "+
|
influxUseProxyProtocol = flag.Bool("influxListenAddr.useProxyProtocol", false, "Whether to use proxy protocol for connections accepted at -influxListenAddr . "+
|
||||||
"See https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt")
|
"See https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt")
|
||||||
opentsdbListenAddr = flag.String("opentsdbListenAddr", "", "TCP and UDP address to listen for OpentTSDB metrics. "+
|
opentsdbListenAddr = flag.String("opentsdbListenAddr", "", "TCP and UDP address to listen for OpenTSDB metrics. "+
|
||||||
"Telnet put messages and HTTP /api/put messages are simultaneously served on TCP port. "+
|
"Telnet put messages and HTTP /api/put messages are simultaneously served on TCP port. "+
|
||||||
"Usually :4242 must be set. Doesn't work if empty. "+
|
"Usually :4242 must be set. Doesn't work if empty. "+
|
||||||
"See also -opentsdbListenAddr.useProxyProtocol")
|
"See also -opentsdbListenAddr.useProxyProtocol")
|
||||||
opentsdbUseProxyProtocol = flag.Bool("opentsdbListenAddr.useProxyProtocol", false, "Whether to use proxy protocol for connections accepted at -opentsdbListenAddr . "+
|
opentsdbUseProxyProtocol = flag.Bool("opentsdbListenAddr.useProxyProtocol", false, "Whether to use proxy protocol for connections accepted at -opentsdbListenAddr . "+
|
||||||
"See https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt")
|
"See https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt")
|
||||||
opentsdbHTTPListenAddr = flag.String("opentsdbHTTPListenAddr", "", "TCP address to listen for OpentTSDB HTTP put requests. Usually :4242 must be set. Doesn't work if empty. "+
|
opentsdbHTTPListenAddr = flag.String("opentsdbHTTPListenAddr", "", "TCP address to listen for OpenTSDB HTTP put requests. Usually :4242 must be set. Doesn't work if empty. "+
|
||||||
"See also -opentsdbHTTPListenAddr.useProxyProtocol")
|
"See also -opentsdbHTTPListenAddr.useProxyProtocol")
|
||||||
opentsdbHTTPUseProxyProtocol = flag.Bool("opentsdbHTTPListenAddr.useProxyProtocol", false, "Whether to use proxy protocol for connections accepted "+
|
opentsdbHTTPUseProxyProtocol = flag.Bool("opentsdbHTTPListenAddr.useProxyProtocol", false, "Whether to use proxy protocol for connections accepted "+
|
||||||
"at -opentsdbHTTPListenAddr . See https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt")
|
"at -opentsdbHTTPListenAddr . See https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt")
|
||||||
|
@ -157,6 +157,9 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
|
||||||
}
|
}
|
||||||
switch path {
|
switch path {
|
||||||
case "/prometheus/api/v1/write", "/api/v1/write":
|
case "/prometheus/api/v1/write", "/api/v1/write":
|
||||||
|
if common.HandleVMProtoServerHandshake(w, r) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
prometheusWriteRequests.Inc()
|
prometheusWriteRequests.Inc()
|
||||||
if err := promremotewrite.InsertHandler(r); err != nil {
|
if err := promremotewrite.InsertHandler(r); err != nil {
|
||||||
prometheusWriteErrors.Inc()
|
prometheusWriteErrors.Inc()
|
||||||
|
|
|
@ -35,8 +35,9 @@ var (
|
||||||
maxQueueDuration = flag.Duration("search.maxQueueDuration", 10*time.Second, "The maximum time the request waits for execution when -search.maxConcurrentRequests "+
|
maxQueueDuration = flag.Duration("search.maxQueueDuration", 10*time.Second, "The maximum time the request waits for execution when -search.maxConcurrentRequests "+
|
||||||
"limit is reached; see also -search.maxQueryDuration")
|
"limit is reached; see also -search.maxQueryDuration")
|
||||||
resetCacheAuthKey = flag.String("search.resetCacheAuthKey", "", "Optional authKey for resetting rollup cache via /internal/resetRollupResultCache call")
|
resetCacheAuthKey = flag.String("search.resetCacheAuthKey", "", "Optional authKey for resetting rollup cache via /internal/resetRollupResultCache call")
|
||||||
logSlowQueryDuration = flag.Duration("search.logSlowQueryDuration", 5*time.Second, "Log queries with execution time exceeding this value. Zero disables slow query logging")
|
logSlowQueryDuration = flag.Duration("search.logSlowQueryDuration", 5*time.Second, "Log queries with execution time exceeding this value. Zero disables slow query logging. "+
|
||||||
vmalertProxyURL = flag.String("vmalert.proxyURL", "", "Optional URL for proxying requests to vmalert. For example, if -vmalert.proxyURL=http://vmalert:8880 , then alerting API requests such as /api/v1/rules from Grafana will be proxied to http://vmalert:8880/api/v1/rules")
|
"See also -search.logQueryMemoryUsage")
|
||||||
|
vmalertProxyURL = flag.String("vmalert.proxyURL", "", "Optional URL for proxying requests to vmalert. For example, if -vmalert.proxyURL=http://vmalert:8880 , then alerting API requests such as /api/v1/rules from Grafana will be proxied to http://vmalert:8880/api/v1/rules")
|
||||||
)
|
)
|
||||||
|
|
||||||
var slowQueries = metrics.NewCounter(`vm_slow_queries_total`)
|
var slowQueries = metrics.NewCounter(`vm_slow_queries_total`)
|
||||||
|
|
|
@ -759,6 +759,9 @@ func QueryHandler(qt *querytracer.Tracer, startTime time.Time, w http.ResponseWr
|
||||||
LookbackDelta: lookbackDelta,
|
LookbackDelta: lookbackDelta,
|
||||||
RoundDigits: getRoundDigits(r),
|
RoundDigits: getRoundDigits(r),
|
||||||
EnforcedTagFilterss: etfs,
|
EnforcedTagFilterss: etfs,
|
||||||
|
GetRequestURI: func() string {
|
||||||
|
return httpserver.GetRequestURI(r)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
result, err := promql.Exec(qt, &ec, query, true)
|
result, err := promql.Exec(qt, &ec, query, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -860,6 +863,9 @@ func queryRangeHandler(qt *querytracer.Tracer, startTime time.Time, w http.Respo
|
||||||
LookbackDelta: lookbackDelta,
|
LookbackDelta: lookbackDelta,
|
||||||
RoundDigits: getRoundDigits(r),
|
RoundDigits: getRoundDigits(r),
|
||||||
EnforcedTagFilterss: etfs,
|
EnforcedTagFilterss: etfs,
|
||||||
|
GetRequestURI: func() string {
|
||||||
|
return httpserver.GetRequestURI(r)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
result, err := promql.Exec(qt, &ec, query, false)
|
result, err := promql.Exec(qt, &ec, query, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -1013,8 +1019,10 @@ func getRoundDigits(r *http.Request) int {
|
||||||
|
|
||||||
func getLatencyOffsetMilliseconds(r *http.Request) (int64, error) {
|
func getLatencyOffsetMilliseconds(r *http.Request) (int64, error) {
|
||||||
d := latencyOffset.Milliseconds()
|
d := latencyOffset.Milliseconds()
|
||||||
if d <= 1000 {
|
if d < 0 {
|
||||||
d = 1000
|
// Zero latency offset may be useful for some use cases.
|
||||||
|
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2061#issuecomment-1299109836
|
||||||
|
d = 0
|
||||||
}
|
}
|
||||||
return searchutils.GetDuration(r, "latency_offset", d)
|
return searchutils.GetDuration(r, "latency_offset", d)
|
||||||
}
|
}
|
||||||
|
|
|
@ -200,7 +200,7 @@ func TestAdjustLastPoints(t *testing.T) {
|
||||||
func TestGetLatencyOffsetMillisecondsSuccess(t *testing.T) {
|
func TestGetLatencyOffsetMillisecondsSuccess(t *testing.T) {
|
||||||
f := func(url string, expectedOffset int64) {
|
f := func(url string, expectedOffset int64) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
r, err := http.NewRequest("GET", url, nil)
|
r, err := http.NewRequest(http.MethodGet, url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected error in NewRequest(%q): %s", url, err)
|
t.Fatalf("unexpected error in NewRequest(%q): %s", url, err)
|
||||||
}
|
}
|
||||||
|
@ -219,7 +219,7 @@ func TestGetLatencyOffsetMillisecondsSuccess(t *testing.T) {
|
||||||
func TestGetLatencyOffsetMillisecondsFailure(t *testing.T) {
|
func TestGetLatencyOffsetMillisecondsFailure(t *testing.T) {
|
||||||
f := func(url string) {
|
f := func(url string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
r, err := http.NewRequest("GET", url, nil)
|
r, err := http.NewRequest(http.MethodGet, url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected error in NewRequest(%q): %s", url, err)
|
t.Fatalf("unexpected error in NewRequest(%q): %s", url, err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,11 @@ var (
|
||||||
"See https://valyala.medium.com/prometheus-subqueries-in-victoriametrics-9b1492b720b3")
|
"See https://valyala.medium.com/prometheus-subqueries-in-victoriametrics-9b1492b720b3")
|
||||||
maxMemoryPerQuery = flagutil.NewBytes("search.maxMemoryPerQuery", 0, "The maximum amounts of memory a single query may consume. "+
|
maxMemoryPerQuery = flagutil.NewBytes("search.maxMemoryPerQuery", 0, "The maximum amounts of memory a single query may consume. "+
|
||||||
"Queries requiring more memory are rejected. The total memory limit for concurrently executed queries can be estimated "+
|
"Queries requiring more memory are rejected. The total memory limit for concurrently executed queries can be estimated "+
|
||||||
"as -search.maxMemoryPerQuery multiplied by -search.maxConcurrentRequests")
|
"as -search.maxMemoryPerQuery multiplied by -search.maxConcurrentRequests . "+
|
||||||
|
"See also -search.logQueryMemoryUsage")
|
||||||
|
logQueryMemoryUsage = flagutil.NewBytes("search.logQueryMemoryUsage", 0, "Log queries, which require more memory than specified by this flag. "+
|
||||||
|
"This may help detecting and optimizing heavy queries. Query logging is disabled by default. "+
|
||||||
|
"See also -search.logSlowQueryDuration and -search.maxMemoryPerQuery")
|
||||||
noStaleMarkers = flag.Bool("search.noStaleMarkers", false, "Set this flag to true if the database doesn't contain Prometheus stale markers, "+
|
noStaleMarkers = flag.Bool("search.noStaleMarkers", false, "Set this flag to true if the database doesn't contain Prometheus stale markers, "+
|
||||||
"so there is no need in spending additional CPU time on its handling. Staleness markers may exist only in data obtained from Prometheus scrape targets")
|
"so there is no need in spending additional CPU time on its handling. Staleness markers may exist only in data obtained from Prometheus scrape targets")
|
||||||
)
|
)
|
||||||
|
@ -123,6 +127,10 @@ type EvalConfig struct {
|
||||||
// EnforcedTagFilterss may contain additional label filters to use in the query.
|
// EnforcedTagFilterss may contain additional label filters to use in the query.
|
||||||
EnforcedTagFilterss [][]storage.TagFilter
|
EnforcedTagFilterss [][]storage.TagFilter
|
||||||
|
|
||||||
|
// The callback, which returns the request URI during logging.
|
||||||
|
// The request URI isn't stored here because its' construction may take non-trivial amounts of CPU.
|
||||||
|
GetRequestURI func() string
|
||||||
|
|
||||||
timestamps []int64
|
timestamps []int64
|
||||||
timestampsOnce sync.Once
|
timestampsOnce sync.Once
|
||||||
}
|
}
|
||||||
|
@ -140,6 +148,7 @@ func copyEvalConfig(src *EvalConfig) *EvalConfig {
|
||||||
ec.LookbackDelta = src.LookbackDelta
|
ec.LookbackDelta = src.LookbackDelta
|
||||||
ec.RoundDigits = src.RoundDigits
|
ec.RoundDigits = src.RoundDigits
|
||||||
ec.EnforcedTagFilterss = src.EnforcedTagFilterss
|
ec.EnforcedTagFilterss = src.EnforcedTagFilterss
|
||||||
|
ec.GetRequestURI = src.GetRequestURI
|
||||||
|
|
||||||
// do not copy src.timestamps - they must be generated again.
|
// do not copy src.timestamps - they must be generated again.
|
||||||
return &ec
|
return &ec
|
||||||
|
@ -1079,26 +1088,33 @@ func evalRollupFuncWithMetricExpr(qt *querytracer.Tracer, ec *EvalConfig, funcNa
|
||||||
}
|
}
|
||||||
rollupPoints := mulNoOverflow(pointsPerTimeseries, int64(timeseriesLen*len(rcs)))
|
rollupPoints := mulNoOverflow(pointsPerTimeseries, int64(timeseriesLen*len(rcs)))
|
||||||
rollupMemorySize = sumNoOverflow(mulNoOverflow(int64(rssLen), 1000), mulNoOverflow(rollupPoints, 16))
|
rollupMemorySize = sumNoOverflow(mulNoOverflow(int64(rssLen), 1000), mulNoOverflow(rollupPoints, 16))
|
||||||
|
if maxMemory := int64(logQueryMemoryUsage.N); maxMemory > 0 && rollupMemorySize > maxMemory {
|
||||||
|
requestURI := ec.GetRequestURI()
|
||||||
|
logger.Warnf("remoteAddr=%s, requestURI=%s: the %s requires %d bytes of memory for processing; "+
|
||||||
|
"logging this query, since it exceeds the -search.logQueryMemoryUsage=%d; "+
|
||||||
|
"the query selects %d time series and generates %d points across all the time series; try reducing the number of selected time series",
|
||||||
|
ec.QuotedRemoteAddr, requestURI, expr.AppendString(nil), rollupMemorySize, maxMemory, timeseriesLen*len(rcs), rollupPoints)
|
||||||
|
}
|
||||||
if maxMemory := int64(maxMemoryPerQuery.N); maxMemory > 0 && rollupMemorySize > maxMemory {
|
if maxMemory := int64(maxMemoryPerQuery.N); maxMemory > 0 && rollupMemorySize > maxMemory {
|
||||||
rss.Cancel()
|
rss.Cancel()
|
||||||
return nil, &UserReadableError{
|
return nil, &UserReadableError{
|
||||||
Err: fmt.Errorf("not enough memory for processing %d data points across %d time series with %d points in each time series "+
|
Err: fmt.Errorf("not enough memory for processing %s, which returns %d data points across %d time series with %d points in each time series "+
|
||||||
"according to -search.maxMemoryPerQuery=%d; requested memory: %d bytes; "+
|
"according to -search.maxMemoryPerQuery=%d; requested memory: %d bytes; "+
|
||||||
"possible solutions are: reducing the number of matching time series; increasing `step` query arg (step=%gs); "+
|
"possible solutions are: reducing the number of matching time series; increasing `step` query arg (step=%gs); "+
|
||||||
"increasing -search.maxMemoryPerQuery",
|
"increasing -search.maxMemoryPerQuery",
|
||||||
rollupPoints, timeseriesLen*len(rcs), pointsPerTimeseries, maxMemory, rollupMemorySize, float64(ec.Step)/1e3),
|
expr.AppendString(nil), rollupPoints, timeseriesLen*len(rcs), pointsPerTimeseries, maxMemory, rollupMemorySize, float64(ec.Step)/1e3),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
rml := getRollupMemoryLimiter()
|
rml := getRollupMemoryLimiter()
|
||||||
if !rml.Get(uint64(rollupMemorySize)) {
|
if !rml.Get(uint64(rollupMemorySize)) {
|
||||||
rss.Cancel()
|
rss.Cancel()
|
||||||
return nil, &UserReadableError{
|
return nil, &UserReadableError{
|
||||||
Err: fmt.Errorf("not enough memory for processing %d data points across %d time series with %d points in each time series; "+
|
Err: fmt.Errorf("not enough memory for processing %s, which returns %d data points across %d time series with %d points in each time series; "+
|
||||||
"total available memory for concurrent requests: %d bytes; "+
|
"total available memory for concurrent requests: %d bytes; "+
|
||||||
"requested memory: %d bytes; "+
|
"requested memory: %d bytes; "+
|
||||||
"possible solutions are: reducing the number of matching time series; increasing `step` query arg (step=%gs); "+
|
"possible solutions are: reducing the number of matching time series; increasing `step` query arg (step=%gs); "+
|
||||||
"switching to node with more RAM; increasing -memory.allowedPercent",
|
"switching to node with more RAM; increasing -memory.allowedPercent",
|
||||||
rollupPoints, timeseriesLen*len(rcs), pointsPerTimeseries, rml.MaxSize, uint64(rollupMemorySize), float64(ec.Step)/1e3),
|
expr.AppendString(nil), rollupPoints, timeseriesLen*len(rcs), pointsPerTimeseries, rml.MaxSize, uint64(rollupMemorySize), float64(ec.Step)/1e3),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
defer rml.Put(uint64(rollupMemorySize))
|
defer rml.Put(uint64(rollupMemorySize))
|
||||||
|
|
|
@ -6212,7 +6212,7 @@ func TestExecSuccess(t *testing.T) {
|
||||||
q := `interpolate(time() < 1300)`
|
q := `interpolate(time() < 1300)`
|
||||||
r1 := netstorage.Result{
|
r1 := netstorage.Result{
|
||||||
MetricName: metricNameExpected,
|
MetricName: metricNameExpected,
|
||||||
Values: []float64{1000, 1200, 1200, 1200, 1200, 1200},
|
Values: []float64{1000, 1200, nan, nan, nan, nan},
|
||||||
Timestamps: timestampsExpected,
|
Timestamps: timestampsExpected,
|
||||||
}
|
}
|
||||||
resultExpected := []netstorage.Result{r1}
|
resultExpected := []netstorage.Result{r1}
|
||||||
|
@ -6223,7 +6223,18 @@ func TestExecSuccess(t *testing.T) {
|
||||||
q := `interpolate(time() > 1500)`
|
q := `interpolate(time() > 1500)`
|
||||||
r1 := netstorage.Result{
|
r1 := netstorage.Result{
|
||||||
MetricName: metricNameExpected,
|
MetricName: metricNameExpected,
|
||||||
Values: []float64{1600, 1600, 1600, 1600, 1800, 2000},
|
Values: []float64{nan, nan, nan, 1600, 1800, 2000},
|
||||||
|
Timestamps: timestampsExpected,
|
||||||
|
}
|
||||||
|
resultExpected := []netstorage.Result{r1}
|
||||||
|
f(q, resultExpected)
|
||||||
|
})
|
||||||
|
t.Run(`interpolate(tail_head_and_middle)`, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
q := `interpolate(time() > 1100 and time() < 1300 default time() > 1700 and time() < 1900)`
|
||||||
|
r1 := netstorage.Result{
|
||||||
|
MetricName: metricNameExpected,
|
||||||
|
Values: []float64{nan, 1200, 1400, 1600, 1800, nan},
|
||||||
Timestamps: timestampsExpected,
|
Timestamps: timestampsExpected,
|
||||||
}
|
}
|
||||||
resultExpected := []netstorage.Result{r1}
|
resultExpected := []netstorage.Result{r1}
|
||||||
|
|
|
@ -1169,7 +1169,8 @@ func transformInterpolate(tfa *transformFuncArg) ([]*timeseries, error) {
|
||||||
}
|
}
|
||||||
rvs := args[0]
|
rvs := args[0]
|
||||||
for _, ts := range rvs {
|
for _, ts := range rvs {
|
||||||
values := ts.Values
|
values := skipLeadingNaNs(ts.Values)
|
||||||
|
values = skipTrailingNaNs(values)
|
||||||
if len(values) == 0 {
|
if len(values) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ func TestGetDurationSuccess(t *testing.T) {
|
||||||
f := func(s string, dExpected int64) {
|
f := func(s string, dExpected int64) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
urlStr := fmt.Sprintf("http://foo.bar/baz?s=%s", url.QueryEscape(s))
|
urlStr := fmt.Sprintf("http://foo.bar/baz?s=%s", url.QueryEscape(s))
|
||||||
r, err := http.NewRequest("GET", urlStr, nil)
|
r, err := http.NewRequest(http.MethodGet, urlStr, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected error in NewRequest: %s", err)
|
t.Fatalf("unexpected error in NewRequest: %s", err)
|
||||||
}
|
}
|
||||||
|
@ -54,7 +54,7 @@ func TestGetDurationError(t *testing.T) {
|
||||||
f := func(s string) {
|
f := func(s string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
urlStr := fmt.Sprintf("http://foo.bar/baz?s=%s", url.QueryEscape(s))
|
urlStr := fmt.Sprintf("http://foo.bar/baz?s=%s", url.QueryEscape(s))
|
||||||
r, err := http.NewRequest("GET", urlStr, nil)
|
r, err := http.NewRequest(http.MethodGet, urlStr, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected error in NewRequest: %s", err)
|
t.Fatalf("unexpected error in NewRequest: %s", err)
|
||||||
}
|
}
|
||||||
|
@ -78,7 +78,7 @@ func TestGetTimeSuccess(t *testing.T) {
|
||||||
f := func(s string, timestampExpected int64) {
|
f := func(s string, timestampExpected int64) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
urlStr := fmt.Sprintf("http://foo.bar/baz?s=%s", url.QueryEscape(s))
|
urlStr := fmt.Sprintf("http://foo.bar/baz?s=%s", url.QueryEscape(s))
|
||||||
r, err := http.NewRequest("GET", urlStr, nil)
|
r, err := http.NewRequest(http.MethodGet, urlStr, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected error in NewRequest: %s", err)
|
t.Fatalf("unexpected error in NewRequest: %s", err)
|
||||||
}
|
}
|
||||||
|
@ -127,7 +127,7 @@ func TestGetTimeError(t *testing.T) {
|
||||||
f := func(s string) {
|
f := func(s string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
urlStr := fmt.Sprintf("http://foo.bar/baz?s=%s", url.QueryEscape(s))
|
urlStr := fmt.Sprintf("http://foo.bar/baz?s=%s", url.QueryEscape(s))
|
||||||
r, err := http.NewRequest("GET", urlStr, nil)
|
r, err := http.NewRequest(http.MethodGet, urlStr, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected error in NewRequest: %s", err)
|
t.Fatalf("unexpected error in NewRequest: %s", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
{
|
{
|
||||||
"files": {
|
"files": {
|
||||||
"main.css": "./static/css/main.b9c2d13c.css",
|
"main.css": "./static/css/main.5c28f4a7.css",
|
||||||
"main.js": "./static/js/main.44784d74.js",
|
"main.js": "./static/js/main.0be86920.js",
|
||||||
"static/js/27.c1ccfd29.chunk.js": "./static/js/27.c1ccfd29.chunk.js",
|
"static/js/27.c1ccfd29.chunk.js": "./static/js/27.c1ccfd29.chunk.js",
|
||||||
"static/media/Lato-Regular.ttf": "./static/media/Lato-Regular.d714fec1633b69a9c2e9.ttf",
|
"static/media/Lato-Regular.ttf": "./static/media/Lato-Regular.d714fec1633b69a9c2e9.ttf",
|
||||||
"static/media/Lato-Bold.ttf": "./static/media/Lato-Bold.32360ba4b57802daa4d6.ttf",
|
"static/media/Lato-Bold.ttf": "./static/media/Lato-Bold.32360ba4b57802daa4d6.ttf",
|
||||||
"index.html": "./index.html"
|
"index.html": "./index.html"
|
||||||
},
|
},
|
||||||
"entrypoints": [
|
"entrypoints": [
|
||||||
"static/css/main.b9c2d13c.css",
|
"static/css/main.5c28f4a7.css",
|
||||||
"static/js/main.44784d74.js"
|
"static/js/main.0be86920.js"
|
||||||
]
|
]
|
||||||
}
|
}
|
|
@ -1 +1 @@
|
||||||
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="./favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"/><meta name="theme-color" content="#000000"/><meta name="description" content="UI for VictoriaMetrics"/><link rel="apple-touch-icon" href="./apple-touch-icon.png"/><link rel="icon" type="image/png" sizes="32x32" href="./favicon-32x32.png"><link rel="manifest" href="./manifest.json"/><title>VM UI</title><script src="./dashboards/index.js" type="module"></script><meta name="twitter:card" content="summary_large_image"><meta name="twitter:image" content="./preview.jpg"><meta name="twitter:title" content="UI for VictoriaMetrics"><meta name="twitter:description" content="Explore and troubleshoot your VictoriaMetrics data"><meta name="twitter:site" content="@VictoriaMetrics"><meta property="og:title" content="Metric explorer for VictoriaMetrics"><meta property="og:description" content="Explore and troubleshoot your VictoriaMetrics data"><meta property="og:image" content="./preview.jpg"><meta property="og:type" content="website"><script defer="defer" src="./static/js/main.44784d74.js"></script><link href="./static/css/main.b9c2d13c.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="./favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"/><meta name="theme-color" content="#000000"/><meta name="description" content="UI for VictoriaMetrics"/><link rel="apple-touch-icon" href="./apple-touch-icon.png"/><link rel="icon" type="image/png" sizes="32x32" href="./favicon-32x32.png"><link rel="manifest" href="./manifest.json"/><title>VM UI</title><script src="./dashboards/index.js" type="module"></script><meta name="twitter:card" content="summary_large_image"><meta name="twitter:image" content="./preview.jpg"><meta name="twitter:title" content="UI for VictoriaMetrics"><meta name="twitter:description" content="Explore and troubleshoot your VictoriaMetrics data"><meta name="twitter:site" content="@VictoriaMetrics"><meta property="og:title" content="Metric explorer for VictoriaMetrics"><meta property="og:description" content="Explore and troubleshoot your VictoriaMetrics data"><meta property="og:image" content="./preview.jpg"><meta property="og:type" content="website"><script defer="defer" src="./static/js/main.0be86920.js"></script><link href="./static/css/main.5c28f4a7.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
1
app/vmselect/vmui/static/css/main.5c28f4a7.css
Normal file
1
app/vmselect/vmui/static/css/main.5c28f4a7.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
app/vmselect/vmui/static/js/main.0be86920.js
Normal file
2
app/vmselect/vmui/static/js/main.0be86920.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1,11 +1,15 @@
|
||||||
import React, { FC } from "preact/compat";
|
import React, { FC, useRef, useState } from "preact/compat";
|
||||||
import { useCustomPanelDispatch, useCustomPanelState } from "../../../state/customPanel/CustomPanelStateContext";
|
import { useCustomPanelDispatch, useCustomPanelState } from "../../../state/customPanel/CustomPanelStateContext";
|
||||||
import { useQueryDispatch, useQueryState } from "../../../state/query/QueryStateContext";
|
import { useQueryDispatch, useQueryState } from "../../../state/query/QueryStateContext";
|
||||||
import "./style.scss";
|
import "./style.scss";
|
||||||
import Switch from "../../Main/Switch/Switch";
|
import Switch from "../../Main/Switch/Switch";
|
||||||
|
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||||
|
import Popper from "../../Main/Popper/Popper";
|
||||||
|
import { TuneIcon } from "../../Main/Icons";
|
||||||
|
import Button from "../../Main/Button/Button";
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
const AdditionalSettings: FC = () => {
|
const AdditionalSettingsControls: FC<{isMobile?: boolean}> = ({ isMobile }) => {
|
||||||
|
|
||||||
const { autocomplete } = useQueryState();
|
const { autocomplete } = useQueryState();
|
||||||
const queryDispatch = useQueryDispatch();
|
const queryDispatch = useQueryDispatch();
|
||||||
|
|
||||||
|
@ -24,23 +28,72 @@ const AdditionalSettings: FC = () => {
|
||||||
queryDispatch({ type: "TOGGLE_AUTOCOMPLETE" });
|
queryDispatch({ type: "TOGGLE_AUTOCOMPLETE" });
|
||||||
};
|
};
|
||||||
|
|
||||||
return <div className="vm-additional-settings">
|
return (
|
||||||
<Switch
|
<div
|
||||||
label={"Autocomplete"}
|
className={classNames({
|
||||||
value={autocomplete}
|
"vm-additional-settings": true,
|
||||||
onChange={onChangeAutocomplete}
|
"vm-additional-settings_mobile": isMobile
|
||||||
/>
|
})}
|
||||||
<Switch
|
>
|
||||||
label={"Disable cache"}
|
<Switch
|
||||||
value={nocache}
|
label={"Autocomplete"}
|
||||||
onChange={onChangeCache}
|
value={autocomplete}
|
||||||
/>
|
onChange={onChangeAutocomplete}
|
||||||
<Switch
|
fullWidth={isMobile}
|
||||||
label={"Trace query"}
|
/>
|
||||||
value={isTracingEnabled}
|
<Switch
|
||||||
onChange={onChangeQueryTracing}
|
label={"Disable cache"}
|
||||||
/>
|
value={nocache}
|
||||||
</div>;
|
onChange={onChangeCache}
|
||||||
|
fullWidth={isMobile}
|
||||||
|
/>
|
||||||
|
<Switch
|
||||||
|
label={"Trace query"}
|
||||||
|
value={isTracingEnabled}
|
||||||
|
onChange={onChangeQueryTracing}
|
||||||
|
fullWidth={isMobile}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const AdditionalSettings: FC = () => {
|
||||||
|
const { isMobile } = useDeviceDetect();
|
||||||
|
const [openList, setOpenList] = useState(false);
|
||||||
|
const targetRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const handleToggleList = () => {
|
||||||
|
setOpenList(prev => !prev);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseList = () => {
|
||||||
|
setOpenList(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isMobile) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div ref={targetRef}>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
startIcon={<TuneIcon/>}
|
||||||
|
onClick={handleToggleList}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Popper
|
||||||
|
open={openList}
|
||||||
|
buttonRef={targetRef}
|
||||||
|
placement="bottom-left"
|
||||||
|
onClose={handleCloseList}
|
||||||
|
title={"Query settings"}
|
||||||
|
>
|
||||||
|
<AdditionalSettingsControls isMobile={isMobile}/>
|
||||||
|
</Popper>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <AdditionalSettingsControls/>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AdditionalSettings;
|
export default AdditionalSettings;
|
||||||
|
|
|
@ -5,10 +5,19 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 24px;
|
gap: $padding-global;
|
||||||
|
|
||||||
&__input {
|
&__input {
|
||||||
flex-basis: 160px;
|
flex-basis: 160px;
|
||||||
margin-bottom: -6px;
|
margin-bottom: -6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&_mobile {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 0 $padding-global;
|
||||||
|
gap: $padding-medium;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,13 +2,15 @@ import React, { FC, useMemo, useRef } from "preact/compat";
|
||||||
import { useCardinalityState, useCardinalityDispatch } from "../../../state/cardinality/CardinalityStateContext";
|
import { useCardinalityState, useCardinalityDispatch } from "../../../state/cardinality/CardinalityStateContext";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import Button from "../../Main/Button/Button";
|
import Button from "../../Main/Button/Button";
|
||||||
import { CalendarIcon } from "../../Main/Icons";
|
import { ArrowDownIcon, CalendarIcon } from "../../Main/Icons";
|
||||||
import Tooltip from "../../Main/Tooltip/Tooltip";
|
import Tooltip from "../../Main/Tooltip/Tooltip";
|
||||||
import { getAppModeEnable } from "../../../utils/app-mode";
|
import { getAppModeEnable } from "../../../utils/app-mode";
|
||||||
import { DATE_FORMAT } from "../../../constants/date";
|
import { DATE_FORMAT } from "../../../constants/date";
|
||||||
import DatePicker from "../../Main/DatePicker/DatePicker";
|
import DatePicker from "../../Main/DatePicker/DatePicker";
|
||||||
|
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||||
|
|
||||||
const CardinalityDatePicker: FC = () => {
|
const CardinalityDatePicker: FC = () => {
|
||||||
|
const { isMobile } = useDeviceDetect();
|
||||||
const appModeEnable = getAppModeEnable();
|
const appModeEnable = getAppModeEnable();
|
||||||
const buttonRef = useRef<HTMLDivElement>(null);
|
const buttonRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
@ -24,18 +26,30 @@ const CardinalityDatePicker: FC = () => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div ref={buttonRef}>
|
<div ref={buttonRef}>
|
||||||
<Tooltip title="Date control">
|
{isMobile ? (
|
||||||
<Button
|
<div className="vm-mobile-option">
|
||||||
className={appModeEnable ? "" : "vm-header-button"}
|
<span className="vm-mobile-option__icon"><CalendarIcon/></span>
|
||||||
variant="contained"
|
<div className="vm-mobile-option-text">
|
||||||
color="primary"
|
<span className="vm-mobile-option-text__label">Date control</span>
|
||||||
startIcon={<CalendarIcon/>}
|
<span className="vm-mobile-option-text__value">{dateFormatted}</span>
|
||||||
>
|
</div>
|
||||||
{dateFormatted}
|
<span className="vm-mobile-option__arrow"><ArrowDownIcon/></span>
|
||||||
</Button>
|
</div>
|
||||||
</Tooltip>
|
) : (
|
||||||
|
<Tooltip title="Date control">
|
||||||
|
<Button
|
||||||
|
className={appModeEnable ? "" : "vm-header-button"}
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
startIcon={<CalendarIcon/>}
|
||||||
|
>
|
||||||
|
{dateFormatted}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
|
label="Date control"
|
||||||
date={date || ""}
|
date={date || ""}
|
||||||
format={DATE_FORMAT}
|
format={DATE_FORMAT}
|
||||||
onChange={handleChangeDate}
|
onChange={handleChangeDate}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { FC, useEffect, useState } from "preact/compat";
|
import React, { FC, useEffect, useState } from "preact/compat";
|
||||||
import ServerConfigurator from "./ServerConfigurator/ServerConfigurator";
|
import ServerConfigurator from "./ServerConfigurator/ServerConfigurator";
|
||||||
import { useAppDispatch, useAppState } from "../../../state/common/StateContext";
|
import { useAppDispatch, useAppState } from "../../../state/common/StateContext";
|
||||||
import { SettingsIcon } from "../../Main/Icons";
|
import { ArrowDownIcon, SettingsIcon } from "../../Main/Icons";
|
||||||
import Button from "../../Main/Button/Button";
|
import Button from "../../Main/Button/Button";
|
||||||
import Modal from "../../Main/Modal/Modal";
|
import Modal from "../../Main/Modal/Modal";
|
||||||
import "./style.scss";
|
import "./style.scss";
|
||||||
|
@ -18,7 +18,7 @@ import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||||
|
|
||||||
const title = "Settings";
|
const title = "Settings";
|
||||||
|
|
||||||
const GlobalSettings: FC<{showTitle?: boolean}> = ({ showTitle }) => {
|
const GlobalSettings: FC = () => {
|
||||||
const { isMobile } = useDeviceDetect();
|
const { isMobile } = useDeviceDetect();
|
||||||
|
|
||||||
const appModeEnable = getAppModeEnable();
|
const appModeEnable = getAppModeEnable();
|
||||||
|
@ -42,7 +42,6 @@ const GlobalSettings: FC<{showTitle?: boolean}> = ({ showTitle }) => {
|
||||||
dispatch({ type: "SET_SERVER", payload: serverUrl });
|
dispatch({ type: "SET_SERVER", payload: serverUrl });
|
||||||
timeDispatch({ type: "SET_TIMEZONE", payload: timezone });
|
timeDispatch({ type: "SET_TIMEZONE", payload: timezone });
|
||||||
customPanelDispatch({ type: "SET_SERIES_LIMITS", payload: limits });
|
customPanelDispatch({ type: "SET_SERIES_LIMITS", payload: limits });
|
||||||
handleClose();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -51,22 +50,30 @@ const GlobalSettings: FC<{showTitle?: boolean}> = ({ showTitle }) => {
|
||||||
}, [stateServerUrl]);
|
}, [stateServerUrl]);
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<Tooltip
|
{isMobile ? (
|
||||||
open={showTitle === true ? false : undefined}
|
<div
|
||||||
title={title}
|
className="vm-mobile-option"
|
||||||
>
|
|
||||||
<Button
|
|
||||||
className={classNames({
|
|
||||||
"vm-header-button": !appModeEnable
|
|
||||||
})}
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
startIcon={<SettingsIcon/>}
|
|
||||||
onClick={handleOpen}
|
onClick={handleOpen}
|
||||||
>
|
>
|
||||||
{showTitle && title}
|
<span className="vm-mobile-option__icon"><SettingsIcon/></span>
|
||||||
</Button>
|
<div className="vm-mobile-option-text">
|
||||||
</Tooltip>
|
<span className="vm-mobile-option-text__label">{title}</span>
|
||||||
|
</div>
|
||||||
|
<span className="vm-mobile-option__arrow"><ArrowDownIcon/></span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Tooltip title={title}>
|
||||||
|
<Button
|
||||||
|
className={classNames({
|
||||||
|
"vm-header-button": !appModeEnable
|
||||||
|
})}
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
startIcon={<SettingsIcon/>}
|
||||||
|
onClick={handleOpen}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
{open && (
|
{open && (
|
||||||
<Modal
|
<Modal
|
||||||
title={title}
|
title={title}
|
||||||
|
@ -84,6 +91,7 @@ const GlobalSettings: FC<{showTitle?: boolean}> = ({ showTitle }) => {
|
||||||
serverUrl={serverUrl}
|
serverUrl={serverUrl}
|
||||||
onChange={setServerUrl}
|
onChange={setServerUrl}
|
||||||
onEnter={handlerApply}
|
onEnter={handlerApply}
|
||||||
|
onBlur={handlerApply}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -105,21 +113,6 @@ const GlobalSettings: FC<{showTitle?: boolean}> = ({ showTitle }) => {
|
||||||
<ThemeControl/>
|
<ThemeControl/>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -6,6 +6,8 @@ import { InfoIcon, RestartIcon } from "../../../Main/Icons";
|
||||||
import Button from "../../../Main/Button/Button";
|
import Button from "../../../Main/Button/Button";
|
||||||
import { DEFAULT_MAX_SERIES } from "../../../../constants/graph";
|
import { DEFAULT_MAX_SERIES } from "../../../../constants/graph";
|
||||||
import "./style.scss";
|
import "./style.scss";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import useDeviceDetect from "../../../../hooks/useDeviceDetect";
|
||||||
|
|
||||||
export interface ServerConfiguratorProps {
|
export interface ServerConfiguratorProps {
|
||||||
limits: SeriesLimits
|
limits: SeriesLimits
|
||||||
|
@ -20,6 +22,7 @@ const fields: {label: string, type: DisplayType}[] = [
|
||||||
];
|
];
|
||||||
|
|
||||||
const LimitsConfigurator: FC<ServerConfiguratorProps> = ({ limits, onChange , onEnter }) => {
|
const LimitsConfigurator: FC<ServerConfiguratorProps> = ({ limits, onChange , onEnter }) => {
|
||||||
|
const { isMobile } = useDeviceDetect();
|
||||||
|
|
||||||
const [error, setError] = useState({
|
const [error, setError] = useState({
|
||||||
table: "",
|
table: "",
|
||||||
|
@ -68,7 +71,12 @@ const LimitsConfigurator: FC<ServerConfiguratorProps> = ({ limits, onChange , on
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="vm-limits-configurator__inputs">
|
<div
|
||||||
|
className={classNames({
|
||||||
|
"vm-limits-configurator__inputs": true,
|
||||||
|
"vm-limits-configurator__inputs_mobile": isMobile
|
||||||
|
})}
|
||||||
|
>
|
||||||
{fields.map(f => (
|
{fields.map(f => (
|
||||||
<div key={f.type}>
|
<div key={f.type}>
|
||||||
<TextField
|
<TextField
|
||||||
|
|
|
@ -18,6 +18,10 @@
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: $padding-global;
|
gap: $padding-global;
|
||||||
|
|
||||||
|
&_mobile {
|
||||||
|
gap: $padding-small;
|
||||||
|
}
|
||||||
|
|
||||||
div {
|
div {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,9 +7,10 @@ export interface ServerConfiguratorProps {
|
||||||
serverUrl: string
|
serverUrl: string
|
||||||
onChange: (url: string) => void
|
onChange: (url: string) => void
|
||||||
onEnter: () => void
|
onEnter: () => void
|
||||||
|
onBlur: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const ServerConfigurator: FC<ServerConfiguratorProps> = ({ serverUrl, onChange , onEnter }) => {
|
const ServerConfigurator: FC<ServerConfiguratorProps> = ({ serverUrl, onChange , onEnter, onBlur }) => {
|
||||||
|
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
|
|
||||||
|
@ -29,6 +30,8 @@ const ServerConfigurator: FC<ServerConfiguratorProps> = ({ serverUrl, onChange ,
|
||||||
error={error}
|
error={error}
|
||||||
onChange={onChangeServer}
|
onChange={onChangeServer}
|
||||||
onEnter={onEnter}
|
onEnter={onEnter}
|
||||||
|
onBlur={onBlur}
|
||||||
|
inputmode="url"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -41,7 +41,7 @@ const TenantsConfiguration: FC<{accountIds: string[]}> = ({ accountIds }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const showTenantSelector = useMemo(() => {
|
const showTenantSelector = useMemo(() => {
|
||||||
const id = getTenantIdFromUrl(serverUrl);
|
const id = true; //getTenantIdFromUrl(serverUrl);
|
||||||
return accountIds.length > 1 && id;
|
return accountIds.length > 1 && id;
|
||||||
}, [accountIds, serverUrl]);
|
}, [accountIds, serverUrl]);
|
||||||
|
|
||||||
|
@ -81,26 +81,40 @@ const TenantsConfiguration: FC<{accountIds: string[]}> = ({ accountIds }) => {
|
||||||
<div className="vm-tenant-input">
|
<div className="vm-tenant-input">
|
||||||
<Tooltip title="Define Tenant ID if you need request to another storage">
|
<Tooltip title="Define Tenant ID if you need request to another storage">
|
||||||
<div ref={optionsButtonRef}>
|
<div ref={optionsButtonRef}>
|
||||||
<Button
|
{isMobile ? (
|
||||||
className={appModeEnable ? "" : "vm-header-button"}
|
<div
|
||||||
variant="contained"
|
className="vm-mobile-option"
|
||||||
color="primary"
|
onClick={toggleOpenOptions}
|
||||||
fullWidth
|
>
|
||||||
startIcon={<StorageIcon/>}
|
<span className="vm-mobile-option__icon"><StorageIcon/></span>
|
||||||
endIcon={!isMobile ? (
|
<div className="vm-mobile-option-text">
|
||||||
<div
|
<span className="vm-mobile-option-text__label">Tenant ID</span>
|
||||||
className={classNames({
|
<span className="vm-mobile-option-text__value">{tenantIdState}</span>
|
||||||
"vm-execution-controls-buttons__arrow": true,
|
|
||||||
"vm-execution-controls-buttons__arrow_open": openOptions,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<ArrowDownIcon/>
|
|
||||||
</div>
|
</div>
|
||||||
) : undefined}
|
<span className="vm-mobile-option__arrow"><ArrowDownIcon/></span>
|
||||||
onClick={toggleOpenOptions}
|
</div>
|
||||||
>
|
) : (
|
||||||
{!isMobile && tenantIdState}
|
<Button
|
||||||
</Button>
|
className={appModeEnable ? "" : "vm-header-button"}
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
fullWidth
|
||||||
|
startIcon={<StorageIcon/>}
|
||||||
|
endIcon={(
|
||||||
|
<div
|
||||||
|
className={classNames({
|
||||||
|
"vm-execution-controls-buttons__arrow": true,
|
||||||
|
"vm-execution-controls-buttons__arrow_open": openOptions,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<ArrowDownIcon/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
onClick={toggleOpenOptions}
|
||||||
|
>
|
||||||
|
{tenantIdState}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Popper
|
<Popper
|
||||||
|
@ -108,20 +122,28 @@ const TenantsConfiguration: FC<{accountIds: string[]}> = ({ accountIds }) => {
|
||||||
placement="bottom-right"
|
placement="bottom-right"
|
||||||
onClose={handleCloseOptions}
|
onClose={handleCloseOptions}
|
||||||
buttonRef={optionsButtonRef}
|
buttonRef={optionsButtonRef}
|
||||||
|
title={isMobile ? "Define Tenant ID" : undefined}
|
||||||
>
|
>
|
||||||
<div className="vm-list vm-tenant-input-list">
|
<div
|
||||||
|
className={classNames({
|
||||||
|
"vm-list vm-tenant-input-list": true,
|
||||||
|
"vm-list vm-tenant-input-list_mobile": isMobile,
|
||||||
|
})}
|
||||||
|
>
|
||||||
<div className="vm-tenant-input-list__search">
|
<div className="vm-tenant-input-list__search">
|
||||||
<TextField
|
<TextField
|
||||||
autofocus
|
autofocus
|
||||||
label="Search"
|
label="Search"
|
||||||
value={search}
|
value={search}
|
||||||
onChange={setSearch}
|
onChange={setSearch}
|
||||||
|
type="search"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{accountIdsFiltered.map(id => (
|
{accountIdsFiltered.map(id => (
|
||||||
<div
|
<div
|
||||||
className={classNames({
|
className={classNames({
|
||||||
"vm-list-item": true,
|
"vm-list-item": true,
|
||||||
|
"vm-list-item_mobile": isMobile,
|
||||||
"vm-list-item_active": id === tenantIdState
|
"vm-list-item_active": id === tenantIdState
|
||||||
})}
|
})}
|
||||||
key={id}
|
key={id}
|
||||||
|
|
|
@ -9,10 +9,18 @@
|
||||||
overscroll-behavior: none;
|
overscroll-behavior: none;
|
||||||
border-radius: $border-radius-medium;
|
border-radius: $border-radius-medium;
|
||||||
|
|
||||||
|
&_mobile {
|
||||||
|
max-height: calc(($vh * 100) - 70px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&_mobile &__search {
|
||||||
|
padding: 0 $padding-global $padding-small;
|
||||||
|
}
|
||||||
|
|
||||||
&__search {
|
&__search {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
padding: $padding-small;
|
padding: $padding-small $padding-global;
|
||||||
background-color: $color-background-block;
|
background-color: $color-background-block;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import dayjs from "dayjs";
|
||||||
import TextField from "../../../Main/TextField/TextField";
|
import TextField from "../../../Main/TextField/TextField";
|
||||||
import { Timezone } from "../../../../types";
|
import { Timezone } from "../../../../types";
|
||||||
import "./style.scss";
|
import "./style.scss";
|
||||||
|
import useDeviceDetect from "../../../../hooks/useDeviceDetect";
|
||||||
|
|
||||||
interface TimezonesProps {
|
interface TimezonesProps {
|
||||||
timezoneState: string
|
timezoneState: string
|
||||||
|
@ -15,7 +16,7 @@ interface TimezonesProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const Timezones: FC<TimezonesProps> = ({ timezoneState, onChange }) => {
|
const Timezones: FC<TimezonesProps> = ({ timezoneState, onChange }) => {
|
||||||
|
const { isMobile } = useDeviceDetect();
|
||||||
const timezones = getTimezoneList();
|
const timezones = getTimezoneList();
|
||||||
|
|
||||||
const [openList, setOpenList] = useState(false);
|
const [openList, setOpenList] = useState(false);
|
||||||
|
@ -92,8 +93,14 @@ const Timezones: FC<TimezonesProps> = ({ timezoneState, onChange }) => {
|
||||||
placement="bottom-left"
|
placement="bottom-left"
|
||||||
onClose={handleCloseList}
|
onClose={handleCloseList}
|
||||||
fullWidth
|
fullWidth
|
||||||
|
title={isMobile ? "Time zone" : undefined}
|
||||||
>
|
>
|
||||||
<div className="vm-timezones-list">
|
<div
|
||||||
|
className={classNames({
|
||||||
|
"vm-timezones-list": true,
|
||||||
|
"vm-timezones-list_mobile": isMobile,
|
||||||
|
})}
|
||||||
|
>
|
||||||
<div className="vm-timezones-list-header">
|
<div className="vm-timezones-list-header">
|
||||||
<div className="vm-timezones-list-header__search">
|
<div className="vm-timezones-list-header__search">
|
||||||
<TextField
|
<TextField
|
||||||
|
|
|
@ -51,6 +51,14 @@
|
||||||
border-radius: $border-radius-medium;
|
border-radius: $border-radius-medium;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
|
||||||
|
&_mobile {
|
||||||
|
max-height: calc(($vh * 100) - 70px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&_mobile &-header__search {
|
||||||
|
padding: 0 $padding-global 0;
|
||||||
|
}
|
||||||
|
|
||||||
&-header {
|
&-header {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: $padding-medium;
|
gap: $padding-medium;
|
||||||
width: 600px;
|
width: 600px;
|
||||||
|
padding-bottom: $padding-medium;
|
||||||
|
|
||||||
&_mobile {
|
&_mobile {
|
||||||
grid-auto-rows: min-content;
|
grid-auto-rows: min-content;
|
||||||
|
@ -20,12 +21,6 @@
|
||||||
|
|
||||||
&__input {
|
&__input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
&_server {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr auto;
|
|
||||||
gap: 0 $padding-small;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__title {
|
&__title {
|
||||||
|
@ -37,20 +32,4 @@
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin-bottom: $padding-global;
|
margin-bottom: $padding-global;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__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;
|
|
||||||
}
|
|
||||||
|
|
||||||
&_mobile &__footer {
|
|
||||||
align-items: flex-end;
|
|
||||||
flex-grow: 1;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,8 @@ import { AxisRange, YaxisState } from "../../../../state/graph/reducer";
|
||||||
import "./style.scss";
|
import "./style.scss";
|
||||||
import TextField from "../../../Main/TextField/TextField";
|
import TextField from "../../../Main/TextField/TextField";
|
||||||
import Switch from "../../../Main/Switch/Switch";
|
import Switch from "../../../Main/Switch/Switch";
|
||||||
|
import useDeviceDetect from "../../../../hooks/useDeviceDetect";
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
interface AxesLimitsConfiguratorProps {
|
interface AxesLimitsConfiguratorProps {
|
||||||
yaxis: YaxisState,
|
yaxis: YaxisState,
|
||||||
|
@ -12,6 +14,7 @@ interface AxesLimitsConfiguratorProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const AxesLimitsConfigurator: FC<AxesLimitsConfiguratorProps> = ({ yaxis, setYaxisLimits, toggleEnableLimits }) => {
|
const AxesLimitsConfigurator: FC<AxesLimitsConfiguratorProps> = ({ yaxis, setYaxisLimits, toggleEnableLimits }) => {
|
||||||
|
const { isMobile } = useDeviceDetect();
|
||||||
|
|
||||||
const axes = useMemo(() => Object.keys(yaxis.limits.range), [yaxis.limits.range]);
|
const axes = useMemo(() => Object.keys(yaxis.limits.range), [yaxis.limits.range]);
|
||||||
|
|
||||||
|
@ -27,11 +30,17 @@ const AxesLimitsConfigurator: FC<AxesLimitsConfiguratorProps> = ({ yaxis, setYax
|
||||||
debouncedOnChangeLimit(val, axis, index);
|
debouncedOnChangeLimit(val, axis, index);
|
||||||
};
|
};
|
||||||
|
|
||||||
return <div className="vm-axes-limits">
|
return <div
|
||||||
|
className={classNames({
|
||||||
|
"vm-axes-limits": true,
|
||||||
|
"vm-axes-limits_mobile": isMobile
|
||||||
|
})}
|
||||||
|
>
|
||||||
<Switch
|
<Switch
|
||||||
value={yaxis.limits.enable}
|
value={yaxis.limits.enable}
|
||||||
onChange={toggleEnableLimits}
|
onChange={toggleEnableLimits}
|
||||||
label="Fix the limits for y-axis"
|
label="Fix the limits for y-axis"
|
||||||
|
fullWidth={isMobile}
|
||||||
/>
|
/>
|
||||||
<div className="vm-axes-limits-list">
|
<div className="vm-axes-limits-list">
|
||||||
{axes.map(axis => (
|
{axes.map(axis => (
|
||||||
|
|
|
@ -6,6 +6,16 @@
|
||||||
gap: $padding-global;
|
gap: $padding-global;
|
||||||
max-width: 300px;
|
max-width: 300px;
|
||||||
|
|
||||||
|
&_mobile {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
gap: $padding-medium;
|
||||||
|
}
|
||||||
|
|
||||||
|
&_mobile &-list__inputs {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
&-list {
|
&-list {
|
||||||
display: grid;
|
display: grid;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import React, { FC, useRef, useState } from "preact/compat";
|
import React, { FC, useRef, useState } from "preact/compat";
|
||||||
import AxesLimitsConfigurator from "./AxesLimitsConfigurator/AxesLimitsConfigurator";
|
import AxesLimitsConfigurator from "./AxesLimitsConfigurator/AxesLimitsConfigurator";
|
||||||
import { AxisRange, YaxisState } from "../../../state/graph/reducer";
|
import { AxisRange, YaxisState } from "../../../state/graph/reducer";
|
||||||
import { CloseIcon, SettingsIcon } from "../../Main/Icons";
|
import { SettingsIcon } from "../../Main/Icons";
|
||||||
import Button from "../../Main/Button/Button";
|
import Button from "../../Main/Button/Button";
|
||||||
import useClickOutside from "../../../hooks/useClickOutside";
|
|
||||||
import Popper from "../../Main/Popper/Popper";
|
import Popper from "../../Main/Popper/Popper";
|
||||||
import "./style.scss";
|
import "./style.scss";
|
||||||
import Tooltip from "../../Main/Tooltip/Tooltip";
|
import Tooltip from "../../Main/Tooltip/Tooltip";
|
||||||
|
@ -20,7 +19,6 @@ const GraphSettings: FC<GraphSettingsProps> = ({ yaxis, setYaxisLimits, toggleEn
|
||||||
const popperRef = useRef<HTMLDivElement>(null);
|
const popperRef = useRef<HTMLDivElement>(null);
|
||||||
const [openPopper, setOpenPopper] = useState(false);
|
const [openPopper, setOpenPopper] = useState(false);
|
||||||
const buttonRef = useRef<HTMLDivElement>(null);
|
const buttonRef = useRef<HTMLDivElement>(null);
|
||||||
useClickOutside(popperRef, () => setOpenPopper(false), buttonRef);
|
|
||||||
|
|
||||||
const toggleOpen = () => {
|
const toggleOpen = () => {
|
||||||
setOpenPopper(prev => !prev);
|
setOpenPopper(prev => !prev);
|
||||||
|
@ -46,22 +44,12 @@ const GraphSettings: FC<GraphSettingsProps> = ({ yaxis, setYaxisLimits, toggleEn
|
||||||
buttonRef={buttonRef}
|
buttonRef={buttonRef}
|
||||||
placement="bottom-right"
|
placement="bottom-right"
|
||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
|
title={title}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="vm-graph-settings-popper"
|
className="vm-graph-settings-popper"
|
||||||
ref={popperRef}
|
ref={popperRef}
|
||||||
>
|
>
|
||||||
<div className="vm-popper-header">
|
|
||||||
<h3 className="vm-popper-header__title">
|
|
||||||
{title}
|
|
||||||
</h3>
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
variant="text"
|
|
||||||
startIcon={<CloseIcon/>}
|
|
||||||
onClick={handleClose}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="vm-graph-settings-popper__body">
|
<div className="vm-graph-settings-popper__body">
|
||||||
<AxesLimitsConfigurator
|
<AxesLimitsConfigurator
|
||||||
yaxis={yaxis}
|
yaxis={yaxis}
|
||||||
|
|
|
@ -78,9 +78,11 @@ const QueryEditor: FC<QueryEditorProps> = ({
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
inputmode={"search"}
|
||||||
/>
|
/>
|
||||||
{autocomplete && (
|
{autocomplete && (
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
|
disabledFullScreen
|
||||||
value={value}
|
value={value}
|
||||||
options={options}
|
options={options}
|
||||||
anchor={autocompleteAnchorEl}
|
anchor={autocompleteAnchorEl}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { FC, useEffect, useRef, useState } from "preact/compat";
|
import React, { FC, useEffect, useRef, useState } from "preact/compat";
|
||||||
import { RestartIcon, TimelineIcon } from "../../Main/Icons";
|
import { ArrowDownIcon, RestartIcon, TimelineIcon } from "../../Main/Icons";
|
||||||
import TextField from "../../Main/TextField/TextField";
|
import TextField from "../../Main/TextField/TextField";
|
||||||
import Button from "../../Main/Button/Button";
|
import Button from "../../Main/Button/Button";
|
||||||
import Tooltip from "../../Main/Tooltip/Tooltip";
|
import Tooltip from "../../Main/Tooltip/Tooltip";
|
||||||
|
@ -11,9 +11,12 @@ import usePrevious from "../../../hooks/usePrevious";
|
||||||
import "./style.scss";
|
import "./style.scss";
|
||||||
import { getAppModeEnable } from "../../../utils/app-mode";
|
import { getAppModeEnable } from "../../../utils/app-mode";
|
||||||
import Popper from "../../Main/Popper/Popper";
|
import Popper from "../../Main/Popper/Popper";
|
||||||
|
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
const StepConfigurator: FC = () => {
|
const StepConfigurator: FC = () => {
|
||||||
const appModeEnable = getAppModeEnable();
|
const appModeEnable = getAppModeEnable();
|
||||||
|
const { isMobile } = useDeviceDetect();
|
||||||
|
|
||||||
const { customStep: value } = useGraphState();
|
const { customStep: value } = useGraphState();
|
||||||
const { period: { step: defaultStep } } = useTimeState();
|
const { period: { step: defaultStep } } = useTimeState();
|
||||||
|
@ -103,29 +106,49 @@ const StepConfigurator: FC = () => {
|
||||||
className="vm-step-control"
|
className="vm-step-control"
|
||||||
ref={buttonRef}
|
ref={buttonRef}
|
||||||
>
|
>
|
||||||
<Tooltip title="Query resolution step width">
|
{isMobile ? (
|
||||||
<Button
|
<div
|
||||||
className={appModeEnable ? "" : "vm-header-button"}
|
className="vm-mobile-option"
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
startIcon={<TimelineIcon/>}
|
|
||||||
onClick={toggleOpenOptions}
|
onClick={toggleOpenOptions}
|
||||||
>
|
>
|
||||||
<p>
|
<span className="vm-mobile-option__icon"><TimelineIcon/></span>
|
||||||
STEP
|
<div className="vm-mobile-option-text">
|
||||||
<p className="vm-step-control__value">
|
<span className="vm-mobile-option-text__label">Step</span>
|
||||||
{customStep}
|
<span className="vm-mobile-option-text__value">{customStep}</span>
|
||||||
|
</div>
|
||||||
|
<span className="vm-mobile-option__arrow"><ArrowDownIcon/></span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Tooltip title="Query resolution step width">
|
||||||
|
<Button
|
||||||
|
className={appModeEnable ? "" : "vm-header-button"}
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
startIcon={<TimelineIcon/>}
|
||||||
|
onClick={toggleOpenOptions}
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
STEP
|
||||||
|
<p className="vm-step-control__value">
|
||||||
|
{customStep}
|
||||||
|
</p>
|
||||||
</p>
|
</p>
|
||||||
</p>
|
</Button>
|
||||||
</Button>
|
</Tooltip>
|
||||||
</Tooltip>
|
)}
|
||||||
<Popper
|
<Popper
|
||||||
open={openOptions}
|
open={openOptions}
|
||||||
placement="bottom-right"
|
placement="bottom-right"
|
||||||
onClose={handleCloseOptions}
|
onClose={handleCloseOptions}
|
||||||
buttonRef={buttonRef}
|
buttonRef={buttonRef}
|
||||||
|
title={isMobile ? "Query resolution step width" : undefined}
|
||||||
>
|
>
|
||||||
<div className="vm-step-control-popper">
|
<div
|
||||||
|
className={classNames({
|
||||||
|
"vm-step-control-popper": true,
|
||||||
|
"vm-step-control-popper_mobile": isMobile,
|
||||||
|
})}
|
||||||
|
>
|
||||||
<TextField
|
<TextField
|
||||||
autofocus
|
autofocus
|
||||||
label="Step value"
|
label="Step value"
|
||||||
|
|
|
@ -11,10 +11,6 @@
|
||||||
&__value {
|
&__value {
|
||||||
display: inline;
|
display: inline;
|
||||||
margin-left: 3px;
|
margin-left: 3px;
|
||||||
|
|
||||||
@media (max-width: 500px) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&-popper {
|
&-popper {
|
||||||
|
@ -26,6 +22,16 @@
|
||||||
padding: $padding-global;
|
padding: $padding-global;
|
||||||
font-size: $font-size;
|
font-size: $font-size;
|
||||||
|
|
||||||
|
&_mobile {
|
||||||
|
padding: 0 $padding-global $padding-small;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: calc(($vh * 100) - 70px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&_mobile &-info {
|
||||||
|
font-size: $font-size;
|
||||||
|
}
|
||||||
|
|
||||||
&-info {
|
&-info {
|
||||||
font-size: $font-size-small;
|
font-size: $font-size-small;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
|
|
|
@ -10,5 +10,6 @@
|
||||||
|
|
||||||
&_mobile &__toggle {
|
&_mobile &__toggle {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
min-width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,12 +2,12 @@ import React, { FC, useEffect, useRef, useState } from "preact/compat";
|
||||||
import { useTimeDispatch } from "../../../../state/time/TimeStateContext";
|
import { useTimeDispatch } from "../../../../state/time/TimeStateContext";
|
||||||
import { getAppModeEnable } from "../../../../utils/app-mode";
|
import { getAppModeEnable } from "../../../../utils/app-mode";
|
||||||
import Button from "../../../Main/Button/Button";
|
import Button from "../../../Main/Button/Button";
|
||||||
import { ArrowDownIcon, RefreshIcon } from "../../../Main/Icons";
|
import { ArrowDownIcon, RefreshIcon, RestartIcon } from "../../../Main/Icons";
|
||||||
import Popper from "../../../Main/Popper/Popper";
|
import Popper from "../../../Main/Popper/Popper";
|
||||||
import "./style.scss";
|
import "./style.scss";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import Tooltip from "../../../Main/Tooltip/Tooltip";
|
import Tooltip from "../../../Main/Tooltip/Tooltip";
|
||||||
import useResize from "../../../../hooks/useResize";
|
import useDeviceDetect from "../../../../hooks/useDeviceDetect";
|
||||||
|
|
||||||
interface AutoRefreshOption {
|
interface AutoRefreshOption {
|
||||||
seconds: number
|
seconds: number
|
||||||
|
@ -30,7 +30,7 @@ const delayOptions: AutoRefreshOption[] = [
|
||||||
];
|
];
|
||||||
|
|
||||||
export const ExecutionControls: FC = () => {
|
export const ExecutionControls: FC = () => {
|
||||||
const windowSize = useResize(document.body);
|
const { isMobile } = useDeviceDetect();
|
||||||
|
|
||||||
const dispatch = useTimeDispatch();
|
const dispatch = useTimeDispatch();
|
||||||
const appModeEnable = getAppModeEnable();
|
const appModeEnable = getAppModeEnable();
|
||||||
|
@ -85,11 +85,11 @@ export const ExecutionControls: FC = () => {
|
||||||
<div
|
<div
|
||||||
className={classNames({
|
className={classNames({
|
||||||
"vm-execution-controls-buttons": true,
|
"vm-execution-controls-buttons": true,
|
||||||
|
"vm-execution-controls-buttons_mobile": isMobile,
|
||||||
"vm-header-button": !appModeEnable,
|
"vm-header-button": !appModeEnable,
|
||||||
"vm-execution-controls-buttons_short": windowSize.width <= 360
|
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{windowSize.width > 360 && (
|
{!isMobile && (
|
||||||
<Tooltip title="Refresh dashboard">
|
<Tooltip title="Refresh dashboard">
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
|
@ -99,28 +99,42 @@ export const ExecutionControls: FC = () => {
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
<Tooltip title="Auto-refresh control">
|
{isMobile ? (
|
||||||
<div ref={optionsButtonRef}>
|
<div
|
||||||
<Button
|
className="vm-mobile-option"
|
||||||
variant="contained"
|
onClick={toggleOpenOptions}
|
||||||
color="primary"
|
>
|
||||||
fullWidth
|
<span className="vm-mobile-option__icon"><RestartIcon/></span>
|
||||||
endIcon={(
|
<div className="vm-mobile-option-text">
|
||||||
<div
|
<span className="vm-mobile-option-text__label">Auto-refresh</span>
|
||||||
className={classNames({
|
<span className="vm-mobile-option-text__value">{selectedDelay.title}</span>
|
||||||
"vm-execution-controls-buttons__arrow": true,
|
</div>
|
||||||
"vm-execution-controls-buttons__arrow_open": openOptions,
|
<span className="vm-mobile-option__arrow"><ArrowDownIcon/></span>
|
||||||
})}
|
|
||||||
>
|
|
||||||
<ArrowDownIcon/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
onClick={toggleOpenOptions}
|
|
||||||
>
|
|
||||||
{selectedDelay.title}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
<Popper
|
<Popper
|
||||||
|
@ -128,12 +142,19 @@ export const ExecutionControls: FC = () => {
|
||||||
placement="bottom-right"
|
placement="bottom-right"
|
||||||
onClose={handleCloseOptions}
|
onClose={handleCloseOptions}
|
||||||
buttonRef={optionsButtonRef}
|
buttonRef={optionsButtonRef}
|
||||||
|
title={isMobile ? "Auto-refresh duration" : undefined}
|
||||||
>
|
>
|
||||||
<div className="vm-execution-controls-list">
|
<div
|
||||||
|
className={classNames({
|
||||||
|
"vm-execution-controls-list": true,
|
||||||
|
"vm-execution-controls-list_mobile": isMobile,
|
||||||
|
})}
|
||||||
|
>
|
||||||
{delayOptions.map(d => (
|
{delayOptions.map(d => (
|
||||||
<div
|
<div
|
||||||
className={classNames({
|
className={classNames({
|
||||||
"vm-list-item": true,
|
"vm-list-item": true,
|
||||||
|
"vm-list-item_mobile": isMobile,
|
||||||
"vm-list-item_active": d.seconds === selectedDelay.seconds
|
"vm-list-item_active": d.seconds === selectedDelay.seconds
|
||||||
})}
|
})}
|
||||||
key={d.seconds}
|
key={d.seconds}
|
||||||
|
|
|
@ -9,8 +9,9 @@
|
||||||
border-radius: calc($button-radius + 1px);
|
border-radius: calc($button-radius + 1px);
|
||||||
min-width: 107px;
|
min-width: 107px;
|
||||||
|
|
||||||
&_short {
|
&_mobile {
|
||||||
min-width: auto;
|
flex-direction: column;
|
||||||
|
gap: $padding-medium;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__arrow {
|
&__arrow {
|
||||||
|
@ -32,5 +33,11 @@
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
padding: $padding-small 0;
|
padding: $padding-small 0;
|
||||||
font-size: $font-size;
|
font-size: $font-size;
|
||||||
|
|
||||||
|
&_mobile {
|
||||||
|
width: 100%;
|
||||||
|
max-height: calc(($vh * 100) - 70px);
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import React, { FC } from "preact/compat";
|
||||||
import { relativeTimeOptions } from "../../../../utils/time";
|
import { relativeTimeOptions } from "../../../../utils/time";
|
||||||
import "./style.scss";
|
import "./style.scss";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
import useDeviceDetect from "../../../../hooks/useDeviceDetect";
|
||||||
|
|
||||||
interface TimeDurationSelector {
|
interface TimeDurationSelector {
|
||||||
setDuration: ({ duration, until, id }: {duration: string, until: Date, id: string}) => void;
|
setDuration: ({ duration, until, id }: {duration: string, until: Date, id: string}) => void;
|
||||||
|
@ -9,17 +10,24 @@ interface TimeDurationSelector {
|
||||||
}
|
}
|
||||||
|
|
||||||
const TimeDurationSelector: FC<TimeDurationSelector> = ({ relativeTime, setDuration }) => {
|
const TimeDurationSelector: FC<TimeDurationSelector> = ({ relativeTime, setDuration }) => {
|
||||||
|
const { isMobile } = useDeviceDetect();
|
||||||
|
|
||||||
const createHandlerClick = (value: { duration: string, until: Date, id: string }) => () => {
|
const createHandlerClick = (value: { duration: string, until: Date, id: string }) => () => {
|
||||||
setDuration(value);
|
setDuration(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="vm-time-duration">
|
<div
|
||||||
|
className={classNames({
|
||||||
|
"vm-time-duration": true,
|
||||||
|
"vm-time-duration_mobile": isMobile,
|
||||||
|
})}
|
||||||
|
>
|
||||||
{relativeTimeOptions.map(({ id, duration, until, title }) => (
|
{relativeTimeOptions.map(({ id, duration, until, title }) => (
|
||||||
<div
|
<div
|
||||||
className={classNames({
|
className={classNames({
|
||||||
"vm-list-item": true,
|
"vm-list-item": true,
|
||||||
|
"vm-list-item_mobile": isMobile,
|
||||||
"vm-list-item_active": id === relativeTime
|
"vm-list-item_active": id === relativeTime
|
||||||
})}
|
})}
|
||||||
key={id}
|
key={id}
|
||||||
|
|
|
@ -4,4 +4,8 @@
|
||||||
max-height: 200px;
|
max-height: 200px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
font-size: $font-size;
|
font-size: $font-size;
|
||||||
|
|
||||||
|
&_mobile {
|
||||||
|
max-height: 100%
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import TimeDurationSelector from "../TimeDurationSelector/TimeDurationSelector";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { getAppModeEnable } from "../../../../utils/app-mode";
|
import { getAppModeEnable } from "../../../../utils/app-mode";
|
||||||
import { useTimeDispatch, useTimeState } from "../../../../state/time/TimeStateContext";
|
import { useTimeDispatch, useTimeState } from "../../../../state/time/TimeStateContext";
|
||||||
import { AlarmIcon, CalendarIcon, ClockIcon } from "../../../Main/Icons";
|
import { AlarmIcon, ArrowDownIcon, CalendarIcon, ClockIcon } from "../../../Main/Icons";
|
||||||
import Button from "../../../Main/Button/Button";
|
import Button from "../../../Main/Button/Button";
|
||||||
import Popper from "../../../Main/Popper/Popper";
|
import Popper from "../../../Main/Popper/Popper";
|
||||||
import Tooltip from "../../../Main/Tooltip/Tooltip";
|
import Tooltip from "../../../Main/Tooltip/Tooltip";
|
||||||
|
@ -15,8 +15,10 @@ import "./style.scss";
|
||||||
import useClickOutside from "../../../../hooks/useClickOutside";
|
import useClickOutside from "../../../../hooks/useClickOutside";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { useAppState } from "../../../../state/common/StateContext";
|
import { useAppState } from "../../../../state/common/StateContext";
|
||||||
|
import useDeviceDetect from "../../../../hooks/useDeviceDetect";
|
||||||
|
|
||||||
export const TimeSelector: FC = () => {
|
export const TimeSelector: FC = () => {
|
||||||
|
const { isMobile } = useDeviceDetect();
|
||||||
const { isDarkTheme } = useAppState();
|
const { isDarkTheme } = useAppState();
|
||||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||||
const documentSize = useResize(document.body);
|
const documentSize = useResize(document.body);
|
||||||
|
@ -112,6 +114,7 @@ export const TimeSelector: FC = () => {
|
||||||
}, [timezone]);
|
}, [timezone]);
|
||||||
|
|
||||||
useClickOutside(wrapperRef, (e) => {
|
useClickOutside(wrapperRef, (e) => {
|
||||||
|
if (isMobile) return;
|
||||||
const target = e.target as HTMLElement;
|
const target = e.target as HTMLElement;
|
||||||
const isFromButton = fromRef?.current && fromRef.current.contains(target);
|
const isFromButton = fromRef?.current && fromRef.current.contains(target);
|
||||||
const isUntilButton = untilRef?.current && untilRef.current.contains(target);
|
const isUntilButton = untilRef?.current && untilRef.current.contains(target);
|
||||||
|
@ -123,17 +126,31 @@ export const TimeSelector: FC = () => {
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<div ref={buttonRef}>
|
<div ref={buttonRef}>
|
||||||
<Tooltip title={displayFullDate ? "Time range controls" : dateTitle}>
|
{isMobile ? (
|
||||||
<Button
|
<div
|
||||||
className={appModeEnable ? "" : "vm-header-button"}
|
className="vm-mobile-option"
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
startIcon={<ClockIcon/>}
|
|
||||||
onClick={toggleOpenOptions}
|
onClick={toggleOpenOptions}
|
||||||
>
|
>
|
||||||
{displayFullDate && <span>{dateTitle}</span>}
|
<span className="vm-mobile-option__icon"><ClockIcon/></span>
|
||||||
</Button>
|
<div className="vm-mobile-option-text">
|
||||||
</Tooltip>
|
<span className="vm-mobile-option-text__label">Time range</span>
|
||||||
|
<span className="vm-mobile-option-text__value">{dateTitle}</span>
|
||||||
|
</div>
|
||||||
|
<span className="vm-mobile-option__arrow"><ArrowDownIcon/></span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Tooltip title={displayFullDate ? "Time range controls" : dateTitle}>
|
||||||
|
<Button
|
||||||
|
className={appModeEnable ? "" : "vm-header-button"}
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
startIcon={<ClockIcon/>}
|
||||||
|
onClick={toggleOpenOptions}
|
||||||
|
>
|
||||||
|
{displayFullDate && <span>{dateTitle}</span>}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Popper
|
<Popper
|
||||||
open={openOptions}
|
open={openOptions}
|
||||||
|
@ -141,9 +158,13 @@ export const TimeSelector: FC = () => {
|
||||||
placement="bottom-right"
|
placement="bottom-right"
|
||||||
onClose={handleCloseOptions}
|
onClose={handleCloseOptions}
|
||||||
clickOutside={false}
|
clickOutside={false}
|
||||||
|
title={isMobile ? "Time range controls" : ""}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="vm-time-selector"
|
className={classNames({
|
||||||
|
"vm-time-selector": true,
|
||||||
|
"vm-time-selector_mobile": isMobile
|
||||||
|
})}
|
||||||
ref={wrapperRef}
|
ref={wrapperRef}
|
||||||
>
|
>
|
||||||
<div className="vm-time-selector-left">
|
<div className="vm-time-selector-left">
|
||||||
|
@ -161,6 +182,7 @@ export const TimeSelector: FC = () => {
|
||||||
<span>{formFormat}</span>
|
<span>{formFormat}</span>
|
||||||
<CalendarIcon/>
|
<CalendarIcon/>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
|
label={"Date From"}
|
||||||
ref={fromPickerRef}
|
ref={fromPickerRef}
|
||||||
date={from || ""}
|
date={from || ""}
|
||||||
onChange={handleFromChange}
|
onChange={handleFromChange}
|
||||||
|
@ -176,6 +198,7 @@ export const TimeSelector: FC = () => {
|
||||||
<span>{untilFormat}</span>
|
<span>{untilFormat}</span>
|
||||||
<CalendarIcon/>
|
<CalendarIcon/>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
|
label={"Date To"}
|
||||||
ref={untilPickerRef}
|
ref={untilPickerRef}
|
||||||
date={until || ""}
|
date={until || ""}
|
||||||
onChange={handleUntilChange}
|
onChange={handleUntilChange}
|
||||||
|
|
|
@ -5,9 +5,18 @@
|
||||||
grid-template-columns: repeat(2, 230px);
|
grid-template-columns: repeat(2, 230px);
|
||||||
padding: $padding-global 0;
|
padding: $padding-global 0;
|
||||||
|
|
||||||
@media (max-width: 500px) {
|
&_mobile {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
min-width: 250px;
|
min-width: 250px;
|
||||||
|
width: 100%;
|
||||||
|
max-height: calc(($vh * 100) - 70px);
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&_mobile &-left {
|
||||||
|
border-right: none;
|
||||||
|
border-bottom: $border-divider;
|
||||||
|
padding-bottom: $padding-global;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-left {
|
&-left {
|
||||||
|
@ -17,12 +26,6 @@
|
||||||
border-right: $border-divider;
|
border-right: $border-divider;
|
||||||
padding: 0 $padding-global;
|
padding: 0 $padding-global;
|
||||||
|
|
||||||
@media (max-width: 500px) {
|
|
||||||
border-right: none;
|
|
||||||
border-bottom: $border-divider;
|
|
||||||
padding-bottom: $padding-global;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-inputs {
|
&-inputs {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
display: grid;
|
display: grid;
|
||||||
|
@ -62,6 +65,7 @@
|
||||||
grid-column: 1/3;
|
grid-column: 1/3;
|
||||||
font-size: $font-size-small;
|
font-size: $font-size-small;
|
||||||
color: $color-text-secondary;
|
color: $color-text-secondary;
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
|
|
|
@ -8,6 +8,8 @@ import Spinner from "../../Main/Spinner/Spinner";
|
||||||
import Alert from "../../Main/Alert/Alert";
|
import Alert from "../../Main/Alert/Alert";
|
||||||
import Button from "../../Main/Button/Button";
|
import Button from "../../Main/Button/Button";
|
||||||
import "./style.scss";
|
import "./style.scss";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||||
|
|
||||||
interface ExploreMetricItemGraphProps {
|
interface ExploreMetricItemGraphProps {
|
||||||
name: string,
|
name: string,
|
||||||
|
@ -26,6 +28,7 @@ const ExploreMetricItem: FC<ExploreMetricItemGraphProps> = ({
|
||||||
isBucket,
|
isBucket,
|
||||||
height
|
height
|
||||||
}) => {
|
}) => {
|
||||||
|
const { isMobile } = useDeviceDetect();
|
||||||
const { customStep, yaxis } = useGraphState();
|
const { customStep, yaxis } = useGraphState();
|
||||||
const { period } = useTimeState();
|
const { period } = useTimeState();
|
||||||
|
|
||||||
|
@ -92,7 +95,12 @@ with (q = ${queryBase}) (
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="vm-explore-metrics-graph">
|
<div
|
||||||
|
className={classNames({
|
||||||
|
"vm-explore-metrics-graph": true,
|
||||||
|
"vm-explore-metrics-graph_mobile": isMobile
|
||||||
|
})}
|
||||||
|
>
|
||||||
{isLoading && <Spinner />}
|
{isLoading && <Spinner />}
|
||||||
{error && <Alert variant="error">{error}</Alert>}
|
{error && <Alert variant="error">{error}</Alert>}
|
||||||
{warning && <Alert variant="warning">
|
{warning && <Alert variant="warning">
|
||||||
|
|
|
@ -3,6 +3,10 @@
|
||||||
.vm-explore-metrics-graph {
|
.vm-explore-metrics-graph {
|
||||||
padding: 0 $padding-global $padding-global;
|
padding: 0 $padding-global $padding-global;
|
||||||
|
|
||||||
|
&_mobile {
|
||||||
|
padding: 0 $padding-global $padding-global;
|
||||||
|
}
|
||||||
|
|
||||||
&__warning {
|
&__warning {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr auto;
|
grid-template-columns: 1fr auto;
|
||||||
|
|
|
@ -10,6 +10,7 @@ interface ExploreMetricItemProps {
|
||||||
job: string
|
job: string
|
||||||
instance: string
|
instance: string
|
||||||
index: number
|
index: number
|
||||||
|
length: number
|
||||||
size: GraphSize
|
size: GraphSize
|
||||||
onRemoveItem: (name: string) => void
|
onRemoveItem: (name: string) => void
|
||||||
onChangeOrder: (name: string, oldIndex: number, newIndex: number) => void
|
onChangeOrder: (name: string, oldIndex: number, newIndex: number) => void
|
||||||
|
@ -20,6 +21,7 @@ const ExploreMetricItem: FC<ExploreMetricItemProps> = ({
|
||||||
job,
|
job,
|
||||||
instance,
|
instance,
|
||||||
index,
|
index,
|
||||||
|
length,
|
||||||
size,
|
size,
|
||||||
onRemoveItem,
|
onRemoveItem,
|
||||||
onChangeOrder,
|
onChangeOrder,
|
||||||
|
@ -42,6 +44,7 @@ const ExploreMetricItem: FC<ExploreMetricItemProps> = ({
|
||||||
<ExploreMetricItemHeader
|
<ExploreMetricItemHeader
|
||||||
name={name}
|
name={name}
|
||||||
index={index}
|
index={index}
|
||||||
|
length={length}
|
||||||
isBucket={isBucket}
|
isBucket={isBucket}
|
||||||
rateEnabled={rateEnabled}
|
rateEnabled={rateEnabled}
|
||||||
size={size.id}
|
size={size.id}
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
import React, { FC } from "preact/compat";
|
import React, { FC, useState } from "preact/compat";
|
||||||
import "./style.scss";
|
import "./style.scss";
|
||||||
import Switch from "../../Main/Switch/Switch";
|
import Switch from "../../Main/Switch/Switch";
|
||||||
import Tooltip from "../../Main/Tooltip/Tooltip";
|
import Tooltip from "../../Main/Tooltip/Tooltip";
|
||||||
import Button from "../../Main/Button/Button";
|
import Button from "../../Main/Button/Button";
|
||||||
import { ArrowDownIcon, CloseIcon } from "../../Main/Icons";
|
import { ArrowDownIcon, CloseIcon, MinusIcon, MoreIcon, PlusIcon } from "../../Main/Icons";
|
||||||
|
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||||
|
import Modal from "../../Main/Modal/Modal";
|
||||||
|
|
||||||
interface ExploreMetricItemControlsProps {
|
interface ExploreMetricItemControlsProps {
|
||||||
name: string
|
name: string
|
||||||
index: number
|
index: number
|
||||||
|
length: number
|
||||||
isBucket: boolean
|
isBucket: boolean
|
||||||
rateEnabled: boolean
|
rateEnabled: boolean
|
||||||
size: string
|
size: string
|
||||||
|
@ -19,12 +22,15 @@ interface ExploreMetricItemControlsProps {
|
||||||
const ExploreMetricItemHeader: FC<ExploreMetricItemControlsProps> = ({
|
const ExploreMetricItemHeader: FC<ExploreMetricItemControlsProps> = ({
|
||||||
name,
|
name,
|
||||||
index,
|
index,
|
||||||
|
length,
|
||||||
isBucket,
|
isBucket,
|
||||||
rateEnabled,
|
rateEnabled,
|
||||||
onChangeRate,
|
onChangeRate,
|
||||||
onRemoveItem,
|
onRemoveItem,
|
||||||
onChangeOrder,
|
onChangeOrder,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { isMobile } = useDeviceDetect();
|
||||||
|
const [openOptions, setOpenOptions] = useState(false);
|
||||||
|
|
||||||
const handleClickRemove = () => {
|
const handleClickRemove = () => {
|
||||||
onRemoveItem(name);
|
onRemoveItem(name);
|
||||||
|
@ -38,6 +44,76 @@ const ExploreMetricItemHeader: FC<ExploreMetricItemControlsProps> = ({
|
||||||
onChangeOrder(name, index, index - 1);
|
onChangeOrder(name, index, index - 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleOpenOptions = () => {
|
||||||
|
setOpenOptions(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseOptions = () => {
|
||||||
|
setOpenOptions(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isMobile) {
|
||||||
|
return (
|
||||||
|
<div className="vm-explore-metrics-item-header vm-explore-metrics-item-header_mobile">
|
||||||
|
<div className="vm-explore-metrics-item-header__name">{name}</div>
|
||||||
|
<Button
|
||||||
|
variant="text"
|
||||||
|
size="small"
|
||||||
|
startIcon={<MoreIcon/>}
|
||||||
|
onClick={handleOpenOptions}
|
||||||
|
/>
|
||||||
|
{openOptions && (
|
||||||
|
<Modal
|
||||||
|
title={name}
|
||||||
|
onClose={handleCloseOptions}
|
||||||
|
>
|
||||||
|
<div className="vm-explore-metrics-item-header-modal">
|
||||||
|
<div className="vm-explore-metrics-item-header-modal-order">
|
||||||
|
<Button
|
||||||
|
startIcon={<MinusIcon/>}
|
||||||
|
variant="outlined"
|
||||||
|
onClick={handleOrderUp}
|
||||||
|
disabled={index === 0}
|
||||||
|
/>
|
||||||
|
<p>position:
|
||||||
|
<span className="vm-explore-metrics-item-header-modal-order__index">#{index + 1}</span>
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
endIcon={<PlusIcon/>}
|
||||||
|
variant="outlined"
|
||||||
|
onClick={handleOrderDown}
|
||||||
|
disabled={index === length - 1}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{!isBucket && (
|
||||||
|
<div className="vm-explore-metrics-item-header-modal__rate">
|
||||||
|
<Switch
|
||||||
|
label={<span>enable <code>rate()</code></span>}
|
||||||
|
value={rateEnabled}
|
||||||
|
onChange={onChangeRate}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
<p>
|
||||||
|
calculates the average per-second speed of metrics change
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
startIcon={<CloseIcon/>}
|
||||||
|
color="error"
|
||||||
|
variant="outlined"
|
||||||
|
onClick={handleClickRemove}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
Remove graph
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="vm-explore-metrics-item-header">
|
<div className="vm-explore-metrics-item-header">
|
||||||
<div className="vm-explore-metrics-item-header-order">
|
<div className="vm-explore-metrics-item-header-order">
|
||||||
|
@ -65,15 +141,17 @@ const ExploreMetricItemHeader: FC<ExploreMetricItemControlsProps> = ({
|
||||||
</div>
|
</div>
|
||||||
<div className="vm-explore-metrics-item-header__name">{name}</div>
|
<div className="vm-explore-metrics-item-header__name">{name}</div>
|
||||||
{!isBucket && (
|
{!isBucket && (
|
||||||
<Tooltip title="calculates the average per-second speed of metric's change">
|
<div className="vm-explore-metrics-item-header__rate">
|
||||||
<Switch
|
<Tooltip title="calculates the average per-second speed of metric's change">
|
||||||
label={<span>enable <code>rate()</code></span>}
|
<Switch
|
||||||
value={rateEnabled}
|
label={<span>enable <code>rate()</code></span>}
|
||||||
onChange={onChangeRate}
|
value={rateEnabled}
|
||||||
/>
|
onChange={onChangeRate}
|
||||||
</Tooltip>
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="vm-explore-metrics-item-header__layout">
|
<div className="vm-explore-metrics-item-header__close">
|
||||||
<Tooltip title="close graph">
|
<Tooltip title="close graph">
|
||||||
<Button
|
<Button
|
||||||
startIcon={<CloseIcon/>}
|
startIcon={<CloseIcon/>}
|
||||||
|
|
|
@ -1,14 +1,19 @@
|
||||||
@use "src/styles/variables" as *;
|
@use "src/styles/variables" as *;
|
||||||
|
|
||||||
.vm-explore-metrics-item-header {
|
.vm-explore-metrics-item-header {
|
||||||
display: flex;
|
display: grid;
|
||||||
flex-wrap: wrap;
|
grid-template-columns: auto 1fr auto auto;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
padding: $padding-global;
|
padding: $padding-global;
|
||||||
border-bottom: $border-divider;
|
border-bottom: $border-divider;
|
||||||
gap: $padding-global;
|
gap: $padding-global;
|
||||||
|
|
||||||
|
&_mobile {
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
padding: $padding-small $padding-global;
|
||||||
|
}
|
||||||
|
|
||||||
&__index {
|
&__index {
|
||||||
color: $color-text-secondary;
|
color: $color-text-secondary;
|
||||||
font-size: $font-size-small;
|
font-size: $font-size-small;
|
||||||
|
@ -17,9 +22,14 @@
|
||||||
&__name {
|
&__name {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
max-width: 100%;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
line-height: 130%;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-order {
|
&-order {
|
||||||
|
grid-column: 1;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: auto 20px auto;
|
grid-template-columns: auto 20px auto;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -31,7 +41,13 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__layout {
|
&__rate {
|
||||||
|
grid-column: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__close {
|
||||||
|
grid-row: 1;
|
||||||
|
grid-column: 4;
|
||||||
display: grid;
|
display: grid;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
@ -42,4 +58,35 @@
|
||||||
background-color: $color-hover-black;
|
background-color: $color-hover-black;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&-modal {
|
||||||
|
display: grid;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: $padding-medium;
|
||||||
|
|
||||||
|
&-order {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: $padding-medium;
|
||||||
|
|
||||||
|
p {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__index {
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__rate {
|
||||||
|
display: grid;
|
||||||
|
gap: $padding-small;
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: $color-text-secondary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,8 @@ import React, { FC, useMemo } from "preact/compat";
|
||||||
import Select from "../../Main/Select/Select";
|
import Select from "../../Main/Select/Select";
|
||||||
import "./style.scss";
|
import "./style.scss";
|
||||||
import { GRAPH_SIZES } from "../../../constants/graph";
|
import { GRAPH_SIZES } from "../../../constants/graph";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||||
|
|
||||||
interface ExploreMetricsHeaderProps {
|
interface ExploreMetricsHeaderProps {
|
||||||
jobs: string[]
|
jobs: string[]
|
||||||
|
@ -34,9 +36,17 @@ const ExploreMetricsHeader: FC<ExploreMetricsHeaderProps> = ({
|
||||||
}) => {
|
}) => {
|
||||||
const noInstanceText = useMemo(() => job ? "" : "No instances. Please select job", [job]);
|
const noInstanceText = useMemo(() => job ? "" : "No instances. Please select job", [job]);
|
||||||
const noMetricsText = useMemo(() => job ? "" : "No metric names. Please select job", [job]);
|
const noMetricsText = useMemo(() => job ? "" : "No metric names. Please select job", [job]);
|
||||||
|
const { isMobile } = useDeviceDetect();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="vm-explore-metrics-header vm-block">
|
<div
|
||||||
|
className={classNames({
|
||||||
|
"vm-explore-metrics-header": true,
|
||||||
|
"vm-explore-metrics-header_mobile": isMobile,
|
||||||
|
"vm-block": true,
|
||||||
|
"vm-block_mobile": isMobile,
|
||||||
|
})}
|
||||||
|
>
|
||||||
<div className="vm-explore-metrics-header__job">
|
<div className="vm-explore-metrics-header__job">
|
||||||
<Select
|
<Select
|
||||||
value={job}
|
value={job}
|
||||||
|
@ -45,6 +55,7 @@ const ExploreMetricsHeader: FC<ExploreMetricsHeaderProps> = ({
|
||||||
placeholder="Please select job"
|
placeholder="Please select job"
|
||||||
onChange={onChangeJob}
|
onChange={onChangeJob}
|
||||||
autofocus={!job}
|
autofocus={!job}
|
||||||
|
searchable
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="vm-explore-metrics-header__instance">
|
<div className="vm-explore-metrics-header__instance">
|
||||||
|
@ -56,6 +67,7 @@ const ExploreMetricsHeader: FC<ExploreMetricsHeaderProps> = ({
|
||||||
onChange={onChangeInstance}
|
onChange={onChangeInstance}
|
||||||
noOptionsText={noInstanceText}
|
noOptionsText={noInstanceText}
|
||||||
clearable
|
clearable
|
||||||
|
searchable
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="vm-explore-metrics-header__size">
|
<div className="vm-explore-metrics-header__size">
|
||||||
|
@ -68,12 +80,14 @@ const ExploreMetricsHeader: FC<ExploreMetricsHeaderProps> = ({
|
||||||
</div>
|
</div>
|
||||||
<div className="vm-explore-metrics-header-metrics">
|
<div className="vm-explore-metrics-header-metrics">
|
||||||
<Select
|
<Select
|
||||||
|
label={"Metrics"}
|
||||||
value={selectedMetrics}
|
value={selectedMetrics}
|
||||||
list={names}
|
list={names}
|
||||||
placeholder="Search metric name"
|
placeholder="Search metric name"
|
||||||
onChange={onToggleMetric}
|
onChange={onToggleMetric}
|
||||||
noOptionsText={noMetricsText}
|
noOptionsText={noMetricsText}
|
||||||
clearable
|
clearable
|
||||||
|
searchable
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -6,17 +6,26 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
gap: $padding-global calc($padding-small + 10px);
|
gap: $padding-global calc($padding-small + 10px);
|
||||||
|
max-width: calc(100vw - var(--scrollbar-width));
|
||||||
|
|
||||||
|
&_mobile {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
&__job {
|
&__job {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
min-width: 150px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__instance {
|
&__instance {
|
||||||
flex-grow: 2;
|
flex-grow: 2;
|
||||||
|
min-width: 150px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__size {
|
&__size {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
min-width: 150px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-metrics {
|
&-metrics {
|
||||||
|
@ -35,5 +44,4 @@
|
||||||
opacity: 0.7
|
opacity: 0.7
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,8 +2,10 @@ import React, { FC } from "preact/compat";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import "./style.scss";
|
import "./style.scss";
|
||||||
import { IssueIcon, LogoIcon, WikiIcon } from "../../Main/Icons";
|
import { IssueIcon, LogoIcon, WikiIcon } from "../../Main/Icons";
|
||||||
|
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||||
|
|
||||||
const Footer: FC = () => {
|
const Footer: FC = () => {
|
||||||
|
const { isMobile } = useDeviceDetect();
|
||||||
const copyrightYears = `2019-${dayjs().format("YYYY")}`;
|
const copyrightYears = `2019-${dayjs().format("YYYY")}`;
|
||||||
|
|
||||||
return <footer className="vm-footer">
|
return <footer className="vm-footer">
|
||||||
|
@ -23,7 +25,7 @@ const Footer: FC = () => {
|
||||||
rel="help noreferrer"
|
rel="help noreferrer"
|
||||||
>
|
>
|
||||||
<WikiIcon/>
|
<WikiIcon/>
|
||||||
Documentation
|
{isMobile ? "Docs" : "Documentation"}
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
className="vm-link vm-footer__link"
|
className="vm-link vm-footer__link"
|
||||||
|
@ -32,7 +34,7 @@ const Footer: FC = () => {
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
<IssueIcon/>
|
<IssueIcon/>
|
||||||
Create an issue
|
{isMobile ? "New issue" : "Create an issue"}
|
||||||
</a>
|
</a>
|
||||||
<div className="vm-footer__copyright">
|
<div className="vm-footer__copyright">
|
||||||
© {copyrightYears} VictoriaMetrics
|
© {copyrightYears} VictoriaMetrics
|
||||||
|
|
|
@ -11,6 +11,11 @@
|
||||||
color: $color-text-secondary;
|
color: $color-text-secondary;
|
||||||
background: $color-background-body;
|
background: $color-background-body;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
padding: $padding-global;
|
||||||
|
gap: $padding-global;
|
||||||
|
}
|
||||||
|
|
||||||
&__link,
|
&__link,
|
||||||
&__website {
|
&__website {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
@ -25,7 +30,6 @@
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,6 +43,7 @@
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
font-size: $font-size-small;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,32 +1,26 @@
|
||||||
import React, { FC, useMemo } from "preact/compat";
|
import React, { FC, useMemo } from "preact/compat";
|
||||||
import { ExecutionControls } from "../../Configurators/TimeRangeSettings/ExecutionControls/ExecutionControls";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { setQueryStringWithoutPageReload } from "../../../utils/query-string";
|
import router from "../../../router";
|
||||||
import { TimeSelector } from "../../Configurators/TimeRangeSettings/TimeSelector/TimeSelector";
|
|
||||||
import GlobalSettings from "../../Configurators/GlobalSettings/GlobalSettings";
|
|
||||||
import { useLocation, useNavigate } from "react-router-dom";
|
|
||||||
import router, { RouterOptions, routerOptions } from "../../../router";
|
|
||||||
import ShortcutKeys from "../../Main/ShortcutKeys/ShortcutKeys";
|
|
||||||
import { getAppModeEnable, getAppModeParams } from "../../../utils/app-mode";
|
import { getAppModeEnable, getAppModeParams } from "../../../utils/app-mode";
|
||||||
import CardinalityDatePicker from "../../Configurators/CardinalityDatePicker/CardinalityDatePicker";
|
|
||||||
import { LogoFullIcon } from "../../Main/Icons";
|
import { LogoFullIcon } from "../../Main/Icons";
|
||||||
import { getCssVariable } from "../../../utils/theme";
|
import { getCssVariable } from "../../../utils/theme";
|
||||||
import "./style.scss";
|
import "./style.scss";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import StepConfigurator from "../../Configurators/StepConfigurator/StepConfigurator";
|
|
||||||
import { useAppState } from "../../../state/common/StateContext";
|
import { useAppState } from "../../../state/common/StateContext";
|
||||||
import HeaderNav from "./HeaderNav/HeaderNav";
|
import HeaderNav from "./HeaderNav/HeaderNav";
|
||||||
import TenantsConfiguration from "../../Configurators/GlobalSettings/TenantsConfiguration/TenantsConfiguration";
|
|
||||||
import { useFetchAccountIds } from "../../Configurators/GlobalSettings/TenantsConfiguration/hooks/useFetchAccountIds";
|
|
||||||
import useResize from "../../../hooks/useResize";
|
import useResize from "../../../hooks/useResize";
|
||||||
import SidebarHeader from "./SidebarNav/SidebarHeader";
|
import SidebarHeader from "./SidebarNav/SidebarHeader";
|
||||||
|
import HeaderControls from "./HeaderControls/HeaderControls";
|
||||||
|
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||||
|
|
||||||
const Header: FC = () => {
|
const Header: FC = () => {
|
||||||
|
const { isMobile } = useDeviceDetect();
|
||||||
|
|
||||||
const windowSize = useResize(document.body);
|
const windowSize = useResize(document.body);
|
||||||
const displaySidebar = useMemo(() => window.innerWidth < 1000, [windowSize]);
|
const displaySidebar = useMemo(() => window.innerWidth < 1000, [windowSize]);
|
||||||
|
|
||||||
const { isDarkTheme } = useAppState();
|
const { isDarkTheme } = useAppState();
|
||||||
const appModeEnable = getAppModeEnable();
|
const appModeEnable = getAppModeEnable();
|
||||||
const { accountIds } = useFetchAccountIds();
|
|
||||||
|
|
||||||
const primaryColor = useMemo(() => {
|
const primaryColor = useMemo(() => {
|
||||||
const variable = isDarkTheme ? "color-background-block" : "color-primary";
|
const variable = isDarkTheme ? "color-background-block" : "color-primary";
|
||||||
|
@ -43,15 +37,9 @@ const Header: FC = () => {
|
||||||
}, [primaryColor]);
|
}, [primaryColor]);
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { search, pathname } = useLocation();
|
|
||||||
|
|
||||||
const headerSetup = useMemo(() => {
|
|
||||||
return ((routerOptions[pathname] || {}) as RouterOptions).header || {};
|
|
||||||
}, [pathname]);
|
|
||||||
|
|
||||||
const onClickLogo = () => {
|
const onClickLogo = () => {
|
||||||
navigate({ pathname: router.home, search: search });
|
navigate({ pathname: router.home });
|
||||||
setQueryStringWithoutPageReload({});
|
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -59,7 +47,8 @@ const Header: FC = () => {
|
||||||
className={classNames({
|
className={classNames({
|
||||||
"vm-header": true,
|
"vm-header": true,
|
||||||
"vm-header_app": appModeEnable,
|
"vm-header_app": appModeEnable,
|
||||||
"vm-header_dark": isDarkTheme
|
"vm-header_dark": isDarkTheme,
|
||||||
|
"vm-header_mobile": isMobile
|
||||||
})}
|
})}
|
||||||
style={{ background, color }}
|
style={{ background, color }}
|
||||||
>
|
>
|
||||||
|
@ -67,7 +56,6 @@ const Header: FC = () => {
|
||||||
<SidebarHeader
|
<SidebarHeader
|
||||||
background={background}
|
background={background}
|
||||||
color={color}
|
color={color}
|
||||||
onClickLogo={onClickLogo}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
@ -86,15 +74,19 @@ const Header: FC = () => {
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<div className="vm-header__settings">
|
{isMobile && (
|
||||||
{headerSetup?.tenant && <TenantsConfiguration accountIds={accountIds}/>}
|
<div
|
||||||
{headerSetup?.stepControl && <StepConfigurator/>}
|
className="vm-header-logo vm-header-logo_mobile"
|
||||||
{headerSetup?.timeSelector && <TimeSelector/>}
|
onClick={onClickLogo}
|
||||||
{headerSetup?.cardinalityDatePicker && <CardinalityDatePicker/>}
|
style={{ color }}
|
||||||
{headerSetup?.executionControls && <ExecutionControls/>}
|
>
|
||||||
{!displaySidebar && <GlobalSettings/>}
|
<LogoFullIcon/>
|
||||||
{!displaySidebar && <ShortcutKeys/>}
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
<HeaderControls
|
||||||
|
displaySidebar={displaySidebar}
|
||||||
|
isMobile={isMobile}
|
||||||
|
/>
|
||||||
</header>;
|
</header>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,107 @@
|
||||||
|
import React, { FC, useMemo, useState } from "preact/compat";
|
||||||
|
import { RouterOptions, routerOptions, RouterOptionsHeader } from "../../../../router";
|
||||||
|
import TenantsConfiguration from "../../../Configurators/GlobalSettings/TenantsConfiguration/TenantsConfiguration";
|
||||||
|
import StepConfigurator from "../../../Configurators/StepConfigurator/StepConfigurator";
|
||||||
|
import { TimeSelector } from "../../../Configurators/TimeRangeSettings/TimeSelector/TimeSelector";
|
||||||
|
import CardinalityDatePicker from "../../../Configurators/CardinalityDatePicker/CardinalityDatePicker";
|
||||||
|
import { ExecutionControls } from "../../../Configurators/TimeRangeSettings/ExecutionControls/ExecutionControls";
|
||||||
|
import GlobalSettings from "../../../Configurators/GlobalSettings/GlobalSettings";
|
||||||
|
import ShortcutKeys from "../../../Main/ShortcutKeys/ShortcutKeys";
|
||||||
|
import { useLocation } from "react-router-dom";
|
||||||
|
import { useFetchAccountIds } from "../../../Configurators/GlobalSettings/TenantsConfiguration/hooks/useFetchAccountIds";
|
||||||
|
import Button from "../../../Main/Button/Button";
|
||||||
|
import { MoreIcon } from "../../../Main/Icons";
|
||||||
|
import "./style.scss";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import { getAppModeEnable } from "../../../../utils/app-mode";
|
||||||
|
import Modal from "../../../Main/Modal/Modal";
|
||||||
|
|
||||||
|
interface HeaderControlsProp {
|
||||||
|
displaySidebar: boolean
|
||||||
|
isMobile?: boolean
|
||||||
|
headerSetup?: RouterOptionsHeader
|
||||||
|
accountIds?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const Controls: FC<HeaderControlsProp> = ({
|
||||||
|
displaySidebar,
|
||||||
|
isMobile,
|
||||||
|
headerSetup,
|
||||||
|
accountIds
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames({
|
||||||
|
"vm-header-controls": true,
|
||||||
|
"vm-header-controls_mobile": isMobile,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{headerSetup?.tenant && <TenantsConfiguration accountIds={accountIds || []}/>}
|
||||||
|
{headerSetup?.stepControl && <StepConfigurator/>}
|
||||||
|
{headerSetup?.timeSelector && <TimeSelector/>}
|
||||||
|
{headerSetup?.cardinalityDatePicker && <CardinalityDatePicker/>}
|
||||||
|
{headerSetup?.executionControls && <ExecutionControls/>}
|
||||||
|
<GlobalSettings/>
|
||||||
|
{!displaySidebar && <ShortcutKeys/>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const HeaderControls: FC<HeaderControlsProp> = (props) => {
|
||||||
|
const appModeEnable = getAppModeEnable();
|
||||||
|
const [openList, setOpenList] = useState(false);
|
||||||
|
const { pathname } = useLocation();
|
||||||
|
const { accountIds } = useFetchAccountIds();
|
||||||
|
|
||||||
|
const headerSetup = useMemo(() => {
|
||||||
|
return ((routerOptions[pathname] || {}) as RouterOptions).header || {};
|
||||||
|
}, [pathname]);
|
||||||
|
|
||||||
|
const handleToggleList = () => {
|
||||||
|
setOpenList(prev => !prev);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseList = () => {
|
||||||
|
setOpenList(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (props.isMobile) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
className={classNames({
|
||||||
|
"vm-header-button": !appModeEnable
|
||||||
|
})}
|
||||||
|
startIcon={<MoreIcon/>}
|
||||||
|
onClick={handleToggleList}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Modal
|
||||||
|
title={"Controls"}
|
||||||
|
onClose={handleCloseList}
|
||||||
|
isOpen={openList}
|
||||||
|
className={classNames({
|
||||||
|
"vm-header-controls-modal": true,
|
||||||
|
"vm-header-controls-modal_open": openList,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Controls
|
||||||
|
{...props}
|
||||||
|
accountIds={accountIds}
|
||||||
|
headerSetup={headerSetup}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Controls
|
||||||
|
{...props}
|
||||||
|
accountIds={accountIds}
|
||||||
|
headerSetup={headerSetup}
|
||||||
|
/>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HeaderControls;
|
|
@ -0,0 +1,27 @@
|
||||||
|
@use "src/styles/variables" as *;
|
||||||
|
|
||||||
|
.vm-header-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: $padding-small;
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
&_mobile {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
.vm-header-button {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-modal {
|
||||||
|
transform: scale(0);
|
||||||
|
|
||||||
|
&_open {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,6 @@
|
||||||
import React, { FC, useEffect, useRef, useState } from "preact/compat";
|
import React, { FC, useEffect, useRef, useState } from "preact/compat";
|
||||||
import GlobalSettings from "../../../Configurators/GlobalSettings/GlobalSettings";
|
|
||||||
import { useLocation } from "react-router-dom";
|
import { useLocation } from "react-router-dom";
|
||||||
import ShortcutKeys from "../../../Main/ShortcutKeys/ShortcutKeys";
|
import ShortcutKeys from "../../../Main/ShortcutKeys/ShortcutKeys";
|
||||||
import { LogoFullIcon } from "../../../Main/Icons";
|
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import HeaderNav from "../HeaderNav/HeaderNav";
|
import HeaderNav from "../HeaderNav/HeaderNav";
|
||||||
import useClickOutside from "../../../../hooks/useClickOutside";
|
import useClickOutside from "../../../../hooks/useClickOutside";
|
||||||
|
@ -13,13 +11,11 @@ import "./style.scss";
|
||||||
interface SidebarHeaderProps {
|
interface SidebarHeaderProps {
|
||||||
background: string
|
background: string
|
||||||
color: string
|
color: string
|
||||||
onClickLogo: () => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const SidebarHeader: FC<SidebarHeaderProps> = ({
|
const SidebarHeader: FC<SidebarHeaderProps> = ({
|
||||||
background,
|
background,
|
||||||
color,
|
color,
|
||||||
onClickLogo,
|
|
||||||
}) => {
|
}) => {
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
const { isMobile } = useDeviceDetect();
|
const { isMobile } = useDeviceDetect();
|
||||||
|
@ -48,11 +44,9 @@ const SidebarHeader: FC<SidebarHeaderProps> = ({
|
||||||
"vm-header-sidebar-button": true,
|
"vm-header-sidebar-button": true,
|
||||||
"vm-header-sidebar-button_open": openMenu
|
"vm-header-sidebar-button_open": openMenu
|
||||||
})}
|
})}
|
||||||
|
onClick={handleToggleMenu}
|
||||||
>
|
>
|
||||||
<MenuBurger
|
<MenuBurger open={openMenu}/>
|
||||||
open={openMenu}
|
|
||||||
onClick={handleToggleMenu}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={classNames({
|
className={classNames({
|
||||||
|
@ -60,13 +54,6 @@ const SidebarHeader: FC<SidebarHeaderProps> = ({
|
||||||
"vm-header-sidebar-menu_open": openMenu
|
"vm-header-sidebar-menu_open": openMenu
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div
|
|
||||||
className="vm-header-sidebar-menu__logo"
|
|
||||||
onClick={onClickLogo}
|
|
||||||
style={{ color }}
|
|
||||||
>
|
|
||||||
<LogoFullIcon/>
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<HeaderNav
|
<HeaderNav
|
||||||
color={color}
|
color={color}
|
||||||
|
@ -75,7 +62,6 @@ const SidebarHeader: FC<SidebarHeaderProps> = ({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="vm-header-sidebar-menu-settings">
|
<div className="vm-header-sidebar-menu-settings">
|
||||||
<GlobalSettings showTitle={true}/>
|
|
||||||
{!isMobile && <ShortcutKeys showTitle={true}/>}
|
{!isMobile && <ShortcutKeys showTitle={true}/>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
@use "src/styles/variables" as *;
|
@use "src/styles/variables" as *;
|
||||||
|
|
||||||
|
$sidebar-transition: cubic-bezier(0.280, 0.840, 0.420, 1);
|
||||||
|
|
||||||
.vm-header-sidebar {
|
.vm-header-sidebar {
|
||||||
width: 24px;
|
width: 24px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
|
@ -7,14 +9,19 @@
|
||||||
background-color: inherit;
|
background-color: inherit;
|
||||||
|
|
||||||
&-button {
|
&-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: $padding-global;
|
left: 0;
|
||||||
top: $padding-global;
|
top: 0;
|
||||||
transition: left 300ms cubic-bezier(0.280, 0.840, 0.420, 1);
|
height: 51px;
|
||||||
|
width: 51px;
|
||||||
|
transition: left 350ms $sidebar-transition;
|
||||||
|
|
||||||
&_open {
|
&_open {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
left: calc(182px - $padding-global);
|
left: 149px;
|
||||||
z-index: 102;
|
z-index: 102;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,14 +33,14 @@
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: $padding-global;
|
gap: $padding-global;
|
||||||
padding: $padding-global;
|
padding: $padding-global;
|
||||||
grid-template-rows: auto 1fr auto;
|
grid-template-rows: 1fr auto;
|
||||||
width: 200px;
|
width: 200px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: inherit;
|
background-color: inherit;
|
||||||
z-index: 101;
|
z-index: 101;
|
||||||
transform-origin: left;
|
transform-origin: left;
|
||||||
transform: translateX(-100%);
|
transform: translateX(-100%);
|
||||||
transition: transform 300ms cubic-bezier(0.280, 0.840, 0.420, 1);
|
transition: transform 300ms $sidebar-transition;
|
||||||
box-shadow: $box-shadow-popper;
|
box-shadow: $box-shadow-popper;
|
||||||
|
|
||||||
&_open {
|
&_open {
|
||||||
|
|
|
@ -21,6 +21,12 @@
|
||||||
padding: $padding-small;
|
padding: $padding-small;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&_mobile {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 33px 1fr 33px;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
&_dark {
|
&_dark {
|
||||||
.vm-header-button,
|
.vm-header-button,
|
||||||
button:before,
|
button:before,
|
||||||
|
@ -50,18 +56,16 @@
|
||||||
max-width: 65px;
|
max-width: 65px;
|
||||||
min-width: 65px;
|
min-width: 65px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&_mobile {
|
||||||
|
max-width: 65px;
|
||||||
|
min-width: 65px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&-nav {
|
&-nav {
|
||||||
font-size: $font-size-small;
|
font-size: $font-size-small;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__settings {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: $padding-small;
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,9 +7,11 @@ import classNames from "classnames";
|
||||||
import Footer from "./Footer/Footer";
|
import Footer from "./Footer/Footer";
|
||||||
import { routerOptions } from "../../router";
|
import { routerOptions } from "../../router";
|
||||||
import { useFetchDashboards } from "../../pages/PredefinedPanels/hooks/useFetchDashboards";
|
import { useFetchDashboards } from "../../pages/PredefinedPanels/hooks/useFetchDashboards";
|
||||||
|
import useDeviceDetect from "../../hooks/useDeviceDetect";
|
||||||
|
|
||||||
const Layout: FC = () => {
|
const Layout: FC = () => {
|
||||||
const appModeEnable = getAppModeEnable();
|
const appModeEnable = getAppModeEnable();
|
||||||
|
const { isMobile } = useDeviceDetect();
|
||||||
useFetchDashboards();
|
useFetchDashboards();
|
||||||
|
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
|
@ -24,6 +26,7 @@ const Layout: FC = () => {
|
||||||
<div
|
<div
|
||||||
className={classNames({
|
className={classNames({
|
||||||
"vm-container-body": true,
|
"vm-container-body": true,
|
||||||
|
"vm-container-body_mobile": isMobile,
|
||||||
"vm-container-body_app": appModeEnable
|
"vm-container-body_app": appModeEnable
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
|
|
@ -11,8 +11,12 @@
|
||||||
padding: $padding-medium;
|
padding: $padding-medium;
|
||||||
background-color: $color-background-body;
|
background-color: $color-background-body;
|
||||||
|
|
||||||
|
&_mobile {
|
||||||
|
padding: $padding-small 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
padding: 0;
|
padding: $padding-small 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&_app {
|
&_app {
|
||||||
|
|
|
@ -4,6 +4,7 @@ import classNames from "classnames";
|
||||||
import { ErrorIcon, InfoIcon, SuccessIcon, WarningIcon } from "../Icons";
|
import { ErrorIcon, InfoIcon, SuccessIcon, WarningIcon } from "../Icons";
|
||||||
import "./style.scss";
|
import "./style.scss";
|
||||||
import { useAppState } from "../../../state/common/StateContext";
|
import { useAppState } from "../../../state/common/StateContext";
|
||||||
|
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||||
|
|
||||||
interface AlertProps {
|
interface AlertProps {
|
||||||
variant?: "success" | "error" | "info" | "warning"
|
variant?: "success" | "error" | "info" | "warning"
|
||||||
|
@ -21,13 +22,15 @@ const Alert: FC<AlertProps> = ({
|
||||||
variant,
|
variant,
|
||||||
children }) => {
|
children }) => {
|
||||||
const { isDarkTheme } = useAppState();
|
const { isDarkTheme } = useAppState();
|
||||||
|
const { isMobile } = useDeviceDetect();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames({
|
className={classNames({
|
||||||
"vm-alert": true,
|
"vm-alert": true,
|
||||||
[`vm-alert_${variant}`]: variant,
|
[`vm-alert_${variant}`]: variant,
|
||||||
"vm-alert_dark": isDarkTheme
|
"vm-alert_dark": isDarkTheme,
|
||||||
|
"vm-alert_mobile": isMobile
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div className="vm-alert__icon">{icons[variant || "info"]}</div>
|
<div className="vm-alert__icon">{icons[variant || "info"]}</div>
|
||||||
|
|
|
@ -15,6 +15,11 @@
|
||||||
color: $color-text;
|
color: $color-text;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
|
|
||||||
|
&_mobile {
|
||||||
|
align-items: flex-start;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
&:after {
|
&:after {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
content: '';
|
content: '';
|
||||||
|
@ -27,6 +32,10 @@
|
||||||
opacity: 0.1;
|
opacity: 0.1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&_mobile:after {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
&__icon,
|
&__icon,
|
||||||
&__content {
|
&__content {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -48,8 +57,8 @@
|
||||||
color: $color-success;
|
color: $color-success;
|
||||||
|
|
||||||
&:after {
|
&:after {
|
||||||
background-color: $color-success;
|
background-color: $color-success;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&_error {
|
&_error {
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import React, { FC, Ref, useEffect, useMemo, useRef, useState } from "preact/compat";
|
import React, { FC, Ref, useEffect, useMemo, useRef, useState } from "preact/compat";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import useClickOutside from "../../../hooks/useClickOutside";
|
|
||||||
import Popper from "../Popper/Popper";
|
import Popper from "../Popper/Popper";
|
||||||
import "./style.scss";
|
import "./style.scss";
|
||||||
import { DoneIcon } from "../Icons";
|
import { DoneIcon } from "../Icons";
|
||||||
|
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||||
|
|
||||||
interface AutocompleteProps {
|
interface AutocompleteProps {
|
||||||
value: string
|
value: string
|
||||||
|
@ -15,7 +15,9 @@ interface AutocompleteProps {
|
||||||
fullWidth?: boolean
|
fullWidth?: boolean
|
||||||
noOptionsText?: string
|
noOptionsText?: string
|
||||||
selected?: string[]
|
selected?: string[]
|
||||||
onSelect: (val: string) => void,
|
label?: string
|
||||||
|
disabledFullScreen?: boolean
|
||||||
|
onSelect: (val: string) => void
|
||||||
onOpenAutocomplete?: (val: boolean) => void
|
onOpenAutocomplete?: (val: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,9 +31,12 @@ const Autocomplete: FC<AutocompleteProps> = ({
|
||||||
fullWidth,
|
fullWidth,
|
||||||
selected,
|
selected,
|
||||||
noOptionsText,
|
noOptionsText,
|
||||||
|
label,
|
||||||
|
disabledFullScreen,
|
||||||
onSelect,
|
onSelect,
|
||||||
onOpenAutocomplete
|
onOpenAutocomplete
|
||||||
}) => {
|
}) => {
|
||||||
|
const { isMobile } = useDeviceDetect();
|
||||||
const wrapperEl = useRef<HTMLDivElement>(null);
|
const wrapperEl = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const [openAutocomplete, setOpenAutocomplete] = useState(false);
|
const [openAutocomplete, setOpenAutocomplete] = useState(false);
|
||||||
|
@ -118,8 +123,6 @@ const Autocomplete: FC<AutocompleteProps> = ({
|
||||||
onOpenAutocomplete && onOpenAutocomplete(openAutocomplete);
|
onOpenAutocomplete && onOpenAutocomplete(openAutocomplete);
|
||||||
}, [openAutocomplete]);
|
}, [openAutocomplete]);
|
||||||
|
|
||||||
useClickOutside(wrapperEl, handleCloseAutocomplete, anchor);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popper
|
<Popper
|
||||||
open={openAutocomplete}
|
open={openAutocomplete}
|
||||||
|
@ -127,9 +130,14 @@ const Autocomplete: FC<AutocompleteProps> = ({
|
||||||
placement="bottom-left"
|
placement="bottom-left"
|
||||||
onClose={handleCloseAutocomplete}
|
onClose={handleCloseAutocomplete}
|
||||||
fullWidth={fullWidth}
|
fullWidth={fullWidth}
|
||||||
|
title={isMobile ? label : undefined}
|
||||||
|
disabledFullScreen={disabledFullScreen}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="vm-autocomplete"
|
className={classNames({
|
||||||
|
"vm-autocomplete": true,
|
||||||
|
"vm-autocomplete_mobile": isMobile && !disabledFullScreen,
|
||||||
|
})}
|
||||||
ref={wrapperEl}
|
ref={wrapperEl}
|
||||||
>
|
>
|
||||||
{displayNoOptionsText && <div className="vm-autocomplete__no-options">{noOptionsText}</div>}
|
{displayNoOptionsText && <div className="vm-autocomplete__no-options">{noOptionsText}</div>}
|
||||||
|
@ -137,6 +145,7 @@ const Autocomplete: FC<AutocompleteProps> = ({
|
||||||
<div
|
<div
|
||||||
className={classNames({
|
className={classNames({
|
||||||
"vm-list-item": true,
|
"vm-list-item": true,
|
||||||
|
"vm-list-item_mobile": isMobile,
|
||||||
"vm-list-item_active": i === focusOption,
|
"vm-list-item_active": i === focusOption,
|
||||||
"vm-list-item_multiselect": selected,
|
"vm-list-item_multiselect": selected,
|
||||||
"vm-list-item_multiselect_selected": selected?.includes(option)
|
"vm-list-item_multiselect_selected": selected?.includes(option)
|
||||||
|
|
|
@ -3,6 +3,11 @@
|
||||||
.vm-autocomplete {
|
.vm-autocomplete {
|
||||||
max-height: 300px;
|
max-height: 300px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
overscroll-behavior: none;
|
||||||
|
|
||||||
|
&_mobile {
|
||||||
|
max-height: calc(($vh * 100) - 70px);
|
||||||
|
}
|
||||||
|
|
||||||
&__no-options {
|
&__no-options {
|
||||||
padding: $padding-global;
|
padding: $padding-global;
|
||||||
|
|
|
@ -93,6 +93,7 @@ $button-radius: 6px;
|
||||||
/* variant CONTAINED */
|
/* variant CONTAINED */
|
||||||
&_contained_primary {
|
&_contained_primary {
|
||||||
color: $color-primary-text;
|
color: $color-primary-text;
|
||||||
|
background-color: $color-primary;
|
||||||
|
|
||||||
&:before {
|
&:before {
|
||||||
background-color: $color-primary;
|
background-color: $color-primary;
|
||||||
|
|
|
@ -8,6 +8,8 @@ import { DATE_TIME_FORMAT } from "../../../../constants/date";
|
||||||
import "./style.scss";
|
import "./style.scss";
|
||||||
import { CalendarIcon, ClockIcon } from "../../Icons";
|
import { CalendarIcon, ClockIcon } from "../../Icons";
|
||||||
import Tabs from "../../Tabs/Tabs";
|
import Tabs from "../../Tabs/Tabs";
|
||||||
|
import useDeviceDetect from "../../../../hooks/useDeviceDetect";
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
interface DatePickerProps {
|
interface DatePickerProps {
|
||||||
date: Date | Dayjs
|
date: Date | Dayjs
|
||||||
|
@ -33,6 +35,7 @@ const Calendar: FC<DatePickerProps> = ({
|
||||||
const [viewDate, setViewDate] = useState(dayjs.tz(date));
|
const [viewDate, setViewDate] = useState(dayjs.tz(date));
|
||||||
const [selectDate, setSelectDate] = useState(dayjs.tz(date));
|
const [selectDate, setSelectDate] = useState(dayjs.tz(date));
|
||||||
const [tab, setTab] = useState(tabs[0].value);
|
const [tab, setTab] = useState(tabs[0].value);
|
||||||
|
const { isMobile } = useDeviceDetect();
|
||||||
|
|
||||||
const toggleDisplayYears = () => {
|
const toggleDisplayYears = () => {
|
||||||
setDisplayYears(prev => !prev);
|
setDisplayYears(prev => !prev);
|
||||||
|
@ -67,7 +70,12 @@ const Calendar: FC<DatePickerProps> = ({
|
||||||
}, [selectDate]);
|
}, [selectDate]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="vm-calendar">
|
<div
|
||||||
|
className={classNames({
|
||||||
|
"vm-calendar": true,
|
||||||
|
"vm-calendar_mobile": isMobile,
|
||||||
|
})}
|
||||||
|
>
|
||||||
{tab === "date" && (
|
{tab === "date" && (
|
||||||
<CalendarHeader
|
<CalendarHeader
|
||||||
viewDate={viewDate}
|
viewDate={viewDate}
|
||||||
|
|
|
@ -9,6 +9,10 @@
|
||||||
background-color: $color-background-block;
|
background-color: $color-background-block;
|
||||||
border-radius: $border-radius-medium;
|
border-radius: $border-radius-medium;
|
||||||
|
|
||||||
|
&_mobile {
|
||||||
|
padding: 0 $padding-global;
|
||||||
|
}
|
||||||
|
|
||||||
&__tabs {
|
&__tabs {
|
||||||
margin: 0 0-$padding-global 0-$padding-global;
|
margin: 0 0-$padding-global 0-$padding-global;
|
||||||
border-top: $border-divider;
|
border-top: $border-divider;
|
||||||
|
@ -61,6 +65,8 @@
|
||||||
|
|
||||||
&__prev,
|
&__prev,
|
||||||
&__next {
|
&__next {
|
||||||
|
margin: -8px;
|
||||||
|
padding: 8px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: opacity 200ms ease-in-out;
|
transition: opacity 200ms ease-in-out;
|
||||||
|
|
||||||
|
@ -87,6 +93,11 @@
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
|
|
||||||
|
@media (max-width: 500px) {
|
||||||
|
grid-template-columns: repeat(7, calc((100vw - ($padding-global * 2) - (6 * 2px))/7));
|
||||||
|
grid-template-rows: repeat(6, calc((100vw - ($padding-global * 2) - (5 * 2px))/7));
|
||||||
|
}
|
||||||
|
|
||||||
&-cell {
|
&-cell {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
@ -3,12 +3,14 @@ import Calendar from "../../Main/DatePicker/Calendar/Calendar";
|
||||||
import dayjs, { Dayjs } from "dayjs";
|
import dayjs, { Dayjs } from "dayjs";
|
||||||
import Popper from "../../Main/Popper/Popper";
|
import Popper from "../../Main/Popper/Popper";
|
||||||
import { DATE_TIME_FORMAT } from "../../../constants/date";
|
import { DATE_TIME_FORMAT } from "../../../constants/date";
|
||||||
|
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||||
|
|
||||||
interface DatePickerProps {
|
interface DatePickerProps {
|
||||||
date: string | Date | Dayjs,
|
date: string | Date | Dayjs,
|
||||||
targetRef: Ref<HTMLElement>
|
targetRef: Ref<HTMLElement>
|
||||||
format?: string
|
format?: string
|
||||||
timepicker?: boolean
|
timepicker?: boolean
|
||||||
|
label?: string
|
||||||
onChange: (val: string) => void
|
onChange: (val: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,9 +20,11 @@ const DatePicker = forwardRef<HTMLDivElement, DatePickerProps>(({
|
||||||
format = DATE_TIME_FORMAT,
|
format = DATE_TIME_FORMAT,
|
||||||
timepicker,
|
timepicker,
|
||||||
onChange,
|
onChange,
|
||||||
|
label
|
||||||
}, ref) => {
|
}, ref) => {
|
||||||
const [openCalendar, setOpenCalendar] = useState(false);
|
const [openCalendar, setOpenCalendar] = useState(false);
|
||||||
const dateDayjs = useMemo(() => date ? dayjs.tz(date) : dayjs().tz(), [date]);
|
const dateDayjs = useMemo(() => date ? dayjs.tz(date) : dayjs().tz(), [date]);
|
||||||
|
const { isMobile } = useDeviceDetect();
|
||||||
|
|
||||||
const toggleOpenCalendar = () => {
|
const toggleOpenCalendar = () => {
|
||||||
setOpenCalendar(prev => !prev);
|
setOpenCalendar(prev => !prev);
|
||||||
|
@ -61,6 +65,7 @@ const DatePicker = forwardRef<HTMLDivElement, DatePickerProps>(({
|
||||||
buttonRef={targetRef}
|
buttonRef={targetRef}
|
||||||
placement="bottom-right"
|
placement="bottom-right"
|
||||||
onClose={handleCloseCalendar}
|
onClose={handleCloseCalendar}
|
||||||
|
title={isMobile ? label : undefined}
|
||||||
>
|
>
|
||||||
<div ref={ref}>
|
<div ref={ref}>
|
||||||
<Calendar
|
<Calendar
|
||||||
|
|
|
@ -125,17 +125,6 @@ export const ArrowDropDownIcon = () => (
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const PlusCircleFillIcon = () => (
|
|
||||||
<svg
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm5 11h-4v4h-2v-4H7v-2h4V7h2v4h4v2z"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
|
|
||||||
export const ClockIcon = () => (
|
export const ClockIcon = () => (
|
||||||
<svg
|
<svg
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
|
@ -181,15 +170,6 @@ export const KeyboardIcon = () => (
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const RemoveCircleIcon = () => (
|
|
||||||
<svg
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="currentColor"
|
|
||||||
>
|
|
||||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm5 11H7v-2h10v2z"></path>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
|
|
||||||
export const PlayIcon = () => (
|
export const PlayIcon = () => (
|
||||||
<svg
|
<svg
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
|
@ -257,6 +237,15 @@ export const PlusIcon = () => (
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const MinusIcon = () => (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path d="M19 13H5v-2h14v2z"></path>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
export const DoneIcon = () => (
|
export const DoneIcon = () => (
|
||||||
<svg
|
<svg
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
|
@ -310,30 +299,6 @@ export const DragIcon = () => (
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const SearchIcon = () => (
|
|
||||||
<svg
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
|
|
||||||
export const ResizeIcon = () => (
|
|
||||||
<svg
|
|
||||||
className="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium MuiBox-root css-1om0hkc"
|
|
||||||
focusable="false"
|
|
||||||
aria-hidden="true"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
data-testid="OpenInFullIcon"
|
|
||||||
fill="currentColor"
|
|
||||||
>
|
|
||||||
<path d="M21 11V3h-8l3.29 3.29-10 10L3 13v8h8l-3.29-3.29 10-10z"></path>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
|
|
||||||
export const TimelineIcon = () => (
|
export const TimelineIcon = () => (
|
||||||
<svg
|
<svg
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
|
@ -401,13 +366,24 @@ export const StorageIcon = () => (
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const MenuIcon = () => (
|
export const MoreIcon = () => (
|
||||||
<svg
|
<svg
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M4 18h16c.55 0 1-.45 1-1s-.45-1-1-1H4c-.55 0-1 .45-1 1s.45 1 1 1zm0-5h16c.55 0 1-.45 1-1s-.45-1-1-1H4c-.55 0-1 .45-1 1s.45 1 1 1zM3 7c0 .55.45 1 1 1h16c.55 0 1-.45 1-1s-.45-1-1-1H4c-.55 0-1 .45-1 1z"
|
d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const TuneIcon = () => (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M3 17v2h6v-2H3zM3 5v2h10V5H3zm10 16v-2h8v-2h-8v-2h-2v6h2zM7 9v2H3v2h4v2h2V9H7zm14 4v-2H11v2h10zm-6-4h2V7h4V5h-4V3h-2v6z"
|
||||||
></path>
|
></path>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
|
@ -2,13 +2,12 @@ import React from "preact/compat";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import "./style.scss";
|
import "./style.scss";
|
||||||
|
|
||||||
const MenuBurger = ({ open, onClick }: {open: boolean, onClick: () => void}) => (
|
const MenuBurger = ({ open }: {open: boolean}) => (
|
||||||
<button
|
<button
|
||||||
className={classNames({
|
className={classNames({
|
||||||
"vm-menu-burger": true,
|
"vm-menu-burger": true,
|
||||||
"vm-menu-burger_opened": open
|
"vm-menu-burger_opened": open
|
||||||
})}
|
})}
|
||||||
onClick={onClick}
|
|
||||||
>
|
>
|
||||||
<span></span>
|
<span></span>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -6,15 +6,26 @@ import { ReactNode, MouseEvent } from "react";
|
||||||
import "./style.scss";
|
import "./style.scss";
|
||||||
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
interface ModalProps {
|
interface ModalProps {
|
||||||
title?: string
|
title?: string
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
|
className?: string
|
||||||
|
isOpen?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const Modal: FC<ModalProps> = ({ title, children, onClose }) => {
|
const Modal: FC<ModalProps> = ({
|
||||||
|
title,
|
||||||
|
children,
|
||||||
|
onClose,
|
||||||
|
className,
|
||||||
|
isOpen= true
|
||||||
|
}) => {
|
||||||
const { isMobile } = useDeviceDetect();
|
const { isMobile } = useDeviceDetect();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
const handleKeyUp = (e: KeyboardEvent) => {
|
const handleKeyUp = (e: KeyboardEvent) => {
|
||||||
if (e.key === "Escape") onClose();
|
if (e.key === "Escape") onClose();
|
||||||
|
@ -24,7 +35,23 @@ const Modal: FC<ModalProps> = ({ title, children, onClose }) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handlePopstate = () => {
|
||||||
|
if (isOpen) {
|
||||||
|
navigate(location, { replace: true });
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
window.addEventListener("popstate", handlePopstate);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("popstate", handlePopstate);
|
||||||
|
};
|
||||||
|
}, [isOpen, location]);
|
||||||
|
|
||||||
|
const handleDisplayModal = () => {
|
||||||
|
if (!isOpen) return;
|
||||||
document.body.style.overflow = "hidden";
|
document.body.style.overflow = "hidden";
|
||||||
window.addEventListener("keyup", handleKeyUp);
|
window.addEventListener("keyup", handleKeyUp);
|
||||||
|
|
||||||
|
@ -32,18 +59,24 @@ const Modal: FC<ModalProps> = ({ title, children, onClose }) => {
|
||||||
document.body.style.overflow = "auto";
|
document.body.style.overflow = "auto";
|
||||||
window.removeEventListener("keyup", handleKeyUp);
|
window.removeEventListener("keyup", handleKeyUp);
|
||||||
};
|
};
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
|
useEffect(handleDisplayModal, [isOpen]);
|
||||||
|
|
||||||
return ReactDOM.createPortal((
|
return ReactDOM.createPortal((
|
||||||
<div
|
<div
|
||||||
className={classNames({
|
className={classNames({
|
||||||
"vm-modal": true,
|
"vm-modal": true,
|
||||||
"vm-modal_mobile": isMobile
|
"vm-modal_mobile": isMobile,
|
||||||
|
[`${className}`]: className
|
||||||
})}
|
})}
|
||||||
onMouseDown={onClose}
|
onMouseDown={onClose}
|
||||||
>
|
>
|
||||||
<div className="vm-modal-content">
|
<div className="vm-modal-content">
|
||||||
<div className="vm-modal-content-header">
|
<div
|
||||||
|
className="vm-modal-content-header"
|
||||||
|
onMouseDown={handleMouseDown}
|
||||||
|
>
|
||||||
{title && (
|
{title && (
|
||||||
<div className="vm-modal-content-header__title">
|
<div className="vm-modal-content-header__title">
|
||||||
{title}
|
{title}
|
||||||
|
|
|
@ -14,16 +14,31 @@ $padding-modal: 22px;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background: rgba($color-black, 0.55);
|
background: rgba($color-black, 0.55);
|
||||||
|
|
||||||
&_mobile &-content {
|
&_mobile {
|
||||||
|
align-items: flex-start;
|
||||||
min-height: calc($vh * 100);
|
min-height: calc($vh * 100);
|
||||||
max-height: calc($vh * 100);
|
max-height: calc($vh * 100);
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&_mobile &-content {
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
|
overflow: visible;
|
||||||
|
min-height: 100%;
|
||||||
|
max-height: max-content;
|
||||||
|
grid-template-rows: 70px max-content;
|
||||||
|
|
||||||
|
&-header {
|
||||||
|
padding: $padding-small $padding-small $padding-small $padding-global;
|
||||||
|
margin-bottom: $padding-global;
|
||||||
|
}
|
||||||
|
|
||||||
&-body {
|
&-body {
|
||||||
display: grid;
|
display: grid;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
|
padding: 0 $padding-global $padding-modal;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,7 +46,6 @@ $padding-modal: 22px;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: auto 1fr;
|
grid-template-rows: auto 1fr;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
padding: $padding-modal;
|
|
||||||
background: $color-background-block;
|
background: $color-background-block;
|
||||||
box-shadow: 0 0 24px rgba($color-black, 0.07);
|
box-shadow: 0 0 24px rgba($color-black, 0.07);
|
||||||
border-radius: $border-radius-small;
|
border-radius: $border-radius-small;
|
||||||
|
@ -39,14 +53,25 @@ $padding-modal: 22px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
|
||||||
&-header {
|
&-header {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr auto;
|
grid-template-columns: 1fr auto;
|
||||||
|
gap: $padding-small;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: $padding-modal ;
|
justify-content: space-between;
|
||||||
|
background-color: $color-background-block;
|
||||||
|
padding: $padding-global $padding-modal;
|
||||||
|
border-radius: $border-radius-small $border-radius-small 0 0;
|
||||||
|
color: $color-text;
|
||||||
|
border-bottom: $border-divider;
|
||||||
|
margin-bottom: $padding-modal;
|
||||||
|
min-height: 51px;
|
||||||
|
z-index: 3;
|
||||||
|
|
||||||
&__title {
|
&__title {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: $font-size-medium;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__close {
|
&__close {
|
||||||
|
@ -61,7 +86,9 @@ $padding-modal: 22px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&-body {}
|
&-body {
|
||||||
|
padding: 0 $padding-modal $padding-modal;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,12 @@
|
||||||
import React, { FC, ReactNode, useEffect, useMemo, useRef, useState } from "react";
|
import React, { FC, MouseEvent as ReactMouseEvent, ReactNode, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import "./style.scss";
|
import "./style.scss";
|
||||||
import useClickOutside from "../../../hooks/useClickOutside";
|
import useClickOutside from "../../../hooks/useClickOutside";
|
||||||
|
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||||
|
import Button from "../Button/Button";
|
||||||
|
import { CloseIcon } from "../Icons";
|
||||||
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
interface PopperProps {
|
interface PopperProps {
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
|
@ -14,6 +18,8 @@ interface PopperProps {
|
||||||
offset?: {top: number, left: number}
|
offset?: {top: number, left: number}
|
||||||
clickOutside?: boolean,
|
clickOutside?: boolean,
|
||||||
fullWidth?: boolean
|
fullWidth?: boolean
|
||||||
|
title?: string
|
||||||
|
disabledFullScreen?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const Popper: FC<PopperProps> = ({
|
const Popper: FC<PopperProps> = ({
|
||||||
|
@ -24,10 +30,14 @@ const Popper: FC<PopperProps> = ({
|
||||||
onClose,
|
onClose,
|
||||||
offset = { top: 6, left: 0 },
|
offset = { top: 6, left: 0 },
|
||||||
clickOutside = true,
|
clickOutside = true,
|
||||||
fullWidth
|
fullWidth,
|
||||||
|
title,
|
||||||
|
disabledFullScreen
|
||||||
}) => {
|
}) => {
|
||||||
|
const { isMobile } = useDeviceDetect();
|
||||||
const [isOpen, setIsOpen] = useState(true);
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [popperSize, setPopperSize] = useState({ width: 0, height: 0 });
|
const [popperSize, setPopperSize] = useState({ width: 0, height: 0 });
|
||||||
|
|
||||||
const popperRef = useRef<HTMLDivElement>(null);
|
const popperRef = useRef<HTMLDivElement>(null);
|
||||||
|
@ -50,6 +60,13 @@ const Popper: FC<PopperProps> = ({
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isOpen && onClose) onClose();
|
if (!isOpen && onClose) onClose();
|
||||||
|
if (isOpen && isMobile && !disabledFullScreen) {
|
||||||
|
document.body.style.overflow = "hidden";
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.body.style.overflow = "auto";
|
||||||
|
};
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -63,7 +80,7 @@ const Popper: FC<PopperProps> = ({
|
||||||
const popperStyle = useMemo(() => {
|
const popperStyle = useMemo(() => {
|
||||||
const buttonEl = buttonRef.current;
|
const buttonEl = buttonRef.current;
|
||||||
|
|
||||||
if (!buttonEl|| !isOpen) return {};
|
if (!buttonEl || !isOpen) return {};
|
||||||
|
|
||||||
const buttonPos = buttonEl.getBoundingClientRect();
|
const buttonPos = buttonEl.getBoundingClientRect();
|
||||||
|
|
||||||
|
@ -104,28 +121,63 @@ const Popper: FC<PopperProps> = ({
|
||||||
return position;
|
return position;
|
||||||
},[buttonRef, placement, isOpen, children, fullWidth]);
|
},[buttonRef, placement, isOpen, children, fullWidth]);
|
||||||
|
|
||||||
|
const handleClickClose = (e: ReactMouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
if (clickOutside) useClickOutside(popperRef, () => setIsOpen(false), buttonRef);
|
if (clickOutside) useClickOutside(popperRef, () => setIsOpen(false), buttonRef);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!popperRef.current || !isOpen) return;
|
if (!popperRef.current || !isOpen || (isMobile && !disabledFullScreen)) return;
|
||||||
const { right, width } = popperRef.current.getBoundingClientRect();
|
const { right, width } = popperRef.current.getBoundingClientRect();
|
||||||
if (right > window.innerWidth) popperRef.current.style.left = `${window.innerWidth - 20 -width}px`;
|
if (right > window.innerWidth) {
|
||||||
|
const left = window.innerWidth - 20 - width;
|
||||||
|
popperRef.current.style.left = left < window.innerWidth ? "0" : `${left}px`;
|
||||||
|
}
|
||||||
}, [isOpen, popperRef]);
|
}, [isOpen, popperRef]);
|
||||||
|
|
||||||
|
const handlePopstate = () => {
|
||||||
|
if (isOpen && isMobile && !disabledFullScreen) {
|
||||||
|
navigate(location, { replace: true });
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.addEventListener("popstate", handlePopstate);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("popstate", handlePopstate);
|
||||||
|
};
|
||||||
|
}, [isOpen, isMobile, disabledFullScreen, location]);
|
||||||
|
|
||||||
const popperClasses = classNames({
|
const popperClasses = classNames({
|
||||||
"vm-popper": true,
|
"vm-popper": true,
|
||||||
"vm-popper_open": isOpen,
|
"vm-popper_mobile": isMobile && !disabledFullScreen,
|
||||||
|
"vm-popper_open": (isMobile || Object.keys(popperStyle).length) && isOpen,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isOpen && ReactDOM.createPortal((
|
{(isOpen || !popperSize.width) && ReactDOM.createPortal((
|
||||||
<div
|
<div
|
||||||
className={popperClasses}
|
className={popperClasses}
|
||||||
ref={popperRef}
|
ref={popperRef}
|
||||||
style={popperStyle}
|
style={(isMobile && !disabledFullScreen) ? {} : popperStyle}
|
||||||
>
|
>
|
||||||
|
{(title || (isMobile && !disabledFullScreen)) && (
|
||||||
|
<div className="vm-popper-header">
|
||||||
|
<p className="vm-popper-header__title">{title}</p>
|
||||||
|
<Button
|
||||||
|
variant="text"
|
||||||
|
size="small"
|
||||||
|
onClick={handleClickClose}
|
||||||
|
>
|
||||||
|
<CloseIcon/>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{children}
|
{children}
|
||||||
</div>), document.body)}
|
</div>), document.body)}
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -17,6 +17,38 @@
|
||||||
animation: vm-slider 150ms cubic-bezier(0.280, 0.840, 0.420, 1.1);
|
animation: vm-slider 150ms cubic-bezier(0.280, 0.840, 0.420, 1.1);
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&_mobile {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 0;
|
||||||
|
overflow: auto;
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-header {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
gap: $padding-small;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
background-color: $color-background-block;
|
||||||
|
padding: $padding-small $padding-small $padding-small $padding-global;
|
||||||
|
border-radius: $border-radius-small $border-radius-small 0 0;
|
||||||
|
color: $color-text;
|
||||||
|
border-bottom: $border-divider;
|
||||||
|
margin-bottom: $padding-global;
|
||||||
|
min-height: 51px;
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-weight: bold;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes vm-slider {
|
@keyframes vm-slider {
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { FormEvent, MouseEvent } from "react";
|
||||||
import Autocomplete from "../Autocomplete/Autocomplete";
|
import Autocomplete from "../Autocomplete/Autocomplete";
|
||||||
import { useAppState } from "../../../state/common/StateContext";
|
import { useAppState } from "../../../state/common/StateContext";
|
||||||
import "./style.scss";
|
import "./style.scss";
|
||||||
|
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||||
|
|
||||||
interface SelectProps {
|
interface SelectProps {
|
||||||
value: string | string[]
|
value: string | string[]
|
||||||
|
@ -13,6 +14,7 @@ interface SelectProps {
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
noOptionsText?: string
|
noOptionsText?: string
|
||||||
clearable?: boolean
|
clearable?: boolean
|
||||||
|
searchable?: boolean
|
||||||
autofocus?: boolean
|
autofocus?: boolean
|
||||||
onChange: (value: string) => void
|
onChange: (value: string) => void
|
||||||
}
|
}
|
||||||
|
@ -24,10 +26,12 @@ const Select: FC<SelectProps> = ({
|
||||||
placeholder,
|
placeholder,
|
||||||
noOptionsText,
|
noOptionsText,
|
||||||
clearable = false,
|
clearable = false,
|
||||||
|
searchable = false,
|
||||||
autofocus,
|
autofocus,
|
||||||
onChange
|
onChange
|
||||||
}) => {
|
}) => {
|
||||||
const { isDarkTheme } = useAppState();
|
const { isDarkTheme } = useAppState();
|
||||||
|
const { isMobile } = useDeviceDetect();
|
||||||
|
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const autocompleteAnchorEl = useRef<HTMLDivElement>(null);
|
const autocompleteAnchorEl = useRef<HTMLDivElement>(null);
|
||||||
|
@ -95,7 +99,7 @@ const Select: FC<SelectProps> = ({
|
||||||
}, [openList, inputRef]);
|
}, [openList, inputRef]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!autofocus || !inputRef.current) return;
|
if (!autofocus || !inputRef.current || isMobile) return;
|
||||||
inputRef.current.focus();
|
inputRef.current.focus();
|
||||||
}, [autofocus, inputRef]);
|
}, [autofocus, inputRef]);
|
||||||
|
|
||||||
|
@ -120,25 +124,33 @@ const Select: FC<SelectProps> = ({
|
||||||
ref={autocompleteAnchorEl}
|
ref={autocompleteAnchorEl}
|
||||||
>
|
>
|
||||||
<div className="vm-select-input-content">
|
<div className="vm-select-input-content">
|
||||||
{selectedValues && selectedValues.map(item => (
|
{!isMobile && selectedValues && selectedValues.map(item => (
|
||||||
<div
|
<div
|
||||||
className="vm-select-input-content__selected"
|
className="vm-select-input-content__selected"
|
||||||
key={item}
|
key={item}
|
||||||
>
|
>
|
||||||
{item}
|
<span>{item}</span>
|
||||||
<div onClick={createHandleClick(item)}>
|
<div onClick={createHandleClick(item)}>
|
||||||
<CloseIcon/>
|
<CloseIcon/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<input
|
{isMobile && !!selectedValues?.length && (
|
||||||
value={textFieldValue}
|
<span className="vm-select-input-content__counter">
|
||||||
type="text"
|
selected {selectedValues.length}
|
||||||
placeholder={placeholder}
|
</span>
|
||||||
onInput={handleChange}
|
)}
|
||||||
onFocus={handleFocus}
|
{!isMobile || (isMobile && (!selectedValues || !selectedValues?.length)) && (
|
||||||
ref={inputRef}
|
<input
|
||||||
/>
|
value={textFieldValue}
|
||||||
|
type="text"
|
||||||
|
placeholder={placeholder}
|
||||||
|
onInput={handleChange}
|
||||||
|
onFocus={handleFocus}
|
||||||
|
ref={inputRef}
|
||||||
|
readOnly={isMobile || !searchable}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{label && <span className="vm-text-field__label">{label}</span>}
|
{label && <span className="vm-text-field__label">{label}</span>}
|
||||||
{clearable && value && (
|
{clearable && value && (
|
||||||
|
@ -159,6 +171,7 @@ const Select: FC<SelectProps> = ({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
|
label={label}
|
||||||
value={autocompleteValue}
|
value={autocompleteValue}
|
||||||
options={list}
|
options={list}
|
||||||
anchor={autocompleteAnchorEl}
|
anchor={autocompleteAnchorEl}
|
||||||
|
|
|
@ -19,6 +19,16 @@
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: $padding-small;
|
gap: $padding-small;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
max-width: calc(100% - ($padding-global + 61px));
|
||||||
|
|
||||||
|
&_mobile {
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__counter {
|
||||||
|
font-size: $font-size;
|
||||||
|
line-height: $font-size;
|
||||||
|
}
|
||||||
|
|
||||||
&__selected {
|
&__selected {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
@ -29,6 +39,13 @@
|
||||||
border-radius: $border-radius-small;
|
border-radius: $border-radius-small;
|
||||||
font-size: $font-size;
|
font-size: $font-size;
|
||||||
line-height: $font-size;
|
line-height: $font-size;
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
|
span {
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
width: 20px;
|
width: 20px;
|
||||||
|
@ -95,4 +112,20 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&-options {
|
||||||
|
display: grid;
|
||||||
|
gap: $padding-small;
|
||||||
|
max-width: 300px;
|
||||||
|
max-height: 208px;
|
||||||
|
overflow: auto;
|
||||||
|
padding: $padding-global;
|
||||||
|
font-size: $font-size;
|
||||||
|
|
||||||
|
&_mobile {
|
||||||
|
padding: 0 $padding-global $padding-small;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: calc(($vh * 100) - 70px);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,11 +8,17 @@ interface SwitchProps {
|
||||||
color?: "primary" | "secondary" | "error"
|
color?: "primary" | "secondary" | "error"
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
label?: string | ReactNode
|
label?: string | ReactNode
|
||||||
|
fullWidth?: boolean
|
||||||
onChange: (value: boolean) => void
|
onChange: (value: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const Switch: FC<SwitchProps> = ({
|
const Switch: FC<SwitchProps> = ({
|
||||||
value = false, disabled = false, label, color = "secondary", onChange
|
value = false,
|
||||||
|
disabled = false,
|
||||||
|
label,
|
||||||
|
color = "secondary",
|
||||||
|
fullWidth,
|
||||||
|
onChange
|
||||||
}) => {
|
}) => {
|
||||||
const toggleSwitch = () => {
|
const toggleSwitch = () => {
|
||||||
if (disabled) return;
|
if (disabled) return;
|
||||||
|
@ -21,6 +27,7 @@ const Switch: FC<SwitchProps> = ({
|
||||||
|
|
||||||
const switchClasses = classNames({
|
const switchClasses = classNames({
|
||||||
"vm-switch": true,
|
"vm-switch": true,
|
||||||
|
"vm-switch_full-width": fullWidth,
|
||||||
"vm-switch_disabled": disabled,
|
"vm-switch_disabled": disabled,
|
||||||
"vm-switch_active": value,
|
"vm-switch_active": value,
|
||||||
[`vm-switch_${color}_active`]: value,
|
[`vm-switch_${color}_active`]: value,
|
||||||
|
|
|
@ -11,6 +11,16 @@ $switch-border-radius: $switch-handle-size + ($switch-padding * 2);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
&_full-width {
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
&_full-width &__label {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
&_disabled {
|
&_disabled {
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
|
|
|
@ -2,6 +2,7 @@ import React, { FC, KeyboardEvent, useEffect, useRef, HTMLInputTypeAttribute, Re
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { useMemo } from "preact/compat";
|
import { useMemo } from "preact/compat";
|
||||||
import { useAppState } from "../../../state/common/StateContext";
|
import { useAppState } from "../../../state/common/StateContext";
|
||||||
|
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||||
import "./style.scss";
|
import "./style.scss";
|
||||||
|
|
||||||
interface TextFieldProps {
|
interface TextFieldProps {
|
||||||
|
@ -15,6 +16,7 @@ interface TextFieldProps {
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
autofocus?: boolean
|
autofocus?: boolean
|
||||||
helperText?: string
|
helperText?: string
|
||||||
|
inputmode?: "search" | "text" | "email" | "tel" | "url" | "none" | "numeric" | "decimal"
|
||||||
onChange?: (value: string) => void
|
onChange?: (value: string) => void
|
||||||
onEnter?: () => void
|
onEnter?: () => void
|
||||||
onKeyDown?: (e: KeyboardEvent) => void
|
onKeyDown?: (e: KeyboardEvent) => void
|
||||||
|
@ -33,6 +35,7 @@ const TextField: FC<TextFieldProps> = ({
|
||||||
disabled = false,
|
disabled = false,
|
||||||
autofocus = false,
|
autofocus = false,
|
||||||
helperText,
|
helperText,
|
||||||
|
inputmode = "text",
|
||||||
onChange,
|
onChange,
|
||||||
onEnter,
|
onEnter,
|
||||||
onKeyDown,
|
onKeyDown,
|
||||||
|
@ -40,6 +43,7 @@ const TextField: FC<TextFieldProps> = ({
|
||||||
onBlur
|
onBlur
|
||||||
}) => {
|
}) => {
|
||||||
const { isDarkTheme } = useAppState();
|
const { isDarkTheme } = useAppState();
|
||||||
|
const { isMobile } = useDeviceDetect();
|
||||||
|
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
@ -67,7 +71,7 @@ const TextField: FC<TextFieldProps> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!autofocus) return;
|
if (!autofocus || isMobile) return;
|
||||||
fieldRef?.current?.focus && fieldRef.current.focus();
|
fieldRef?.current?.focus && fieldRef.current.focus();
|
||||||
}, [fieldRef, autofocus]);
|
}, [fieldRef, autofocus]);
|
||||||
|
|
||||||
|
@ -97,7 +101,9 @@ const TextField: FC<TextFieldProps> = ({
|
||||||
ref={textareaRef}
|
ref={textareaRef}
|
||||||
value={value}
|
value={value}
|
||||||
rows={1}
|
rows={1}
|
||||||
|
inputMode={inputmode}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
|
autoCapitalize={"none"}
|
||||||
onInput={handleChange}
|
onInput={handleChange}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
onFocus={handleFocus}
|
onFocus={handleFocus}
|
||||||
|
@ -112,6 +118,8 @@ const TextField: FC<TextFieldProps> = ({
|
||||||
value={value}
|
value={value}
|
||||||
type={type}
|
type={type}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
|
inputMode={inputmode}
|
||||||
|
autoCapitalize={"none"}
|
||||||
onInput={handleChange}
|
onInput={handleChange}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
onFocus={handleFocus}
|
onFocus={handleFocus}
|
||||||
|
|
|
@ -43,6 +43,11 @@
|
||||||
-webkit-line-clamp: 2; /* number of lines to show */
|
-webkit-line-clamp: 2; /* number of lines to show */
|
||||||
line-clamp: 2;
|
line-clamp: 2;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
|
|
||||||
|
@media (max-width: 500px) {
|
||||||
|
-webkit-line-clamp: 1; /* number of lines to show */
|
||||||
|
line-clamp: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__label {
|
&__label {
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { darkPalette, lightPalette } from "../../../constants/palette";
|
||||||
import { Theme } from "../../../types";
|
import { Theme } from "../../../types";
|
||||||
import { useAppDispatch, useAppState } from "../../../state/common/StateContext";
|
import { useAppDispatch, useAppState } from "../../../state/common/StateContext";
|
||||||
import useSystemTheme from "../../../hooks/useSystemTheme";
|
import useSystemTheme from "../../../hooks/useSystemTheme";
|
||||||
|
import useResize from "../../../hooks/useResize";
|
||||||
|
|
||||||
interface ThemeProviderProps {
|
interface ThemeProviderProps {
|
||||||
onLoaded: (val: boolean) => void
|
onLoaded: (val: boolean) => void
|
||||||
|
@ -28,6 +29,7 @@ export const ThemeProvider: FC<ThemeProviderProps> = ({ onLoaded }) => {
|
||||||
const { theme } = useAppState();
|
const { theme } = useAppState();
|
||||||
const isDarkTheme = useSystemTheme();
|
const isDarkTheme = useSystemTheme();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
const windowSize = useResize(document.body);
|
||||||
|
|
||||||
const [palette, setPalette] = useState({
|
const [palette, setPalette] = useState({
|
||||||
[Theme.dark]: darkPalette,
|
[Theme.dark]: darkPalette,
|
||||||
|
@ -93,6 +95,7 @@ export const ThemeProvider: FC<ThemeProviderProps> = ({ onLoaded }) => {
|
||||||
setTheme();
|
setTheme();
|
||||||
}, [palette]);
|
}, [palette]);
|
||||||
|
|
||||||
|
useEffect(setScrollbarSize, [windowSize]);
|
||||||
useEffect(updatePalette, [theme, isDarkTheme]);
|
useEffect(updatePalette, [theme, isDarkTheme]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue