Compare commits

...

13 Commits

Author SHA1 Message Date
ktx-vaidehi 7598d460ea refactor: remove dashboard changes from here 2024-09-25 18:39:19 +05:30
Sai Nikhil 56ecc36c24 build fixed 2024-09-25 15:42:32 +05:30
ktx-vaidehi e09367ceee add: in dashboard use common component and pass data to config 2024-09-25 15:42:32 +05:30
ktx-vaidehi 21e30a6563 add: key for config 2024-09-25 15:42:32 +05:30
ktx-vaidehi 9f86b6d799 feat: backend changes to store time_shift from config 2024-09-25 15:42:32 +05:30
Sai Nikhil 517d136bc6 fix: more generic 2024-09-25 15:42:32 +05:30
Sai Nikhil 7acbbda171 fix: made component generic for both alerts and dashboard 2024-09-25 15:42:32 +05:30
Sai Nikhil 8c4441e801 fix: added multi time range shifts 2024-09-25 15:42:32 +05:30
Omkar Kesarkhane 7fcd40f631
fix: fixed input validation of panel layout input (#4641)
#4630 

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Introduced a form submission mechanism in the Panel Layout Settings,
enhancing functionality for saving panel layouts.
	- Added form validation capabilities with the new `<q-form>` component.
- Enhanced validation messages with a new entry for "Value must be
greater than zero."

- **Bug Fixes**
- Updated the save button to function correctly as a form submission
button.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-09-25 15:29:43 +05:30
Ashish Kolhe e20c509bf4
feat: multi search with multiple time ranges (#4626)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Introduced a new `SqlQuery` structure for improved SQL query handling.
- Added a `per_query_response` feature to enhance query response
flexibility.

- **Bug Fixes**
	- Improved error handling and response clarity in search functions.
	- Enhanced request formatting for better readability and consistency.

- **Refactor**
- Restructured request and response handling in `search_multi` and
`_search_partition_multi` functions for improved code clarity.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-09-25 15:02:30 +05:30
Ashish Kolhe 4fffa6d8a0
fix: add request timeout for tonic requests (#4633)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Introduced timeout settings for gRPC requests across multiple
functions, enhancing request handling and response management.
  
- **Bug Fixes**
- Improved the stability of gRPC requests by ensuring they respect the
configured timeout values, potentially reducing failed requests due to
timeouts.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-09-25 13:57:16 +05:30
Subhra264 34f4a3c7cc
fix: use max of silence and frequency for alert next run and support all threshold symbols (#4627)
Addresses #4623 

- [x] When the alert conditions are satisfied, use the max of frequency
and silence period to calculate the next trigger time.
- [x] Support all the alert threshold symbols.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Enhanced alert scheduling logic to improve the timing of alert
triggers based on silence periods and alert frequency.
- Introduced refined conditional checks for processing query results,
improving clarity and maintainability.

- **Bug Fixes**
- Resolved issues with alert timing to ensure alerts run as expected
even during silence periods.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-09-25 11:18:07 +05:30
Omkar Kesarkhane 58dc29408e
fix: Updated dashboard panel height row count logic (#4639)
#4630 

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **Bug Fixes**
- Improved the calculation of the row count in the dashboard panel
layout settings for better accuracy.
  
- **Refactor**
- Simplified the logic for determining the row count by adjusting the
height calculations.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-09-25 10:59:54 +05:30
17 changed files with 886 additions and 131 deletions

View File

@ -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(),

View File

@ -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()

View File

@ -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")

View File

@ -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))
}
}
}

View File

@ -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();

View File

@ -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) => {

View File

@ -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(

View File

@ -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(

View File

@ -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

View File

@ -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: {}",

View File

@ -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(),

View File

@ -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>

View File

@ -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({

View File

@ -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;
};

View File

@ -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(

View File

@ -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",

View File

@ -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;