Compare commits
13 Commits
147f16f356
...
7598d460ea
Author | SHA1 | Date |
---|---|---|
ktx-vaidehi | 7598d460ea | |
Sai Nikhil | 56ecc36c24 | |
ktx-vaidehi | e09367ceee | |
ktx-vaidehi | 21e30a6563 | |
ktx-vaidehi | 9f86b6d799 | |
Sai Nikhil | 517d136bc6 | |
Sai Nikhil | 7acbbda171 | |
Sai Nikhil | 8c4441e801 | |
Omkar Kesarkhane | 7fcd40f631 | |
Ashish Kolhe | e20c509bf4 | |
Ashish Kolhe | 4fffa6d8a0 | |
Subhra264 | 34f4a3c7cc | |
Omkar Kesarkhane | 58dc29408e |
|
@ -16,7 +16,7 @@
|
|||
use std::str::FromStr;
|
||||
|
||||
use proto::cluster_rpc;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::{
|
||||
|
@ -739,10 +739,24 @@ pub struct MultiSearchPartitionResponse {
|
|||
pub error: hashbrown::HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct SqlQuery {
|
||||
pub sql: String,
|
||||
#[serde(default)]
|
||||
pub start_time: Option<i64>,
|
||||
#[serde(default)]
|
||||
pub end_time: Option<i64>,
|
||||
#[serde(default)]
|
||||
pub query_fn: Option<String>,
|
||||
#[serde(default)]
|
||||
pub is_old_format: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[schema(as = SearchRequest)]
|
||||
pub struct MultiStreamRequest {
|
||||
pub sql: Vec<String>,
|
||||
#[serde(default, deserialize_with = "deserialize_sql")]
|
||||
pub sql: Vec<SqlQuery>, // Use the new struct for SQL queries
|
||||
#[serde(default)]
|
||||
pub encoding: RequestEncoding,
|
||||
#[serde(default)]
|
||||
|
@ -774,25 +788,63 @@ pub struct MultiStreamRequest {
|
|||
pub search_type: Option<SearchEventType>,
|
||||
#[serde(default)]
|
||||
pub index_type: String, // parquet(default) or fst
|
||||
#[serde(default)]
|
||||
pub per_query_response: bool,
|
||||
}
|
||||
|
||||
fn deserialize_sql<'de, D>(deserializer: D) -> Result<Vec<SqlQuery>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum SqlOrSqlQuery {
|
||||
OldFormat(String),
|
||||
NewFormat(SqlQuery),
|
||||
}
|
||||
|
||||
let v: Vec<SqlOrSqlQuery> = Vec::deserialize(deserializer)?;
|
||||
|
||||
// Convert old format into the new format
|
||||
let result: Vec<SqlQuery> = v
|
||||
.into_iter()
|
||||
.map(|item| match item {
|
||||
SqlOrSqlQuery::OldFormat(sql) => SqlQuery {
|
||||
sql,
|
||||
start_time: None,
|
||||
end_time: None,
|
||||
query_fn: None,
|
||||
is_old_format: true,
|
||||
},
|
||||
SqlOrSqlQuery::NewFormat(query) => query,
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
impl MultiStreamRequest {
|
||||
pub fn to_query_req(&mut self) -> Vec<Request> {
|
||||
let mut res = vec![];
|
||||
for query in &self.sql {
|
||||
let query_fn = if query.is_old_format {
|
||||
self.query_fn.clone()
|
||||
} else {
|
||||
query.query_fn.clone()
|
||||
};
|
||||
res.push(Request {
|
||||
query: Query {
|
||||
sql: query.to_string(),
|
||||
sql: query.sql.clone(),
|
||||
from: self.from,
|
||||
size: self.size,
|
||||
start_time: self.start_time,
|
||||
end_time: self.end_time,
|
||||
start_time: query.start_time.unwrap_or(self.start_time),
|
||||
end_time: query.end_time.unwrap_or(self.end_time),
|
||||
sort_by: self.sort_by.clone(),
|
||||
quick_mode: self.quick_mode,
|
||||
query_type: self.query_type.clone(),
|
||||
track_total_hits: self.track_total_hits,
|
||||
uses_zo_fn: self.uses_zo_fn,
|
||||
query_fn: self.query_fn.clone(),
|
||||
query_fn,
|
||||
skip_wal: self.skip_wal,
|
||||
},
|
||||
regions: self.regions.clone(),
|
||||
|
|
|
@ -88,7 +88,7 @@ pub fn check_auth(req: Request<()>) -> Result<Request<()>, Status> {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use config::cache_instance_id;
|
||||
use config::{cache_instance_id, get_config};
|
||||
|
||||
use super::*;
|
||||
use crate::common::meta::user::User;
|
||||
|
@ -114,6 +114,9 @@ mod tests {
|
|||
);
|
||||
|
||||
let mut request = tonic::Request::new(());
|
||||
request.set_timeout(std::time::Duration::from_secs(
|
||||
get_config().limit.query_timeout,
|
||||
));
|
||||
|
||||
let token: MetadataValue<_> = "basic cm9vdEBleGFtcGxlLmNvbTp0b2tlbg==".parse().unwrap();
|
||||
let meta: &mut tonic::metadata::MetadataMap = request.metadata_mut();
|
||||
|
@ -144,6 +147,9 @@ mod tests {
|
|||
);
|
||||
|
||||
let mut request = tonic::Request::new(());
|
||||
request.set_timeout(std::time::Duration::from_secs(
|
||||
get_config().limit.query_timeout,
|
||||
));
|
||||
let token: MetadataValue<_> = "instance".parse().unwrap();
|
||||
let meta: &mut tonic::metadata::MetadataMap = request.metadata_mut();
|
||||
meta.insert("authorization", token.clone());
|
||||
|
@ -171,6 +177,9 @@ mod tests {
|
|||
},
|
||||
);
|
||||
let mut request = tonic::Request::new(());
|
||||
request.set_timeout(std::time::Duration::from_secs(
|
||||
get_config().limit.query_timeout,
|
||||
));
|
||||
|
||||
let token: MetadataValue<_> = "basic cm9vdEBleGFtcGxlLmNvbTp0b2tlbjg4OA=="
|
||||
.parse()
|
||||
|
|
|
@ -49,13 +49,12 @@ use crate::{
|
|||
context_path = "/api",
|
||||
tag = "Search",
|
||||
operation_id = "SearchSQL",
|
||||
security(
|
||||
("Authorization"= [])
|
||||
),
|
||||
params(
|
||||
("org_id" = String, Path, description = "Organization name"),
|
||||
),
|
||||
request_body(content = SearchRequest, description = "Search query", content_type = "application/json", example = json!({
|
||||
params(("org_id" = String, Path, description = "Organization name")),
|
||||
request_body(
|
||||
content = SearchRequest,
|
||||
description = "Search query",
|
||||
content_type = "application/json",
|
||||
example = json!({
|
||||
"query": {
|
||||
"sql": "select * from k8s ",
|
||||
"start_time": 1675182660872049i64,
|
||||
|
@ -63,9 +62,15 @@ use crate::{
|
|||
"from": 0,
|
||||
"size": 10
|
||||
}
|
||||
})),
|
||||
})
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Success", content_type = "application/json", body = SearchResponse, example = json!({
|
||||
(
|
||||
status = 200,
|
||||
description = "Success",
|
||||
content_type = "application/json",
|
||||
body = SearchResponse,
|
||||
example = json!({
|
||||
"took": 155,
|
||||
"hits": [
|
||||
{
|
||||
|
@ -89,9 +94,20 @@ use crate::{
|
|||
"from": 0,
|
||||
"size": 1,
|
||||
"scan_size": 28943
|
||||
})),
|
||||
(status = 400, description = "Failure", content_type = "application/json", body = HttpResponse),
|
||||
(status = 500, description = "Failure", content_type = "application/json", body = HttpResponse),
|
||||
}),
|
||||
),
|
||||
(
|
||||
status = 400,
|
||||
description = "Failure",
|
||||
content_type = "application/json",
|
||||
body = HttpResponse,
|
||||
),
|
||||
(
|
||||
status = 500,
|
||||
description = "Failure",
|
||||
content_type = "application/json",
|
||||
body = HttpResponse,
|
||||
)
|
||||
)
|
||||
)]
|
||||
#[post("/{org_id}/_search_multi")]
|
||||
|
@ -115,18 +131,24 @@ pub async fn search_multi(
|
|||
let query = web::Query::<HashMap<String, String>>::from_query(in_req.query_string()).unwrap();
|
||||
let stream_type = match get_stream_type_from_request(&query) {
|
||||
Ok(v) => v.unwrap_or(StreamType::Logs),
|
||||
Err(e) => return Ok(MetaHttpResponse::bad_request(e)),
|
||||
Err(e) => {
|
||||
return Ok(MetaHttpResponse::bad_request(e));
|
||||
}
|
||||
};
|
||||
|
||||
let search_type = match get_search_type_from_request(&query) {
|
||||
Ok(v) => v,
|
||||
Err(e) => return Ok(MetaHttpResponse::bad_request(e)),
|
||||
Err(e) => {
|
||||
return Ok(MetaHttpResponse::bad_request(e));
|
||||
}
|
||||
};
|
||||
|
||||
// handle encoding for query and aggs
|
||||
let mut multi_req: search::MultiStreamRequest = match json::from_slice(&body) {
|
||||
Ok(v) => v,
|
||||
Err(e) => return Ok(MetaHttpResponse::bad_request(e)),
|
||||
Err(e) => {
|
||||
return Ok(MetaHttpResponse::bad_request(e));
|
||||
}
|
||||
};
|
||||
|
||||
let mut query_fn = multi_req
|
||||
|
@ -144,6 +166,8 @@ pub async fn search_multi(
|
|||
let mut queries = multi_req.to_query_req();
|
||||
let mut multi_res = search::Response::new(multi_req.from, multi_req.size);
|
||||
|
||||
let per_query_resp = multi_req.per_query_response;
|
||||
|
||||
// Before making any rpc requests, first check the sql expressions can be decoded correctly
|
||||
for req in queries.iter_mut() {
|
||||
if let Err(e) = req.decode() {
|
||||
|
@ -311,7 +335,7 @@ pub async fn search_multi(
|
|||
multi_res.took += res.took;
|
||||
|
||||
if res.total > multi_res.total {
|
||||
multi_res.total = res.total
|
||||
multi_res.total = res.total;
|
||||
}
|
||||
multi_res.from = res.from;
|
||||
multi_res.size += res.size;
|
||||
|
@ -319,10 +343,15 @@ pub async fn search_multi(
|
|||
multi_res.scan_size += res.scan_size;
|
||||
multi_res.scan_records += res.scan_records;
|
||||
multi_res.columns.extend(res.columns);
|
||||
multi_res.hits.extend(res.hits);
|
||||
multi_res.response_type = res.response_type;
|
||||
multi_res.trace_id = res.trace_id;
|
||||
multi_res.cached_ratio = res.cached_ratio;
|
||||
|
||||
if per_query_resp {
|
||||
multi_res.hits.push(serde_json::Value::Array(res.hits));
|
||||
} else {
|
||||
multi_res.hits.extend(res.hits);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
let time = start.elapsed().as_secs_f64();
|
||||
|
@ -379,19 +408,24 @@ pub async fn search_multi(
|
|||
context_path = "/api",
|
||||
tag = "Search",
|
||||
operation_id = "SearchPartitionMulti",
|
||||
security(
|
||||
("Authorization"= [])
|
||||
),
|
||||
params(
|
||||
("org_id" = String, Path, description = "Organization name"),
|
||||
),
|
||||
request_body(content = SearchRequest, description = "Search query", content_type = "application/json", example = json!({
|
||||
params(("org_id" = String, Path, description = "Organization name")),
|
||||
request_body(
|
||||
content = SearchRequest,
|
||||
description = "Search query",
|
||||
content_type = "application/json",
|
||||
example = json!({
|
||||
"sql": "select * from k8s ",
|
||||
"start_time": 1675182660872049i64,
|
||||
"end_time": 1675185660872049i64
|
||||
})),
|
||||
})
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Success", content_type = "application/json", body = SearchResponse, example = json!({
|
||||
(
|
||||
status = 200,
|
||||
description = "Success",
|
||||
content_type = "application/json",
|
||||
body = SearchResponse,
|
||||
example = json!({
|
||||
"took": 155,
|
||||
"file_num": 10,
|
||||
"original_size": 10240,
|
||||
|
@ -400,9 +434,20 @@ pub async fn search_multi(
|
|||
[1674213225158000i64, 1674213225158000i64],
|
||||
[1674213225158000i64, 1674213225158000i64],
|
||||
]
|
||||
})),
|
||||
(status = 400, description = "Failure", content_type = "application/json", body = HttpResponse),
|
||||
(status = 500, description = "Failure", content_type = "application/json", body = HttpResponse),
|
||||
}),
|
||||
),
|
||||
(
|
||||
status = 400,
|
||||
description = "Failure",
|
||||
content_type = "application/json",
|
||||
body = HttpResponse,
|
||||
),
|
||||
(
|
||||
status = 500,
|
||||
description = "Failure",
|
||||
content_type = "application/json",
|
||||
body = HttpResponse,
|
||||
)
|
||||
)
|
||||
)]
|
||||
#[post("/{org_id}/_search_partition_multi")]
|
||||
|
@ -428,12 +473,16 @@ pub async fn _search_partition_multi(
|
|||
let query = web::Query::<HashMap<String, String>>::from_query(in_req.query_string()).unwrap();
|
||||
let stream_type = match get_stream_type_from_request(&query) {
|
||||
Ok(v) => v.unwrap_or(StreamType::Logs),
|
||||
Err(e) => return Ok(MetaHttpResponse::bad_request(e)),
|
||||
Err(e) => {
|
||||
return Ok(MetaHttpResponse::bad_request(e));
|
||||
}
|
||||
};
|
||||
|
||||
let req: search::MultiSearchPartitionRequest = match json::from_slice(&body) {
|
||||
Ok(v) => v,
|
||||
Err(e) => return Ok(MetaHttpResponse::bad_request(e)),
|
||||
Err(e) => {
|
||||
return Ok(MetaHttpResponse::bad_request(e));
|
||||
}
|
||||
};
|
||||
|
||||
let search_fut = SearchService::search_partition_multi(&trace_id, &org_id, stream_type, &req);
|
||||
|
@ -570,12 +619,16 @@ pub async fn around_multi(
|
|||
let query = web::Query::<HashMap<String, String>>::from_query(in_req.query_string()).unwrap();
|
||||
let stream_type = match get_stream_type_from_request(&query) {
|
||||
Ok(v) => v.unwrap_or(StreamType::Logs),
|
||||
Err(e) => return Ok(MetaHttpResponse::bad_request(e)),
|
||||
Err(e) => {
|
||||
return Ok(MetaHttpResponse::bad_request(e));
|
||||
}
|
||||
};
|
||||
|
||||
let around_key = match query.get("key") {
|
||||
Some(v) => v.parse::<i64>().unwrap_or(0),
|
||||
None => return Ok(MetaHttpResponse::bad_request("around key is empty")),
|
||||
None => {
|
||||
return Ok(MetaHttpResponse::bad_request("around key is empty"));
|
||||
}
|
||||
};
|
||||
let mut query_fn = query
|
||||
.get("query_fn")
|
||||
|
|
|
@ -242,18 +242,50 @@ impl QueryCondition {
|
|||
}
|
||||
}
|
||||
};
|
||||
if self.search_event_type.is_none() && resp.total < trigger_condition.threshold as usize {
|
||||
let records: Option<Vec<Map<String, Value>>> = Some(
|
||||
resp.hits
|
||||
.iter()
|
||||
.map(|hit| hit.as_object().unwrap().clone())
|
||||
.collect(),
|
||||
);
|
||||
if self.search_event_type.is_none() {
|
||||
let threshold = trigger_condition.threshold as usize;
|
||||
match trigger_condition.operator {
|
||||
Operator::EqualTo => {
|
||||
if records.as_ref().unwrap().len() == threshold {
|
||||
return Ok((records, now));
|
||||
}
|
||||
}
|
||||
Operator::NotEqualTo => {
|
||||
if records.as_ref().unwrap().len() != threshold {
|
||||
return Ok((records, now));
|
||||
}
|
||||
}
|
||||
Operator::GreaterThan => {
|
||||
if records.as_ref().unwrap().len() > threshold {
|
||||
return Ok((records, now));
|
||||
}
|
||||
}
|
||||
Operator::GreaterThanEquals => {
|
||||
if records.as_ref().unwrap().len() >= threshold {
|
||||
return Ok((records, now));
|
||||
}
|
||||
}
|
||||
Operator::LessThan => {
|
||||
if records.as_ref().unwrap().len() < threshold {
|
||||
return Ok((records, now));
|
||||
}
|
||||
}
|
||||
Operator::LessThanEquals => {
|
||||
if records.as_ref().unwrap().len() <= threshold {
|
||||
return Ok((records, now));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok((None, now))
|
||||
} else {
|
||||
Ok((
|
||||
Some(
|
||||
resp.hits
|
||||
.iter()
|
||||
.map(|hit| hit.as_object().unwrap().clone())
|
||||
.collect(),
|
||||
),
|
||||
now,
|
||||
))
|
||||
Ok((records, now))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -235,7 +235,17 @@ async fn handle_alert_triggers(trigger: db::scheduler::Trigger) -> Result<(), an
|
|||
// Check for the cron timestamp after the silence period
|
||||
new_trigger.next_run_at = schedule.after(&silence).next().unwrap().timestamp_micros();
|
||||
} else {
|
||||
new_trigger.next_run_at += Duration::try_minutes(alert.trigger_condition.silence)
|
||||
// When the silence period is less than the frequency, the alert runs after the silence
|
||||
// period completely ignoring the frequency. So, if frequency is 60 mins and
|
||||
// silence is 10 mins, the condition is satisfied, in that case, the alert
|
||||
// will run after 10 mins of silence period. To avoid this scenario, we
|
||||
// should use the max of (frequency, silence) as the next_run_at.
|
||||
// Silence period is in minutes, and the frequency is in seconds.
|
||||
let next_run_in_seconds = std::cmp::max(
|
||||
alert.trigger_condition.silence * 60,
|
||||
alert.trigger_condition.frequency,
|
||||
);
|
||||
new_trigger.next_run_at += Duration::try_seconds(next_run_in_seconds)
|
||||
.unwrap()
|
||||
.num_microseconds()
|
||||
.unwrap();
|
||||
|
|
|
@ -202,7 +202,8 @@ async fn send_to_node(
|
|||
);
|
||||
break;
|
||||
}
|
||||
let request = tonic::Request::new(req_query.clone());
|
||||
let mut request = tonic::Request::new(req_query.clone());
|
||||
request.set_timeout(std::time::Duration::from_secs(cfg.limit.query_timeout));
|
||||
match client.send_file_list(request).await {
|
||||
Ok(_) => break,
|
||||
Err(e) => {
|
||||
|
|
|
@ -241,7 +241,7 @@ async fn get_file_list(
|
|||
.parse()
|
||||
.map_err(|_| DataFusionError::Execution("invalid org_id".to_string()))?;
|
||||
let mut request = tonic::Request::new(req);
|
||||
// request.set_timeout(Duration::from_secs(cfg.grpc.timeout));
|
||||
request.set_timeout(std::time::Duration::from_secs(cfg.limit.query_timeout));
|
||||
|
||||
opentelemetry::global::get_text_map_propagator(|propagator| {
|
||||
propagator.inject_context(
|
||||
|
|
|
@ -160,7 +160,7 @@ async fn search_in_cluster(
|
|||
.parse()
|
||||
.map_err(|_| Error::Message(format!("invalid org_id: {}", req.org_id)))?;
|
||||
let mut request = tonic::Request::new(req);
|
||||
// request.set_timeout(Duration::from_secs(cfg.grpc.timeout));
|
||||
request.set_timeout(std::time::Duration::from_secs(cfg.limit.query_timeout));
|
||||
|
||||
opentelemetry::global::get_text_map_propagator(|propagator| {
|
||||
propagator.inject_context(
|
||||
|
|
|
@ -86,8 +86,8 @@ pub async fn get_cached_results(
|
|||
is_descending:cache_req.is_descending,
|
||||
};
|
||||
|
||||
let request = tonic::Request::new(req);
|
||||
|
||||
let mut request = tonic::Request::new(req);
|
||||
request.set_timeout(std::time::Duration::from_secs(cfg.limit.query_timeout));
|
||||
log::info!(
|
||||
"[trace_id {trace_id}] get_cached_results->grpc: request node: {}",
|
||||
&node_addr
|
||||
|
|
|
@ -85,7 +85,8 @@ pub async fn get_cached_results(
|
|||
is_descending:cache_req.is_descending,
|
||||
};
|
||||
|
||||
let request = tonic::Request::new(req);
|
||||
let mut request = tonic::Request::new(req);
|
||||
request.set_timeout(std::time::Duration::from_secs(cfg.limit.query_timeout));
|
||||
|
||||
log::info!(
|
||||
"[trace_id {trace_id}] get_cached_results->grpc: request node: {}",
|
||||
|
|
|
@ -432,6 +432,7 @@ pub async fn query_status() -> Result<search::QueryStatusResponse, Error> {
|
|||
async move {
|
||||
let cfg = get_config();
|
||||
let mut request = tonic::Request::new(proto::cluster_rpc::QueryStatusRequest {});
|
||||
request.set_timeout(std::time::Duration::from_secs(cfg.limit.query_timeout));
|
||||
|
||||
opentelemetry::global::get_text_map_propagator(|propagator| {
|
||||
propagator.inject_context(
|
||||
|
@ -591,6 +592,7 @@ pub async fn cancel_query(
|
|||
let cfg = get_config();
|
||||
let mut request =
|
||||
tonic::Request::new(proto::cluster_rpc::CancelQueryRequest { trace_id });
|
||||
request.set_timeout(std::time::Duration::from_secs(cfg.limit.query_timeout));
|
||||
opentelemetry::global::get_text_map_propagator(|propagator| {
|
||||
propagator.inject_context(
|
||||
&tracing::Span::current().context(),
|
||||
|
|
|
@ -0,0 +1,518 @@
|
|||
<template>
|
||||
|
||||
<div v-for="(picker, index) in dateTimePickers" :key="index" class="q-mb-md">
|
||||
<q-btn
|
||||
style="width: 180px;"
|
||||
data-test="date-time-btn"
|
||||
:label="getDisplayValue(picker)"
|
||||
icon="schedule"
|
||||
icon-right="arrow_drop_down"
|
||||
class="date-time-button"
|
||||
outline
|
||||
no-caps
|
||||
@click="picker.showMenu = !picker.showMenu"
|
||||
/>
|
||||
<q-menu
|
||||
v-if="picker.showMenu"
|
||||
class="date-time-dialog"
|
||||
anchor="bottom left"
|
||||
self="top left"
|
||||
no-route-dismiss
|
||||
@before-show="onBeforeShow"
|
||||
@before-hide="onBeforeHide"
|
||||
>
|
||||
<q-tab-panels class="tw-flex tw-justify-between" v-model="picker.activeTab">
|
||||
<q-tab-panel name="relative" class="q-pa-none">
|
||||
<div class="date-time-table relative column">
|
||||
<div
|
||||
class="relative-row q-px-md q-py-sm"
|
||||
v-for="(period, periodIndex) in relativePeriods"
|
||||
:key="'date_' + periodIndex"
|
||||
>
|
||||
<div class="relative-period-name">
|
||||
{{ period.label }}
|
||||
</div>
|
||||
<div
|
||||
v-for="(item, itemIndex) in relativeDates[period.value]"
|
||||
:key="item"
|
||||
>
|
||||
|
||||
<q-btn
|
||||
|
||||
:data-test="`date-time-relative-${item}-${period.value}-btn`"
|
||||
:label="item"
|
||||
:class="
|
||||
picker.data.selectedDate.relative.value == item &&
|
||||
picker.data.selectedDate.relative.period == period.value
|
||||
? 'rp-selector-selected'
|
||||
: `rp-selector ${picker.relativePeriod}`
|
||||
"
|
||||
outline
|
||||
dense
|
||||
flat
|
||||
@click="setRelativeDate(period, item, picker)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="relative-row q-px-md q-py-sm">
|
||||
<div class="relative-period-name">Custom</div>
|
||||
<div class="row q-gutter-sm">
|
||||
<div class="col">
|
||||
<q-input
|
||||
v-model="picker.data.selectedDate.relative.value"
|
||||
type="number"
|
||||
dense
|
||||
filled
|
||||
min="1"
|
||||
@update:model-value="onCustomPeriodSelect(picker)"
|
||||
/>
|
||||
</div>
|
||||
<div class="col">
|
||||
|
||||
<q-select
|
||||
v-model="picker.data.selectedDate.relative.period"
|
||||
:options="relativePeriodsSelect"
|
||||
dense
|
||||
filled
|
||||
emit-value
|
||||
@update:modelValue="onCustomPeriodSelect(picker)"
|
||||
style="width: 100px"
|
||||
>
|
||||
<template v-slot:selected-item>
|
||||
<div>{{ getPeriodLabel(picker) }}</div>
|
||||
</template>
|
||||
</q-select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</q-tab-panel>
|
||||
</q-tab-panels>
|
||||
</q-menu>
|
||||
<q-btn
|
||||
v-if="props.deleteIcon == 'outlinedDelete'"
|
||||
data-test="custom-date-picker-delete-btn"
|
||||
:icon="outlinedDelete"
|
||||
class=" q-mb-sm q-ml-xs q-mr-sm"
|
||||
:class="store.state?.theme === 'dark' ? 'icon-dark' : ''"
|
||||
padding="xs"
|
||||
unelevated
|
||||
size="sm"
|
||||
round
|
||||
flat
|
||||
@click="removeDateTimePicker(index)"
|
||||
style="min-width: auto;"
|
||||
/>
|
||||
|
||||
<q-icon
|
||||
v-else
|
||||
class="q-mr-xs q-ml-sm"
|
||||
size="15px"
|
||||
name="close"
|
||||
style="cursor: pointer"
|
||||
@click="removeDateTimePicker(index)"
|
||||
:data-test="`dashboard-addpanel-config-markline-remove-${index}`"
|
||||
/>
|
||||
</div>
|
||||
<q-btn
|
||||
@click="addDateTimePicker"
|
||||
:class="!props.alertsPage ? 'dashboard-add-btn' : 'alert-add-btn'"
|
||||
label="+ Add"
|
||||
no-caps
|
||||
data-test="date-time-picker-add-btn"
|
||||
/>
|
||||
</template>
|
||||
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, watch, onMounted, onBeforeMount } from 'vue';
|
||||
import { useStore } from 'vuex';
|
||||
import { outlinedDelete,outlinedInfo } from '@quasar/extras/material-icons-outlined';
|
||||
|
||||
const store = useStore();
|
||||
const dateTimePickers = ref([createPicker()]);
|
||||
const relativePeriod = ref("m");
|
||||
const relativeValue = ref(15);
|
||||
const selectedType = ref("relative");
|
||||
|
||||
const props = defineProps({
|
||||
deleteIcon: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
alertsPage: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
function createPicker() {
|
||||
return reactive({
|
||||
activeTab: 'relative',
|
||||
data: {
|
||||
selectedDate: {
|
||||
relative: {
|
||||
value: 15,
|
||||
period: "m",
|
||||
label: "Minutes"
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
let relativePeriods = [
|
||||
{ label: "Minutes", value: "m" },
|
||||
{ label: "Hours", value: "h" },
|
||||
{ label: "Days", value: "d" },
|
||||
{ label: "Weeks", value: "w" },
|
||||
{ label: "Months", value: "M" },
|
||||
];
|
||||
let relativePeriodsSelect = ref([
|
||||
{ label: "Minutes", value: "m" },
|
||||
{ label: "Hours", value: "h" },
|
||||
{ label: "Days", value: "d" },
|
||||
{ label: "Weeks", value: "w" },
|
||||
{ label: "Months", value: "M" },
|
||||
]);
|
||||
|
||||
const relativeDates = {
|
||||
m: [1, 5, 10, 15, 30, 45],
|
||||
h: [1, 2, 3, 6, 8, 12],
|
||||
d: [1, 2, 3, 4, 5, 6],
|
||||
w: [1, 2, 3, 4, 5, 6],
|
||||
M: [1, 2, 3, 4, 5, 6],
|
||||
};
|
||||
|
||||
const relativeDatesInHour = {
|
||||
m: [1, 1, 1, 1, 1, 1],
|
||||
h: [1, 2, 3, 6, 8, 12],
|
||||
d: [24, 48, 72, 96, 120, 144],
|
||||
w: [168, 336, 504, 672, 840, 1008],
|
||||
M: [744, 1488, 2232, 2976, 3720, 4464],
|
||||
};
|
||||
|
||||
let relativePeriodsMaxValue = ref({
|
||||
m: 0,
|
||||
h: 0,
|
||||
d: 0,
|
||||
w: 0,
|
||||
M: 0,
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:dateTime']);
|
||||
|
||||
const setRelativeDate = (period, item,picker) => {
|
||||
const {label,value} = period;
|
||||
picker.data.selectedDate.relative.period = value;
|
||||
picker.data.selectedDate.relative.value = item;
|
||||
picker.data.selectedDate.relative.label = label
|
||||
};
|
||||
const onCustomPeriodSelect = (picker) => {
|
||||
const {value,period} = picker.data.selectedDate.relative;
|
||||
if(value == 0) {
|
||||
|
||||
}
|
||||
// const { value, period } = picker.data.selectedDate.relative;
|
||||
// picker.data.selectedDate.relative.label = period;
|
||||
};
|
||||
|
||||
const dateTimeArray = computed(() => {
|
||||
return dateTimePickers.value.map(picker => {
|
||||
const { value, period } = picker.data.selectedDate.relative;
|
||||
return { offSet: value && period ? `${value}${period}` : null };
|
||||
});
|
||||
});
|
||||
|
||||
const onBeforeShow = () => {
|
||||
// if (props.modelValue) selectedDate.value = cloneDeep(props.modelValue);
|
||||
};
|
||||
|
||||
const onBeforeHide = () => {
|
||||
if (selectedType.value === "absolute")
|
||||
resetTime(selectedTime.value.startTime, selectedTime.value.endTime);
|
||||
};
|
||||
const getDisplayValue = (picker) => {
|
||||
return `${picker.data.selectedDate.relative.value} ${picker.data.selectedDate.relative.label} ago`;
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
function removeDateTimePicker(index) {
|
||||
dateTimePickers.value.splice(index, 1);
|
||||
emit('update:dateTime', dateTimeArray.value);
|
||||
}
|
||||
const getPeriodLabel = (picker) => {
|
||||
const periodMapping = {
|
||||
m: "Minutes",
|
||||
h: "Hours",
|
||||
d: "Days",
|
||||
w: "Weeks",
|
||||
M: "Months",
|
||||
};
|
||||
picker.data.selectedDate.relative.label = periodMapping[picker.data.selectedDate.relative.period];
|
||||
return periodMapping[picker.data.selectedDate.relative.period];
|
||||
};
|
||||
|
||||
function addDateTimePicker() {
|
||||
dateTimePickers.value.push(createPicker());
|
||||
emit('update:dateTime', dateTimeArray.value);
|
||||
}
|
||||
|
||||
watch(dateTimeArray, (newVal) => {
|
||||
emit('update:dateTime', newVal);
|
||||
}, { deep: true });
|
||||
|
||||
onBeforeMount(()=>{
|
||||
emit('update:dateTime',dateTimeArray.value)
|
||||
|
||||
})
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<style scoped>
|
||||
.relative-row {
|
||||
/* Add your styles here */
|
||||
}
|
||||
|
||||
.date-time-table {
|
||||
/* Add your styles here */
|
||||
}
|
||||
.alerts-condition-action {
|
||||
.q-btn {
|
||||
&.icon-dark {
|
||||
filter: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
.alert-page-font {
|
||||
background-color: red;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
<style lang="scss" scoped>
|
||||
.q-btn--rectangle {
|
||||
border-radius: 3px;
|
||||
}
|
||||
.date-time-button {
|
||||
height: 100%;
|
||||
border-radius: 3px;
|
||||
padding: 0px 5px;
|
||||
font-size: 12px;
|
||||
min-width: auto;
|
||||
background: rgba(89, 96, 178, 0.2) !important;
|
||||
|
||||
.q-icon.on-right {
|
||||
transition: transform 0.25s ease;
|
||||
}
|
||||
&.isOpen .q-icon.on-right {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.q-btn__content {
|
||||
justify-content: flex-start;
|
||||
|
||||
.block {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.date-time-dialog {
|
||||
width: 341px;
|
||||
z-index: 10001;
|
||||
max-height: 600px;
|
||||
|
||||
.tab-button {
|
||||
&.q-btn {
|
||||
padding-bottom: 0.1rem;
|
||||
padding-top: 0.1rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
|
||||
&.text-primary {
|
||||
.q-btn__content {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.date-time-table.relative {
|
||||
display: flex;
|
||||
|
||||
.relative-row {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid $border-color;
|
||||
|
||||
.block {
|
||||
font-weight: 700;
|
||||
}
|
||||
.q-field {
|
||||
&__control {
|
||||
height: 40px;
|
||||
}
|
||||
&__native {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.q-select__dropdown-icon {
|
||||
}
|
||||
}
|
||||
|
||||
> * {
|
||||
margin-right: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.absolute-calendar {
|
||||
box-shadow: none;
|
||||
.q-date__header {
|
||||
display: none;
|
||||
}
|
||||
.q-date__view {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.relative-period-name {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
min-width: 75px;
|
||||
}
|
||||
|
||||
.rp-selector,
|
||||
.rp-selector-selected {
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
// border: $secondary;
|
||||
background: rgba(0, 0, 0, 0.07);
|
||||
}
|
||||
|
||||
.rp-selector-selected {
|
||||
color: #ffffff;
|
||||
background: $primary;
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
width: 154px;
|
||||
}
|
||||
|
||||
.notePara {
|
||||
padding-right: 1.5rem;
|
||||
padding-left: 1.5rem;
|
||||
font-size: 0.625rem;
|
||||
}
|
||||
.q-date {
|
||||
&__navigation {
|
||||
justify-content: center;
|
||||
padding: 0 0.5rem;
|
||||
|
||||
.q-date__arrow {
|
||||
& + .q-date__arrow {
|
||||
margin-left: auto;
|
||||
}
|
||||
& + .col {
|
||||
flex: initial;
|
||||
}
|
||||
}
|
||||
|
||||
.q-btn .block {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
&__calendar {
|
||||
&-item .block {
|
||||
font-weight: 700;
|
||||
}
|
||||
&-weekdays > div {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 700;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
&__range {
|
||||
&,
|
||||
&-from,
|
||||
&-to {
|
||||
.block {
|
||||
color: white;
|
||||
}
|
||||
&:before {
|
||||
bottom: 3px;
|
||||
top: 3px;
|
||||
}
|
||||
}
|
||||
.block {
|
||||
color: $dark-page;
|
||||
}
|
||||
}
|
||||
}
|
||||
.startEndTime {
|
||||
.q-field {
|
||||
padding-bottom: 0.125rem;
|
||||
}
|
||||
.label {
|
||||
font-size: 0.75rem;
|
||||
// color: $dark-page;
|
||||
font-weight: 600;
|
||||
}
|
||||
.timeInput {
|
||||
.q-field__control {
|
||||
padding-right: 0.375rem;
|
||||
}
|
||||
|
||||
.q-btn-group {
|
||||
& > .q-btn-item {
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.q-btn {
|
||||
padding: 0 0.3125rem;
|
||||
|
||||
.block {
|
||||
font-size: 0.625rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.drawer-footer {
|
||||
.q-btn {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
|
||||
&.clearBtn {
|
||||
margin-right: 1rem;
|
||||
color: $dark-page;
|
||||
}
|
||||
}
|
||||
}
|
||||
.timezone-select {
|
||||
.q-item:nth-child(2) {
|
||||
border-bottom: 1px solid #dcdcdc;
|
||||
}
|
||||
}
|
||||
.dashboard-add-btn{
|
||||
cursor: pointer;
|
||||
padding: 0px 5px;
|
||||
}
|
||||
.alert-add-btn{
|
||||
border-radius: 4px;
|
||||
text-transform: capitalize;
|
||||
background: #f2f2f2 !important;
|
||||
color: #000 !important;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -191,6 +191,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
:sqlQueryErrorMsg="sqlQueryErrorMsg"
|
||||
:vrlFunctionError="vrlFunctionError"
|
||||
:showTimezoneWarning="showTimezoneWarning"
|
||||
v-model:multipleTimeRangeData = "dateTimeData"
|
||||
v-model:trigger="formData.trigger_condition"
|
||||
v-model:sql="formData.query_condition.sql"
|
||||
v-model:promql="formData.query_condition.promql"
|
||||
|
@ -198,8 +199,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
v-model:aggregation="formData.query_condition.aggregation"
|
||||
v-model:promql_condition="
|
||||
formData.query_condition.promql_condition
|
||||
"
|
||||
v-model:vrl_function="formData.query_condition.vrl_function"
|
||||
" v-model:vrl_function="formData.query_condition.vrl_function"
|
||||
v-model:isAggregationEnabled="isAggregationEnabled"
|
||||
v-model:showVrlFunction="showVrlFunction"
|
||||
@field:add="addField"
|
||||
|
@ -207,6 +207,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
@input:update="onInputUpdate"
|
||||
@validate-sql="validateSqlQuery"
|
||||
@update:showVrlFunction="updateFunctionVisibility"
|
||||
@update:multipleTimeRangeData="handleMultiTimeRangeData"
|
||||
class="q-mt-sm"
|
||||
/>
|
||||
</div>
|
||||
|
@ -283,6 +284,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
data-test="add-alert-delay-error"
|
||||
v-if="formData.trigger_condition.silence < 0"
|
||||
|
@ -294,6 +296,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="o2-input flex justify-start items-center">
|
||||
<div
|
||||
data-test="add-alert-destination-title"
|
||||
|
@ -468,6 +471,7 @@ import useFunctions from "@/composables/useFunctions";
|
|||
import useQuery from "@/composables/useQuery";
|
||||
import searchService from "@/services/search";
|
||||
import { convertDateToTimestamp } from "@/utils/date";
|
||||
import CustomDateTimePicker from "@/components/CustomDateTimePicker.vue"
|
||||
|
||||
const defaultValue: any = () => {
|
||||
return {
|
||||
|
@ -509,6 +513,7 @@ const defaultValue: any = () => {
|
|||
silence: 10,
|
||||
frequency_type: "minutes",
|
||||
timezone: "UTC",
|
||||
multiTimeRange:[],
|
||||
},
|
||||
destinations: [],
|
||||
context_attributes: {},
|
||||
|
@ -544,6 +549,7 @@ export default defineComponent({
|
|||
RealTimeAlert: defineAsyncComponent(() => import("./RealTimeAlert.vue")),
|
||||
VariablesInput: defineAsyncComponent(() => import("./VariablesInput.vue")),
|
||||
PreviewAlert: defineAsyncComponent(() => import("./PreviewAlert.vue")),
|
||||
CustomDateTimePicker,
|
||||
},
|
||||
setup(props) {
|
||||
const store: any = useStore();
|
||||
|
@ -597,6 +603,7 @@ export default defineComponent({
|
|||
const validateSqlQueryPromise = ref<Promise<unknown>>();
|
||||
|
||||
const addAlertFormRef = ref(null);
|
||||
const dateTimeData = ref(null);
|
||||
|
||||
const router = useRouter();
|
||||
const scheduledAlertRef: any = ref(null);
|
||||
|
@ -633,6 +640,9 @@ export default defineComponent({
|
|||
const showPreview = computed(() => {
|
||||
return formData.value.stream_type && formData.value.stream_name;
|
||||
});
|
||||
function handleMultiTimeRangeData(updatedData : any) {
|
||||
formData.value.trigger_condition.multiTimeRange = updatedData
|
||||
};
|
||||
|
||||
const updateConditions = (e: any) => {
|
||||
try {
|
||||
|
@ -1184,6 +1194,16 @@ export default defineComponent({
|
|||
}
|
||||
};
|
||||
|
||||
|
||||
const validateMultiTimeRange = (multiTimeRange: any) => {
|
||||
for (const range of multiTimeRange) {
|
||||
if (range.offSet.startsWith('0')) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
return {
|
||||
t,
|
||||
q,
|
||||
|
@ -1244,9 +1264,12 @@ export default defineComponent({
|
|||
sqlQueryErrorMsg,
|
||||
vrlFunctionError,
|
||||
updateFunctionVisibility,
|
||||
validateMultiTimeRange,
|
||||
convertDateToTimestamp,
|
||||
getTimezonesByOffset,
|
||||
showTimezoneWarning,
|
||||
handleMultiTimeRangeData,
|
||||
dateTimeData,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -1341,6 +1364,17 @@ export default defineComponent({
|
|||
});
|
||||
return false;
|
||||
}
|
||||
const isValid = this.validateMultiTimeRange(this.formData.trigger_condition.multiTimeRange);
|
||||
if (!isValid) {
|
||||
|
||||
this.q.notify({
|
||||
type: "negative",
|
||||
message: "Selecting 0 in multi window selection is not allowed.",
|
||||
timeout: 1500,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
if (this.formData.stream_name == "") {
|
||||
this.q.notify({
|
||||
|
|
|
@ -613,6 +613,38 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center q-mr-sm">
|
||||
<div
|
||||
data-test="scheduled-alert-period-title"
|
||||
class="text-bold q-py-md flex items-center"
|
||||
style="width: 190px"
|
||||
>
|
||||
Multi Window Selection
|
||||
<q-btn
|
||||
no-caps
|
||||
padding="xs"
|
||||
class=""
|
||||
size="sm"
|
||||
flat
|
||||
icon="info_outline"
|
||||
data-test="dashboard-addpanel-config-drilldown-info">
|
||||
<q-tooltip
|
||||
class="tool-tip-multi-window-selection"
|
||||
anchor="bottom middle" self="top middle"
|
||||
style="font-size: 14px;"
|
||||
|
||||
max-width="300px" >
|
||||
<span>Additional timeframe for query execution: <br />
|
||||
For example, selecting "past 10 hours" means that each time the query runs, it will retrieve data from 10 hours prior, using the last 10 minutes of that period. <br /> If the query is scheduled from 4:00 PM to 4:10 PM, additionally it will pull data from 6:00 AM to 6:10 AM.
|
||||
</span>
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
<CustomDateTimePicker :deleteIcon="'outlinedDelete'" :alertsPage=true @update:dateTime="handleDateTimeUpdate" v-if="tab == 'sql'"/>
|
||||
|
||||
|
||||
|
||||
<div class="flex items-center q-mr-sm">
|
||||
<div
|
||||
data-test="scheduled-alert-cron-toggle-title"
|
||||
|
@ -833,6 +865,7 @@ import {
|
|||
import { useStore } from "vuex";
|
||||
import { getImageURL, useLocalTimezone } from "@/utils/zincutils";
|
||||
import { useQuasar } from "quasar";
|
||||
import CustomDateTimePicker from "@/components/CustomDateTimePicker.vue"
|
||||
|
||||
const QueryEditor = defineAsyncComponent(
|
||||
() => import("@/components/QueryEditor.vue"),
|
||||
|
@ -857,6 +890,7 @@ const props = defineProps([
|
|||
"disableQueryTypeSelection",
|
||||
"vrlFunctionError",
|
||||
"showTimezoneWarning",
|
||||
"multipleTimeRangeData"
|
||||
]);
|
||||
|
||||
const emits = defineEmits([
|
||||
|
@ -873,6 +907,7 @@ const emits = defineEmits([
|
|||
"update:vrl_function",
|
||||
"update:showVrlFunction",
|
||||
"validate-sql",
|
||||
"update:multipleTimeRangeData"
|
||||
]);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
@ -936,6 +971,9 @@ const filteredNumericColumns = ref(getNumericColumns.value);
|
|||
const addField = () => {
|
||||
emits("field:add");
|
||||
};
|
||||
const handleDateTimeUpdate = (data: any) =>{
|
||||
emits("update:multipleTimeRangeData",data)
|
||||
}
|
||||
|
||||
var triggerOperators: any = ref(["=", "!=", ">=", "<=", ">", "<"]);
|
||||
|
||||
|
@ -1186,6 +1224,7 @@ const validateInputs = (notify: boolean = true) => {
|
|||
return true;
|
||||
}
|
||||
|
||||
|
||||
if (
|
||||
!props.disableThreshold &&
|
||||
(isNaN(triggerData.value.threshold) ||
|
||||
|
@ -1200,6 +1239,7 @@ const validateInputs = (notify: boolean = true) => {
|
|||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
return true;
|
||||
};
|
||||
|
|
|
@ -932,7 +932,11 @@ import MarkLineConfig from "./MarkLineConfig.vue";
|
|||
import CommonAutoComplete from "@/components/dashboards/addPanel/CommonAutoComplete.vue";
|
||||
|
||||
export default defineComponent({
|
||||
components: { Drilldown, CommonAutoComplete, MarkLineConfig },
|
||||
components: {
|
||||
Drilldown,
|
||||
CommonAutoComplete,
|
||||
MarkLineConfig,
|
||||
},
|
||||
props: ["dashboardPanelData", "variablesData"],
|
||||
setup(props) {
|
||||
const dashboardPanelDataPageKey = inject(
|
||||
|
|
|
@ -58,7 +58,8 @@
|
|||
"logs": "Logs",
|
||||
"addGroup":"Add Group",
|
||||
"addCondition":"Add Condition",
|
||||
"required": "Value is required"
|
||||
"required": "Value is required",
|
||||
"valueMustBeGreaterThanZero": "Value must be greater than zero"
|
||||
},
|
||||
"search": {
|
||||
"selectIndex": "Select Stream First",
|
||||
|
|
|
@ -36,74 +36,76 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
</div>
|
||||
</div>
|
||||
<q-separator></q-separator>
|
||||
<div class="q-mx-md">
|
||||
<div
|
||||
data-test="panel-layout-settings-height"
|
||||
class="o2-input tw-relative"
|
||||
style="padding-top: 12px"
|
||||
>
|
||||
<q-input
|
||||
v-model.number="updatedLayout.h"
|
||||
:label="t('dashboard.panelHeight') + ' *'"
|
||||
color="input-border"
|
||||
bg-color="input-bg"
|
||||
class="showLabelOnTop"
|
||||
stack-label
|
||||
outlined
|
||||
filled
|
||||
dense
|
||||
type="number"
|
||||
:rules="[
|
||||
(val: any) => {
|
||||
if (val === null || val === undefined || val === '') {
|
||||
return t('common.required'); // If value is empty or null
|
||||
}
|
||||
return val > 0 ? true : 'Value must be greater than 0'; // Ensure value is greater than 0
|
||||
},
|
||||
]"
|
||||
style="min-width: 220px"
|
||||
/>
|
||||
|
||||
<div class="tw-text-[12px]">
|
||||
Approximately
|
||||
<span class="tw-font-bold">{{ getRowCount }}</span> table rows will be
|
||||
displayed
|
||||
</div>
|
||||
|
||||
<q-icon
|
||||
name="info_outline"
|
||||
class="cursor-pointer q-ml-sm tw-absolute tw-top-[14px] tw-left-[94px]"
|
||||
size="16px"
|
||||
<q-form @submit="savePanelLayout">
|
||||
<div class="q-mx-md">
|
||||
<div
|
||||
data-test="panel-layout-settings-height"
|
||||
class="o2-input tw-relative"
|
||||
style="padding-top: 12px"
|
||||
>
|
||||
<q-tooltip
|
||||
anchor="center end"
|
||||
self="center left"
|
||||
class="tw-text-[12px]"
|
||||
<q-input
|
||||
v-model.number="updatedLayout.h"
|
||||
:label="t('dashboard.panelHeight') + ' *'"
|
||||
color="input-border"
|
||||
bg-color="input-bg"
|
||||
class="showLabelOnTop"
|
||||
stack-label
|
||||
outlined
|
||||
filled
|
||||
dense
|
||||
type="number"
|
||||
:rules="[
|
||||
(val: any) => {
|
||||
if (val === null || val === undefined || val === '') {
|
||||
return t('common.required'); // If value is empty or null
|
||||
}
|
||||
return val > 0 ? true : t('common.valueMustBeGreaterThanZero'); // Ensure value is greater than 0
|
||||
},
|
||||
]"
|
||||
style="min-width: 220px"
|
||||
/>
|
||||
|
||||
<div class="tw-text-[12px]">
|
||||
Approximately
|
||||
<span class="tw-font-bold">{{ getRowCount }}</span> table rows will
|
||||
be displayed
|
||||
</div>
|
||||
|
||||
<q-icon
|
||||
name="info_outline"
|
||||
class="cursor-pointer q-ml-sm tw-absolute tw-top-[14px] tw-left-[94px]"
|
||||
size="16px"
|
||||
>
|
||||
1 unit = 30px
|
||||
</q-tooltip>
|
||||
</q-icon>
|
||||
<q-tooltip
|
||||
anchor="center end"
|
||||
self="center left"
|
||||
class="tw-text-[12px]"
|
||||
>
|
||||
1 unit = 30px
|
||||
</q-tooltip>
|
||||
</q-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-center q-mt-lg">
|
||||
<q-btn
|
||||
ref="closeBtn"
|
||||
v-close-popup="true"
|
||||
class="q-mb-md text-bold"
|
||||
:label="t('dashboard.cancel')"
|
||||
text-color="light-text"
|
||||
padding="sm md"
|
||||
no-caps
|
||||
/>
|
||||
<q-btn
|
||||
:label="t('dashboard.save')"
|
||||
class="q-mb-md text-bold no-border q-ml-md"
|
||||
color="secondary"
|
||||
padding="sm xl"
|
||||
@click="savePanelLayout"
|
||||
no-caps
|
||||
/>
|
||||
</div>
|
||||
<div class="flex justify-center q-mt-lg">
|
||||
<q-btn
|
||||
ref="closeBtn"
|
||||
v-close-popup="true"
|
||||
class="q-mb-md text-bold"
|
||||
:label="t('dashboard.cancel')"
|
||||
text-color="light-text"
|
||||
padding="sm md"
|
||||
no-caps
|
||||
/>
|
||||
<q-btn
|
||||
:label="t('dashboard.save')"
|
||||
class="q-mb-md text-bold no-border q-ml-md"
|
||||
color="secondary"
|
||||
padding="sm xl"
|
||||
type="submit"
|
||||
no-caps
|
||||
/>
|
||||
</div>
|
||||
</q-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -137,12 +139,8 @@ export default defineComponent({
|
|||
|
||||
const getRowCount = computed(() => {
|
||||
// 24 is the height of toolbar
|
||||
// 28 is the height of table header
|
||||
// 28.5 is the height of each row
|
||||
// 33 is the height of pagination
|
||||
const count = Number(
|
||||
Math.ceil((updatedLayout.value.h * 30 - (28 + 24 + 33)) / 28.5),
|
||||
);
|
||||
const count = Number(Math.ceil((updatedLayout.value.h * 30 - 24) / 28.5));
|
||||
|
||||
if (count < 0) return 0;
|
||||
|
||||
|
|
Loading…
Reference in New Issue