feat: added destination type for remote destination (#9080)
Signed-off-by: Yashodhan Joshi <yjdoc2@gmail.com> Co-authored-by: Yashodhan Joshi <yjdoc2@gmail.com> Co-authored-by: mmosarafO2 <mosraf@openobserve.ai> Co-authored-by: Yashodhan Joshi <yashodhan@openobserve.ai>
@@ -17,6 +17,7 @@ use std::fmt;
|
||||
|
||||
use hashbrown::HashMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
|
||||
@@ -100,6 +101,11 @@ pub struct Endpoint {
|
||||
pub action_id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub output_format: Option<HTTPOutputFormat>,
|
||||
/// Destination type (e.g., "openobserve", "splunk", "elasticsearch", "custom")
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub destination_type: Option<String>,
|
||||
#[serde(default)]
|
||||
pub metadata: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
|
||||
@@ -124,6 +130,68 @@ pub enum HTTPOutputFormat {
|
||||
#[default]
|
||||
JSON,
|
||||
NDJSON,
|
||||
NestedEvent,
|
||||
ESBulk {
|
||||
index: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl HTTPOutputFormat {
|
||||
pub fn get_content_type(&self) -> &'static str {
|
||||
match self {
|
||||
Self::JSON => "application/json",
|
||||
Self::NDJSON => "application/x-ndjson",
|
||||
Self::NestedEvent => "application/x-ndjson",
|
||||
Self::ESBulk { .. } => "application/x-ndjson",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_body_from_data<T: AsRef<Value> + Serialize>(
|
||||
&self,
|
||||
data: &[T],
|
||||
metadata: &HashMap<String, String>,
|
||||
) -> Vec<u8> {
|
||||
let data = data
|
||||
.iter()
|
||||
.map(|val| {
|
||||
let mut t = val.as_ref().clone();
|
||||
if let Some(obj) = t.as_object_mut() {
|
||||
for (k, v) in metadata.iter() {
|
||||
obj.insert(k.clone(), Value::String(v.clone()));
|
||||
}
|
||||
}
|
||||
t
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
match self {
|
||||
Self::JSON => serde_json::to_vec(&data).unwrap(),
|
||||
Self::NDJSON => data
|
||||
.iter()
|
||||
.map(|x| x.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
.into_bytes(),
|
||||
Self::NestedEvent => data
|
||||
.iter()
|
||||
.map(|v| serde_json::json!({"event":v}).to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
.into_bytes(),
|
||||
Self::ESBulk { index } => {
|
||||
let expected_count = data.len();
|
||||
let mut ret = Vec::with_capacity(expected_count * 2);
|
||||
data.iter().for_each(|v| {
|
||||
ret.push(serde_json::json!({ "index": { "_index": index } }).to_string());
|
||||
ret.push(v.to_string());
|
||||
});
|
||||
let mut temp = ret.join("\n").into_bytes();
|
||||
// for ES Bulk format, the payload must end with a newline
|
||||
temp.push(b'\n');
|
||||
temp
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for HTTPType {
|
||||
@@ -249,6 +317,8 @@ mod tests {
|
||||
headers: Some(headers.clone()),
|
||||
action_id: Some("action_123".to_string()),
|
||||
output_format: Some(HTTPOutputFormat::JSON),
|
||||
destination_type: Some("custom".to_string()),
|
||||
metadata: HashMap::new(),
|
||||
};
|
||||
|
||||
assert_eq!(endpoint.url, "https://api.example.com");
|
||||
@@ -257,6 +327,7 @@ mod tests {
|
||||
assert_eq!(endpoint.headers, Some(headers));
|
||||
assert_eq!(endpoint.action_id, Some("action_123".to_string()));
|
||||
assert_eq!(endpoint.output_format, Some(HTTPOutputFormat::JSON));
|
||||
assert_eq!(endpoint.destination_type, Some("custom".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -325,6 +396,8 @@ mod tests {
|
||||
headers: None,
|
||||
action_id: None,
|
||||
output_format: None,
|
||||
destination_type: Some("openobserve".to_string()),
|
||||
metadata: HashMap::new(),
|
||||
};
|
||||
|
||||
let dest_type = DestinationType::Http(endpoint.clone());
|
||||
@@ -383,6 +456,8 @@ mod tests {
|
||||
headers: None,
|
||||
action_id: None,
|
||||
output_format: None,
|
||||
destination_type: Some("splunk".to_string()),
|
||||
metadata: HashMap::new(),
|
||||
};
|
||||
|
||||
let module = Module::Pipeline {
|
||||
@@ -395,6 +470,10 @@ mod tests {
|
||||
} => {
|
||||
assert_eq!(pipeline_endpoint.url, "https://pipeline.example.com");
|
||||
assert_eq!(pipeline_endpoint.method, HTTPType::PUT);
|
||||
assert_eq!(
|
||||
pipeline_endpoint.destination_type,
|
||||
Some("splunk".to_string())
|
||||
);
|
||||
}
|
||||
_ => panic!("Should be Pipeline module"),
|
||||
}
|
||||
|
||||
@@ -59,6 +59,8 @@ impl From<meta_dest::Destination> for Destination {
|
||||
#[cfg(feature = "enterprise")]
|
||||
action_id: endpoint.action_id,
|
||||
output_format: endpoint.output_format,
|
||||
destination_type_name: endpoint.destination_type,
|
||||
metadata: endpoint.metadata,
|
||||
..Default::default()
|
||||
},
|
||||
meta_dest::DestinationType::Sns(aws_sns) => Self {
|
||||
@@ -78,6 +80,8 @@ impl From<meta_dest::Destination> for Destination {
|
||||
headers: endpoint.headers,
|
||||
destination_type: DestinationType::Http,
|
||||
output_format: endpoint.output_format,
|
||||
destination_type_name: endpoint.destination_type,
|
||||
metadata: endpoint.metadata,
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
@@ -100,6 +104,8 @@ impl Destination {
|
||||
headers: self.headers,
|
||||
action_id: None,
|
||||
output_format: self.output_format,
|
||||
destination_type: self.destination_type_name,
|
||||
metadata: self.metadata,
|
||||
})
|
||||
}
|
||||
DestinationType::Sns => meta_dest::DestinationType::Sns(meta_dest::AwsSns {
|
||||
@@ -122,6 +128,8 @@ impl Destination {
|
||||
headers: None,
|
||||
action_id: Some(action_id),
|
||||
output_format: self.output_format,
|
||||
destination_type: self.destination_type_name,
|
||||
metadata: self.metadata,
|
||||
})
|
||||
} else {
|
||||
return Err(DestinationError::InvalidActionId(anyhow::anyhow!(
|
||||
@@ -148,6 +156,8 @@ impl Destination {
|
||||
headers: self.headers,
|
||||
action_id: None,
|
||||
output_format: self.output_format,
|
||||
destination_type: self.destination_type_name,
|
||||
metadata: self.metadata,
|
||||
};
|
||||
Ok(meta_dest::Destination {
|
||||
id: None,
|
||||
@@ -231,6 +241,11 @@ pub struct Destination {
|
||||
pub action_id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub output_format: Option<meta_dest::HTTPOutputFormat>,
|
||||
/// Specific destination type identifier (e.g., "openobserve", "splunk", "elasticsearch")
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub destination_type_name: Option<String>,
|
||||
#[serde(default)]
|
||||
pub metadata: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Default, PartialEq, Eq, Deserialize, Clone, ToSchema)]
|
||||
|
||||
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 8.7 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
@@ -0,0 +1,7 @@
|
||||
<svg width="166" height="166" viewBox="0 0 166 166" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M131.44 35.1002C126.69 42.7202 116.74 45.1102 109.06 40.5502C109.02 40.5302 108.98 40.5002 108.94 40.4802C108.81 40.4002 108.68 40.3202 108.55 40.2402C85.3698 26.0602 55.05 33.1502 40.57 56.1802C45.1 48.5102 42.71 38.5802 35.1 33.8402C27.41 29.0402 17.3 31.3602 12.46 39.0102C12.48 38.9702 12.51 38.9402 12.53 38.9002C36.64 0.260193 87.4398 -11.5698 126.13 12.3902C126.15 12.3902 126.16 12.4102 126.18 12.4202C126.18 12.4202 126.19 12.4202 126.2 12.4302C133.9 17.2502 136.25 27.4002 131.44 35.1102V35.1002Z" fill="#6B76E3"/>
|
||||
<path d="M33.8395 130.17C29.0395 137.86 31.3595 147.97 39.0095 152.81C38.9695 152.79 38.9395 152.76 38.8995 152.74C0.419548 128.73 -11.4805 78.2204 12.1095 39.6004C12.2095 39.4304 12.3095 39.2604 12.4195 39.0904C12.4395 39.0604 12.4495 39.0304 12.4695 39.0004C17.3095 31.3504 27.4195 29.0304 35.1095 33.8304C42.7095 38.5704 45.1095 48.5004 40.5795 56.1704C40.5795 56.1804 40.5795 56.1904 40.5595 56.2004C40.5395 56.2404 40.5095 56.2804 40.4895 56.3204C40.4095 56.4504 40.3295 56.5804 40.2495 56.7104C40.2495 56.7304 40.2295 56.7404 40.2195 56.7604C26.0895 79.9504 33.1895 110.24 56.2295 124.7C48.5595 120.14 38.6095 122.54 33.8595 130.15L33.8395 130.17Z" fill="#F45B49"/>
|
||||
<path d="M152.859 126.201C152.819 126.261 152.789 126.321 152.749 126.381C128.739 164.861 78.2288 176.761 39.6092 153.171C39.4392 153.071 39.2692 152.971 39.0992 152.861C39.0692 152.841 39.0392 152.831 39.0092 152.811C31.3592 147.971 29.0392 137.861 33.8392 130.171C38.5892 122.561 48.5392 120.161 56.2092 124.721C56.2492 124.741 56.2892 124.771 56.3292 124.791C56.4592 124.871 56.5892 124.951 56.7192 125.031C56.7192 125.031 56.7392 125.031 56.7392 125.051C79.9288 139.211 110.249 132.111 124.719 109.061C120.159 116.731 122.549 126.691 130.159 131.441C137.869 136.251 148.029 133.911 152.849 126.201H152.859Z" fill="#5ACA7A"/>
|
||||
<path d="M152.89 126.13C152.89 126.13 152.87 126.16 152.86 126.18C152.86 126.18 152.86 126.19 152.85 126.2C148.03 133.9 137.88 136.25 130.16 131.44C122.55 126.69 120.16 116.74 124.72 109.06C124.74 109.02 124.77 108.98 124.79 108.94C124.87 108.81 124.95 108.68 125.03 108.55C139.21 85.3504 132.12 55.0204 109.05 40.5504C116.72 45.1104 126.68 42.7204 131.43 35.1004C136.24 27.3904 133.9 17.2404 126.19 12.4204C126.25 12.4604 126.31 12.4904 126.37 12.5304C165.01 36.6404 176.84 87.4404 152.88 126.13H152.89Z" fill="#45A4F3"/>
|
||||
<path d="M89.0197 119.48L74.4497 72.8804L65.5999 106.04L56.4899 87.2004C56.3399 86.8804 56.0099 86.6804 55.6599 86.6804H38.0399C36.2499 86.6804 34.7599 85.2704 34.7199 83.4804C34.7199 83.4404 34.7199 83.3904 34.7199 83.3504C34.6999 81.5304 36.2199 80.0704 38.0399 80.0704H55.6599C58.5499 80.0704 61.1799 81.7204 62.4399 84.3204L63.8099 87.1504L73.9497 49.1401L89.0097 97.3004L100.3 61.0301L107.79 78.5804C107.98 79.0304 108.43 79.3304 108.92 79.3304H127.74C129.53 79.3304 131.03 80.7604 131.02 82.5504C131.02 82.6104 131.02 82.6704 131.02 82.7304C131 84.5104 129.52 85.9404 127.73 85.9404H107.65C105.27 85.9404 103.13 84.5204 102.19 82.3404L101.25 80.1504L88.9997 119.48H89.0197Z" fill="#121212"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
@@ -19,13 +19,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
<div class="row items-center no-wrap card-container q-px-md tw-mb-[0.675rem]">
|
||||
<div class="flex items-center tw-h-[60px]">
|
||||
<div
|
||||
class="flex justify-center items-center q-mr-md cursor-pointer"
|
||||
style="
|
||||
border: 1.5px solid;
|
||||
border-radius: 50%;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
"
|
||||
no-caps
|
||||
padding="xs"
|
||||
outline
|
||||
icon="arrow_back_ios_new"
|
||||
class="el-border tw-w-6 tw-h-6 flex items-center justify-center cursor-pointer el-border-radius q-mr-sm"
|
||||
title="Go Back"
|
||||
@click="$emit('cancel:hideform')"
|
||||
>
|
||||
|
||||
@@ -23,13 +23,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
>
|
||||
<div class="flex items-center tw-h-[60px]">
|
||||
<div
|
||||
class="flex justify-center items-center q-mr-md cursor-pointer"
|
||||
style="
|
||||
border: 1.5px solid;
|
||||
border-radius: 50%;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
"
|
||||
no-caps
|
||||
padding="xs"
|
||||
outline
|
||||
icon="arrow_back_ios_new"
|
||||
class="el-border tw-w-6 tw-h-6 flex items-center justify-center cursor-pointer el-border-radius q-mr-sm"
|
||||
title="Go Back"
|
||||
@click="$emit('cancel:hideform')"
|
||||
>
|
||||
|
||||
@@ -120,12 +120,30 @@ describe('PipelinesDestinationList Component - Comprehensive Function Tests', ()
|
||||
});
|
||||
|
||||
it('should initialize columns correctly', () => {
|
||||
expect(wrapper.vm.columns).toHaveLength(5);
|
||||
expect(wrapper.vm.columns).toHaveLength(7);
|
||||
expect(wrapper.vm.columns[0].name).toBe('#');
|
||||
expect(wrapper.vm.columns[1].name).toBe('name');
|
||||
expect(wrapper.vm.columns[2].name).toBe('url');
|
||||
expect(wrapper.vm.columns[3].name).toBe('method');
|
||||
expect(wrapper.vm.columns[4].name).toBe('actions');
|
||||
expect(wrapper.vm.columns[2].name).toBe('destination_type');
|
||||
expect(wrapper.vm.columns[3].name).toBe('url');
|
||||
expect(wrapper.vm.columns[4].name).toBe('method');
|
||||
expect(wrapper.vm.columns[5].name).toBe('output_format');
|
||||
expect(wrapper.vm.columns[6].name).toBe('actions');
|
||||
});
|
||||
|
||||
it('should format output_format column correctly for string values', () => {
|
||||
const outputFormatColumn = wrapper.vm.columns.find((col: any) => col.name === 'output_format');
|
||||
expect(outputFormatColumn).toBeDefined();
|
||||
expect(outputFormatColumn?.format?.('json')).toBe('JSON');
|
||||
expect(outputFormatColumn?.format?.('ndjson')).toBe('NDJSON');
|
||||
expect(outputFormatColumn?.format?.('nestedevent')).toBe('NESTEDEVENT');
|
||||
expect(outputFormatColumn?.format?.(null)).toBe('N/A');
|
||||
expect(outputFormatColumn?.format?.(undefined)).toBe('N/A');
|
||||
});
|
||||
|
||||
it('should format output_format column correctly for esbulk object', () => {
|
||||
const outputFormatColumn = wrapper.vm.columns.find((col: any) => col.name === 'output_format');
|
||||
expect(outputFormatColumn).toBeDefined();
|
||||
expect(outputFormatColumn?.format?.({ esbulk: { index: 'test' } })).toBe('ESBULK');
|
||||
});
|
||||
|
||||
it('should initialize per page options correctly', () => {
|
||||
@@ -249,9 +267,9 @@ describe('PipelinesDestinationList Component - Comprehensive Function Tests', ()
|
||||
|
||||
describe('filterData', () => {
|
||||
const mockRows = [
|
||||
{ name: 'webhook-destination', url: 'http://example.com' },
|
||||
{ name: 'email-destination', url: 'smtp://mail.com' },
|
||||
{ name: 'slack-webhook', url: 'http://slack.com' },
|
||||
{ name: 'webhook-destination', url: 'http://example.com', method: 'post', destination_type_name: 'openobserve', output_format: 'json' },
|
||||
{ name: 'email-destination', url: 'smtp://mail.com', method: 'post', destination_type_name: 'custom', output_format: 'json' },
|
||||
{ name: 'slack-webhook', url: 'http://slack.com', method: 'post', destination_type_name: 'custom', output_format: 'ndjson' },
|
||||
];
|
||||
|
||||
it('should filter data by name (case insensitive)', () => {
|
||||
|
||||
@@ -15,25 +15,32 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<q-page class="q-pa-none" style="height: calc(100vh - 88px); min-height: inherit">
|
||||
<div v-if="!showDestinationEditor" >
|
||||
<div class="tw-flex tw-justify-between tw-items-center tw-px-4 tw-py-3 tw-h-[68px] tw-border-b-[1px]"
|
||||
<q-page
|
||||
class="q-pa-none"
|
||||
style="height: calc(100vh - 88px); min-height: inherit"
|
||||
>
|
||||
<div v-if="!showDestinationEditor">
|
||||
<div
|
||||
class="tw-flex tw-justify-between tw-items-center tw-px-4 tw-py-3 tw-h-[68px] tw-border-b-[1px]"
|
||||
>
|
||||
<div class="q-table__title tw-font-[600]" data-test="alert-destinations-list-title">
|
||||
{{ t("pipeline_destinations.header") }}
|
||||
</div>
|
||||
<div class="tw-flex tw-justify-end">
|
||||
<q-input
|
||||
v-model="filterQuery"
|
||||
borderless
|
||||
dense
|
||||
class="q-ml-auto no-border o2-search-input"
|
||||
:placeholder="t('pipeline_destinations.search')"
|
||||
>
|
||||
<template #prepend>
|
||||
<q-icon class="o2-search-input-icon" name="search" />
|
||||
</template>
|
||||
</q-input>
|
||||
<div
|
||||
class="q-table__title tw-font-[600]"
|
||||
data-test="alert-destinations-list-title"
|
||||
>
|
||||
{{ t("pipeline_destinations.header") }}
|
||||
</div>
|
||||
<div class="tw-flex tw-justify-end">
|
||||
<q-input
|
||||
v-model="filterQuery"
|
||||
borderless
|
||||
dense
|
||||
class="q-ml-auto no-border o2-search-input"
|
||||
:placeholder="t('pipeline_destinations.search')"
|
||||
>
|
||||
<template #prepend>
|
||||
<q-icon class="o2-search-input-icon" name="search" />
|
||||
</template>
|
||||
</q-input>
|
||||
<q-btn
|
||||
data-test="pipeline-destination-list-add-btn"
|
||||
class="o2-primary-button q-ml-sm tw-h-[36px]"
|
||||
@@ -42,7 +49,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
:label="t(`alert_destinations.add`)"
|
||||
@click="editDestination(null)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<q-table
|
||||
data-test="alert-destinations-list-table"
|
||||
@@ -52,9 +59,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
row-key="id"
|
||||
:pagination="pagination"
|
||||
class="o2-quasar-table o2-row-md o2-quasar-table-header-sticky o2-last-row-border"
|
||||
:style="hasVisibleRows
|
||||
:style="
|
||||
hasVisibleRows
|
||||
? 'width: 100%; height: calc(100vh - 112px); overflow-y: auto;'
|
||||
: 'width: 100%'"
|
||||
: 'width: 100%'
|
||||
"
|
||||
>
|
||||
<template #no-data>
|
||||
<template>
|
||||
@@ -65,69 +74,72 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
<q-td :props="props">
|
||||
<div class="tw-flex tw-justify-center tw-items-center">
|
||||
<q-btn
|
||||
:data-test="`alert-destination-list-${props.row.name}-update-destination`"
|
||||
padding="sm"
|
||||
unelevated
|
||||
size="sm"
|
||||
round
|
||||
flat
|
||||
icon="edit"
|
||||
:title="t('alert_destinations.edit')"
|
||||
@click="editDestination(props.row)"
|
||||
>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
:data-test="`alert-destination-list-${props.row.name}-delete-destination`"
|
||||
padding="sm"
|
||||
unelevated
|
||||
size="sm"
|
||||
round
|
||||
flat
|
||||
:icon="outlinedDelete"
|
||||
:title="t('alert_destinations.delete')"
|
||||
@click="conformDeleteDestination(props.row)"
|
||||
>
|
||||
</q-btn>
|
||||
:data-test="`alert-destination-list-${props.row.name}-update-destination`"
|
||||
padding="sm"
|
||||
unelevated
|
||||
size="sm"
|
||||
round
|
||||
flat
|
||||
icon="edit"
|
||||
:title="t('alert_destinations.edit')"
|
||||
@click="editDestination(props.row)"
|
||||
>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
:data-test="`alert-destination-list-${props.row.name}-delete-destination`"
|
||||
padding="sm"
|
||||
unelevated
|
||||
size="sm"
|
||||
round
|
||||
flat
|
||||
:icon="outlinedDelete"
|
||||
:title="t('alert_destinations.delete')"
|
||||
@click="conformDeleteDestination(props.row)"
|
||||
>
|
||||
</q-btn>
|
||||
</div>
|
||||
</q-td>
|
||||
</template>
|
||||
<template #bottom="scope">
|
||||
<div class="tw-flex tw-items-center tw-justify-end tw-w-full tw-h-[48px]">
|
||||
<div class="o2-table-footer-title tw-flex tw-items-center tw-w-[200px] tw-mr-md">
|
||||
{{ resultTotal }} {{ t('pipeline_destinations.header') }}
|
||||
</div>
|
||||
<QTablePagination
|
||||
:scope="scope"
|
||||
:position="'bottom'"
|
||||
:resultTotal="resultTotal"
|
||||
:perPageOptions="perPageOptions"
|
||||
@update:changeRecordPerPage="changePagination"
|
||||
/>
|
||||
<div
|
||||
class="tw-flex tw-items-center tw-justify-end tw-w-full tw-h-[48px]"
|
||||
>
|
||||
<div
|
||||
class="o2-table-footer-title tw-flex tw-items-center tw-w-[200px] tw-mr-md"
|
||||
>
|
||||
{{ resultTotal }} {{ t("pipeline_destinations.header") }}
|
||||
</div>
|
||||
<QTablePagination
|
||||
:scope="scope"
|
||||
:position="'bottom'"
|
||||
:resultTotal="resultTotal"
|
||||
:perPageOptions="perPageOptions"
|
||||
@update:changeRecordPerPage="changePagination"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<!-- render the table headers -->
|
||||
<q-th
|
||||
v-for="col in props.cols"
|
||||
:key="col.name"
|
||||
:props="props"
|
||||
:class="col.classes"
|
||||
:style="col.style"
|
||||
>
|
||||
{{ col.label }}
|
||||
</q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
<q-tr :props="props">
|
||||
<!-- render the table headers -->
|
||||
<q-th
|
||||
v-for="col in props.cols"
|
||||
:key="col.name"
|
||||
:props="props"
|
||||
:class="col.classes"
|
||||
:style="col.style"
|
||||
>
|
||||
{{ col.label }}
|
||||
</q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
</q-table>
|
||||
</div>
|
||||
<div v-else>
|
||||
<AddDestination
|
||||
:is-alerts="false"
|
||||
<PipelineDestinationEditor
|
||||
:destination="editingDestination"
|
||||
:templates="templates"
|
||||
@cancel:hideform="toggleDestinationEditor"
|
||||
@get:destinations="getDestinations"
|
||||
@cancel="toggleDestinationEditor"
|
||||
@created="handleDestinationCreated"
|
||||
@updated="handleDestinationUpdated"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -141,13 +153,21 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
</q-page>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { ref, onBeforeMount, onActivated, watch, defineComponent, onMounted, computed } from "vue";
|
||||
import {
|
||||
ref,
|
||||
onBeforeMount,
|
||||
onActivated,
|
||||
watch,
|
||||
defineComponent,
|
||||
onMounted,
|
||||
computed,
|
||||
} from "vue";
|
||||
import type { Ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useQuasar, type QTableProps } from "quasar";
|
||||
import NoData from "../shared/grid/NoData.vue";
|
||||
import { getImageURL } from "@/utils/zincutils";
|
||||
import AddDestination from "./AddDestination.vue";
|
||||
import PipelineDestinationEditor from "../pipeline/PipelineDestinationEditor.vue";
|
||||
import destinationService from "@/services/alert_destination";
|
||||
import templateService from "@/services/alert_templates";
|
||||
import { useStore } from "vuex";
|
||||
@@ -166,7 +186,12 @@ interface ConformDelete {
|
||||
}
|
||||
export default defineComponent({
|
||||
name: "PageAlerts",
|
||||
components: { AddDestination, NoData, ConfirmDialog, QTablePagination },
|
||||
components: {
|
||||
PipelineDestinationEditor,
|
||||
NoData,
|
||||
ConfirmDialog,
|
||||
QTablePagination,
|
||||
},
|
||||
setup() {
|
||||
const qTable = ref();
|
||||
const store = useStore();
|
||||
@@ -189,6 +214,15 @@ export default defineComponent({
|
||||
align: "left",
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: "destination_type",
|
||||
field: "destination_type_name",
|
||||
label: "Destination Type",
|
||||
align: "left",
|
||||
sortable: true,
|
||||
style: "width: 150px",
|
||||
format: (val: string) => val || "N/A",
|
||||
},
|
||||
{
|
||||
name: "url",
|
||||
field: "url",
|
||||
@@ -202,7 +236,21 @@ export default defineComponent({
|
||||
label: t("alert_destinations.method"),
|
||||
align: "left",
|
||||
sortable: true,
|
||||
style: "width: 150px",
|
||||
style: "width: 120px",
|
||||
},
|
||||
{
|
||||
name: "output_format",
|
||||
field: "output_format",
|
||||
label: "Output Format",
|
||||
align: "left",
|
||||
sortable: true,
|
||||
style: "width: 140px",
|
||||
format: (val: any) => {
|
||||
if (!val) return "N/A";
|
||||
if (typeof val === "string") return val.toUpperCase();
|
||||
if (typeof val === "object" && val.esbulk) return "ESBULK";
|
||||
return "N/A";
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "actions",
|
||||
@@ -210,7 +258,7 @@ export default defineComponent({
|
||||
label: t("alert_destinations.actions"),
|
||||
align: "center",
|
||||
sortable: false,
|
||||
classes:'actions-column'
|
||||
classes: "actions-column",
|
||||
},
|
||||
]);
|
||||
const destinations: Ref<DestinationPayload[]> = ref([]);
|
||||
@@ -250,12 +298,12 @@ export default defineComponent({
|
||||
() => router.currentRoute.value.query.action,
|
||||
(action) => {
|
||||
if (!action) showDestinationEditor.value = false;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
onMounted(()=>{
|
||||
onMounted(() => {
|
||||
updateRoute();
|
||||
})
|
||||
});
|
||||
|
||||
const getDestinations = () => {
|
||||
const dismiss = q.notify({
|
||||
@@ -283,7 +331,7 @@ export default defineComponent({
|
||||
updateRoute();
|
||||
})
|
||||
.catch((err) => {
|
||||
if(err.response.status != 403){
|
||||
if (err.response.status != 403) {
|
||||
q.notify({
|
||||
type: "negative",
|
||||
message: "Error while pulling destinations.",
|
||||
@@ -306,19 +354,19 @@ export default defineComponent({
|
||||
editDestination(null);
|
||||
if (router.currentRoute.value.query.action === "update")
|
||||
editDestination(
|
||||
getDestinationByName(router.currentRoute.value.query.name as string)
|
||||
getDestinationByName(router.currentRoute.value.query.name as string),
|
||||
);
|
||||
};
|
||||
const getDestinationByName = (name: string) => {
|
||||
return destinations.value.find(
|
||||
(destination) => destination.name === name
|
||||
(destination) => destination.name === name,
|
||||
);
|
||||
};
|
||||
const editDestination = (destination: any) => {
|
||||
if (!destination) {
|
||||
track("Button Click", {
|
||||
button: "Add Pipeline Destination",
|
||||
page: "Pipeline Destinations"
|
||||
page: "Pipeline Destinations",
|
||||
});
|
||||
}
|
||||
toggleDestinationEditor();
|
||||
@@ -394,6 +442,29 @@ export default defineComponent({
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleDestinationCreated = (destinationName: string) => {
|
||||
// Close the editor and refresh the list
|
||||
toggleDestinationEditor();
|
||||
getDestinations();
|
||||
|
||||
q.notify({
|
||||
type: "positive",
|
||||
message: `Destination "${destinationName}" created successfully.`,
|
||||
});
|
||||
};
|
||||
|
||||
const handleDestinationUpdated = (destinationName: string) => {
|
||||
// Close the editor and refresh the list
|
||||
toggleDestinationEditor();
|
||||
getDestinations();
|
||||
|
||||
q.notify({
|
||||
type: "positive",
|
||||
message: `Destination "${destinationName}" updated successfully.`,
|
||||
});
|
||||
};
|
||||
|
||||
const changePagination = (val: { label: string; value: any }) => {
|
||||
selectedPerPage.value = val.value;
|
||||
pagination.value.rowsPerPage = val.value;
|
||||
@@ -403,7 +474,26 @@ export default defineComponent({
|
||||
var filtered = [];
|
||||
terms = terms.toLowerCase();
|
||||
for (var i = 0; i < rows.length; i++) {
|
||||
if (rows[i]["name"].toLowerCase().includes(terms)) {
|
||||
// Convert output_format to string for filtering
|
||||
let outputFormatStr = "";
|
||||
if (rows[i]["output_format"]) {
|
||||
if (typeof rows[i]["output_format"] === "string") {
|
||||
outputFormatStr = rows[i]["output_format"].toLowerCase();
|
||||
} else if (
|
||||
typeof rows[i]["output_format"] === "object" &&
|
||||
rows[i]["output_format"].esbulk
|
||||
) {
|
||||
outputFormatStr = "esbulk";
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
rows[i]["name"].toLowerCase().includes(terms) ||
|
||||
rows[i]["destination_type_name"].toLowerCase().includes(terms) ||
|
||||
rows[i]["url"].toLowerCase().includes(terms) ||
|
||||
rows[i]["method"].toLowerCase().includes(terms) ||
|
||||
outputFormatStr.includes(terms)
|
||||
) {
|
||||
filtered.push(rows[i]);
|
||||
}
|
||||
}
|
||||
@@ -427,9 +517,13 @@ export default defineComponent({
|
||||
const hasVisibleRows = computed(() => visibleRows.value.length > 0);
|
||||
|
||||
// Watch visibleRows to sync resultTotal with search filter
|
||||
watch(visibleRows, (newVisibleRows) => {
|
||||
resultTotal.value = newVisibleRows.length;
|
||||
}, { immediate: true });
|
||||
watch(
|
||||
visibleRows,
|
||||
(newVisibleRows) => {
|
||||
resultTotal.value = newVisibleRows.length;
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
return {
|
||||
t,
|
||||
@@ -445,6 +539,8 @@ export default defineComponent({
|
||||
editingDestination,
|
||||
templates,
|
||||
toggleDestinationEditor,
|
||||
handleDestinationCreated,
|
||||
handleDestinationUpdated,
|
||||
getDestinations,
|
||||
deleteDestination,
|
||||
cancelDeleteDestination,
|
||||
|
||||
@@ -18,13 +18,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
<div class="row items-center no-wrap q-mx-md q-pt-sm">
|
||||
<div class="flex items-center tw-py-2">
|
||||
<div
|
||||
class="flex justify-center items-center q-mr-md cursor-pointer"
|
||||
style="
|
||||
border: 1.5px solid;
|
||||
border-radius: 50%;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
"
|
||||
no-caps
|
||||
padding="xs"
|
||||
outline
|
||||
icon="arrow_back_ios_new"
|
||||
class="el-border tw-w-6 tw-h-6 flex items-center justify-center cursor-pointer el-border-radius q-mr-sm"
|
||||
title="Go Back"
|
||||
@click="$emit('cancel:hideform')"
|
||||
>
|
||||
|
||||
@@ -17,8 +17,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
<template>
|
||||
<div
|
||||
data-test="add-stream-input-stream-routing-section"
|
||||
class="full-height"
|
||||
style="width: 40vw"
|
||||
class="tw-h-[calc(100vh)] tw-overflow-auto tw-w-[40vw]"
|
||||
:class="store.state.theme === 'dark' ? 'bg-dark' : 'bg-white'"
|
||||
>
|
||||
<q-page>
|
||||
@@ -26,242 +25,92 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
<div class="row items-center no-wrap q-mx-md q-pb-sm q-pl-md q-pt-md">
|
||||
<div class="flex items-center tw-w-full">
|
||||
<div class="tw-w-full" data-test="add-destination-title">
|
||||
<div class="tw-text-[18px] tw-flex tw-items-center tw-justify-between">
|
||||
<div
|
||||
class="tw-text-[18px] tw-flex tw-items-center tw-justify-between"
|
||||
>
|
||||
External Destination
|
||||
<div>
|
||||
<q-btn v-close-popup="true" round flat icon="cancel" >
|
||||
</q-btn>
|
||||
<q-btn v-close-popup="true" round flat icon="cancel"> </q-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<q-separator />
|
||||
<div class="row q-col-gutter-sm q-px-lg q-my-sm">
|
||||
<div class="row q-col-gutter-sm q-px-lg">
|
||||
<q-toggle
|
||||
data-test="create-stream-toggle"
|
||||
class="q-mb-sm tw-h-[36px] o2-toggle-button-lg tw-mr-3 -tw-ml-4"
|
||||
size="lg"
|
||||
:class="store.state.theme === 'dark' ? 'o2-toggle-button-lg-dark' : 'o2-toggle-button-lg-light'"
|
||||
class="q-mb-sm tw-h-[36px] o2-toggle-button-xs tw-mr-3 q-mt-md"
|
||||
size="xs"
|
||||
:class="
|
||||
store.state.theme === 'dark'
|
||||
? 'o2-toggle-button-xs-dark'
|
||||
: 'o2-toggle-button-xs-light'
|
||||
"
|
||||
:label="'Create new Destination'"
|
||||
v-model="createNewDestination"
|
||||
/>
|
||||
<div
|
||||
v-if="!createNewDestination"
|
||||
class="col-12 q-py-xs destination-method-select"
|
||||
>
|
||||
<q-select
|
||||
data-test="external-destination-select"
|
||||
v-model="selectedDestination"
|
||||
:label="'Destination *'"
|
||||
:options="getFormattedDestinations"
|
||||
color="input-border"
|
||||
bg-color="input-bg"
|
||||
class="showLabelOnTop"
|
||||
stack-label
|
||||
outlined
|
||||
filled
|
||||
dense
|
||||
tabindex="0"
|
||||
>
|
||||
<template v-slot:option="scope">
|
||||
<q-item
|
||||
style="max-width: calc(40vw - 42px)"
|
||||
v-bind="scope.itemProps"
|
||||
>
|
||||
<q-item-section class="flex flex-col">
|
||||
<q-item-label>
|
||||
<span class="text-bold"> {{ scope.opt.label }}</span> -
|
||||
<span class="truncate-url"> {{ scope.opt.url }}</span>
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
</q-select>
|
||||
|
||||
<div v-if="createNewDestination" class="q-mt-sm q-mb-md col-12">
|
||||
|
||||
<!-- Create New Destination Form -->
|
||||
<CreateDestinationForm
|
||||
@created="handleDestinationCreated"
|
||||
@cancel="handleCancel"
|
||||
/>
|
||||
</div>
|
||||
<q-form
|
||||
ref="destinationForm"
|
||||
@submit="
|
||||
createNewDestination ? createDestination() : saveDestination()
|
||||
"
|
||||
class="col-12"
|
||||
>
|
||||
<div class="col-12 q-py-xs">
|
||||
<q-input
|
||||
v-if="createNewDestination"
|
||||
data-test="add-destination-name-input"
|
||||
v-model="formData.name"
|
||||
:label="t('alerts.name') + ' *'"
|
||||
color="input-border"
|
||||
bg-color="input-bg"
|
||||
class="showLabelOnTop"
|
||||
stack-label
|
||||
outlined
|
||||
filled
|
||||
dense
|
||||
:rules="[
|
||||
(val: any) =>
|
||||
!!val
|
||||
? isValidResourceName(val) ||
|
||||
`Characters like :, ?, /, #, and spaces are not allowed.`
|
||||
: t('common.nameRequired'),
|
||||
]"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="createNewDestination" class="col-12 q-py-xs">
|
||||
<q-input
|
||||
data-test="add-destination-url-input"
|
||||
v-model="formData.url"
|
||||
:label="t('alert_destinations.url') + ' *'"
|
||||
color="input-border"
|
||||
bg-color="input-bg"
|
||||
class="showLabelOnTop"
|
||||
stack-label
|
||||
outlined
|
||||
filled
|
||||
dense
|
||||
:rules="[(val: any) => !!val.trim() || 'Field is required!']"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
<div class="tw-flex tw-flex-row tw-gap-x-2">
|
||||
<div
|
||||
v-if="createNewDestination"
|
||||
class="tw-w-1/2 q-py-xs destination-method-select"
|
||||
>
|
||||
|
||||
<!-- Select Existing Destination -->
|
||||
<div v-else class="col-12">
|
||||
<div class="col-12 q-py-xs destination-method-select">
|
||||
<q-select
|
||||
data-test="add-destination-method-select"
|
||||
v-model="formData.method"
|
||||
:label="t('alert_destinations.method') + ' *'"
|
||||
:options="apiMethods"
|
||||
data-test="external-destination-select"
|
||||
v-model="selectedDestination"
|
||||
:label="'Destination *'"
|
||||
:options="getFormattedDestinations"
|
||||
color="input-border"
|
||||
bg-color="input-bg"
|
||||
class="showLabelOnTop"
|
||||
stack-label
|
||||
outlined
|
||||
:popup-content-style="{ textTransform: 'uppercase' }"
|
||||
filled
|
||||
dense
|
||||
:rules="[(val: any) => !!val || 'Field is required!']"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="createNewDestination"
|
||||
class="tw-w-1/2 q-py-xs destination-method-select"
|
||||
>
|
||||
<q-select
|
||||
data-test="add-destination-output-format-select"
|
||||
v-model="formData.output_format"
|
||||
:label="t('alert_destinations.output_format') + ' *'"
|
||||
:options="outputFormats"
|
||||
color="input-border"
|
||||
bg-color="input-bg"
|
||||
class="showLabelOnTop "
|
||||
stack-label
|
||||
outlined
|
||||
:popup-content-style="{ textTransform: 'uppercase' }"
|
||||
filled
|
||||
dense
|
||||
:rules="[(val: any) => !!val || 'Field is required!']"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
>
|
||||
<template v-slot:option="scope">
|
||||
<q-item
|
||||
style="max-width: calc(40vw - 42px)"
|
||||
v-bind="scope.itemProps"
|
||||
>
|
||||
<q-item-section class="flex flex-col">
|
||||
<q-item-label>
|
||||
<span class="text-bold"> {{ scope.opt.label }}</span> -
|
||||
<span class="truncate-url"> {{ scope.opt.url }}</span>
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
</q-select>
|
||||
</div>
|
||||
|
||||
<div v-if="createNewDestination" class="col-12 q-py-sm">
|
||||
<div class="text-bold q-py-xs" style="paddingleft: 10px">
|
||||
Headers
|
||||
</div>
|
||||
<div
|
||||
v-for="(header, index) in apiHeaders"
|
||||
:key="header.uuid"
|
||||
class="row q-col-gutter-sm q-pb-sm wrap"
|
||||
>
|
||||
<div class="col-5 q-ml-none">
|
||||
<q-input
|
||||
:data-test="`add-destination-header-${header['key']}-key-input`"
|
||||
v-model="header.key"
|
||||
color="input-border"
|
||||
bg-color="input-bg"
|
||||
stack-label
|
||||
outlined
|
||||
filled
|
||||
:placeholder="t('alert_destinations.api_header')"
|
||||
dense
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-5 q-ml-none">
|
||||
<q-input
|
||||
:data-test="`add-destination-header-${header['key']}-value-input`"
|
||||
v-model="header.value"
|
||||
:placeholder="t('alert_destinations.api_header_value')"
|
||||
color="input-border"
|
||||
bg-color="input-bg"
|
||||
stack-label
|
||||
outlined
|
||||
filled
|
||||
dense
|
||||
isUpdatingDestination
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-2 q-ml-none headers-btns">
|
||||
<q-btn
|
||||
:data-test="`add-destination-header-${header['key']}-delete-btn`"
|
||||
icon="delete"
|
||||
class="q-ml-xs iconHoverBtn"
|
||||
:class="store.state?.theme === 'dark' ? 'icon-dark' : ''"
|
||||
padding="sm"
|
||||
unelevated
|
||||
size="sm"
|
||||
round
|
||||
flat
|
||||
:title="t('alert_templates.edit')"
|
||||
@click="deleteApiHeader(header)"
|
||||
/>
|
||||
<q-btn
|
||||
data-test="add-destination-add-header-btn"
|
||||
v-if="index === apiHeaders.length - 1"
|
||||
icon="add"
|
||||
:class="store.state?.theme === 'dark' ? 'icon-dark' : ''"
|
||||
class="q-ml-xs iconHoverBtn"
|
||||
padding="sm"
|
||||
unelevated
|
||||
size="sm"
|
||||
round
|
||||
flat
|
||||
:title="t('alert_templates.edit')"
|
||||
@click="addApiHeader()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="createNewDestination" class="col-12 q-py-sm">
|
||||
<q-toggle
|
||||
data-test="add-destination-skip-tls-verify-toggle"
|
||||
class=" tw-h-[36px] o2-toggle-button-lg tw-mr-3 -tw-ml-4"
|
||||
size="lg"
|
||||
:class="store.state.theme === 'dark' ? 'o2-toggle-button-lg-dark' : 'o2-toggle-button-lg-light'"
|
||||
v-model="formData.skip_tls_verify"
|
||||
:label="t('alert_destinations.skip_tls_verify')"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex justify-start">
|
||||
<!-- Action buttons for existing destination selection -->
|
||||
<div class="flex justify-start q-mt-md q-mb-md">
|
||||
<q-btn
|
||||
v-if="pipelineObj.isEditNode && !createNewDestination"
|
||||
data-test="add-destination-delete-btn"
|
||||
class="o2-secondary-button tw-h-[36px] q-mr-md"
|
||||
color="negative"
|
||||
flat
|
||||
:class="store.state.theme === 'dark' ? 'o2-secondary-button-dark' : 'o2-secondary-button-light'"
|
||||
no-caps
|
||||
@click="openDeleteDialog"
|
||||
>
|
||||
v-if="pipelineObj.isEditNode"
|
||||
data-test="add-destination-delete-btn"
|
||||
class="o2-secondary-button tw-h-[36px] q-mr-sm"
|
||||
color="negative"
|
||||
flat
|
||||
:class="
|
||||
store.state.theme === 'dark'
|
||||
? 'o2-secondary-button-dark'
|
||||
: 'o2-secondary-button-light'
|
||||
"
|
||||
no-caps
|
||||
@click="openDeleteDialog"
|
||||
>
|
||||
<q-icon name="delete" class="q-mr-xs" />
|
||||
{{ t('pipeline.deleteNode') }}
|
||||
{{ t("pipeline.deleteNode") }}
|
||||
</q-btn>
|
||||
<q-btn
|
||||
data-test="add-destination-cancel-btn"
|
||||
@@ -269,21 +118,29 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
class="o2-secondary-button tw-h-[36px]"
|
||||
:label="t('alerts.cancel')"
|
||||
flat
|
||||
:class="store.state.theme === 'dark' ? 'o2-secondary-button-dark' : 'o2-secondary-button-light'"
|
||||
:class="
|
||||
store.state.theme === 'dark'
|
||||
? 'o2-secondary-button-dark'
|
||||
: 'o2-secondary-button-light'
|
||||
"
|
||||
no-caps
|
||||
@click="$emit('cancel:hideform')"
|
||||
@click="handleCancel"
|
||||
/>
|
||||
<q-btn
|
||||
data-test="add-destination-submit-btn"
|
||||
data-test="add-destination-save-btn"
|
||||
:label="t('alerts.save')"
|
||||
class="no-border q-ml-md o2-primary-button tw-h-[36px]"
|
||||
:class="store.state.theme === 'dark' ? 'o2-primary-button-dark' : 'o2-primary-button-light'"
|
||||
class="no-border q-ml-sm o2-primary-button tw-h-[36px]"
|
||||
:class="
|
||||
store.state.theme === 'dark'
|
||||
? 'o2-primary-button-dark'
|
||||
: 'o2-primary-button-light'
|
||||
"
|
||||
flat
|
||||
type="submit"
|
||||
no-caps
|
||||
@click="saveDestination"
|
||||
/>
|
||||
</div>
|
||||
</q-form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</q-page>
|
||||
@@ -296,54 +153,24 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
@update:cancel="dialog.show = false"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
ref,
|
||||
computed,
|
||||
onBeforeMount,
|
||||
onActivated,
|
||||
watch,
|
||||
} from "vue";
|
||||
import type { Ref } from "vue";
|
||||
import { ref, computed, onBeforeMount, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import destinationService from "@/services/alert_destination";
|
||||
import { useStore } from "vuex";
|
||||
import { useQuasar } from "quasar";
|
||||
import type {
|
||||
Template,
|
||||
DestinationData,
|
||||
Headers,
|
||||
DestinationPayload,
|
||||
} from "@/ts/interfaces";
|
||||
import { useRouter } from "vue-router";
|
||||
import { isValidResourceName } from "@/utils/zincutils";
|
||||
import AppTabs from "@/components/common/AppTabs.vue";
|
||||
import ConfirmDialog from "@/components/ConfirmDialog.vue";
|
||||
|
||||
import CreateDestinationForm from "./CreateDestinationForm.vue";
|
||||
import useDragAndDrop from "@/plugins/pipelines/useDnD";
|
||||
|
||||
const emit = defineEmits(["get:destinations", "cancel:hideform"]);
|
||||
const q = useQuasar();
|
||||
const apiMethods = ["get", "post", "put"];
|
||||
const outputFormats = ["json", "ndjson"];
|
||||
const store = useStore();
|
||||
const { t } = useI18n();
|
||||
const formData: Ref<DestinationData> = ref({
|
||||
name: "",
|
||||
url: "",
|
||||
method: "post",
|
||||
skip_tls_verify: false,
|
||||
template: "",
|
||||
headers: {},
|
||||
emails: "",
|
||||
type: "http",
|
||||
output_format: "json",
|
||||
});
|
||||
const isUpdatingDestination = ref(false);
|
||||
const createNewDestination = ref(false);
|
||||
const destinationForm = ref(null);
|
||||
|
||||
const { addNode, pipelineObj, deletePipelineNode } = useDragAndDrop();
|
||||
const retries = ref(0);
|
||||
const createNewDestination = ref(false);
|
||||
const selectedDestination: any = ref(
|
||||
pipelineObj.currentSelectedNodeData?.data?.destination_name
|
||||
? {
|
||||
@@ -354,8 +181,6 @@ const selectedDestination: any = ref(
|
||||
);
|
||||
const destinations = ref([]);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const dialog = ref({
|
||||
show: false,
|
||||
title: "",
|
||||
@@ -363,120 +188,20 @@ const dialog = ref({
|
||||
okCallback: () => {},
|
||||
});
|
||||
|
||||
// TODO OK: Use UUID package instead of this and move this method in utils
|
||||
const getUUID = () => {
|
||||
return (Math.floor(Math.random() * (9999999999 - 100 + 1)) + 100).toString();
|
||||
};
|
||||
|
||||
const apiHeaders: Ref<
|
||||
{
|
||||
key: string;
|
||||
value: string;
|
||||
uuid: string;
|
||||
}[]
|
||||
> = ref([{ key: "", value: "", uuid: getUUID() }]);
|
||||
|
||||
onActivated(() => {});
|
||||
onBeforeMount(() => {
|
||||
getDestinations();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => createNewDestination.value,
|
||||
(val) => {
|
||||
if (val) {
|
||||
formData.value = {
|
||||
name: "",
|
||||
url: "",
|
||||
method: "post",
|
||||
skip_tls_verify: false,
|
||||
template: "",
|
||||
headers: {},
|
||||
emails: "",
|
||||
type: "http",
|
||||
output_format: "json",
|
||||
};
|
||||
apiHeaders.value = [{ key: "", value: "", uuid: getUUID() }];
|
||||
if (!val) {
|
||||
// When switching back to select mode, refresh destinations
|
||||
getDestinations();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const isValidDestination = computed(
|
||||
() => formData.value.name && formData.value.url && formData.value.method,
|
||||
);
|
||||
const createDestination = () => {
|
||||
if (!isValidDestination.value) {
|
||||
q.notify({
|
||||
type: "negative",
|
||||
message: "Please fill required fields",
|
||||
timeout: 1500,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const dismiss = q.notify({
|
||||
spinner: true,
|
||||
message: "Please wait...",
|
||||
timeout: 2000,
|
||||
});
|
||||
const headers: Headers = {};
|
||||
apiHeaders.value.forEach((header) => {
|
||||
if (header["key"] && header["value"]) headers[header.key] = header.value;
|
||||
});
|
||||
|
||||
const payload: any = {
|
||||
url: formData.value.url,
|
||||
method: formData.value.method,
|
||||
skip_tls_verify: formData.value.skip_tls_verify,
|
||||
template: formData.value.template,
|
||||
headers: headers,
|
||||
name: formData.value.name,
|
||||
type: "http",
|
||||
output_format: formData.value.output_format,
|
||||
};
|
||||
|
||||
destinationService
|
||||
.create({
|
||||
org_identifier: store.state.selectedOrganization.identifier,
|
||||
destination_name: formData.value.name,
|
||||
data: payload,
|
||||
})
|
||||
.then(() => {
|
||||
dismiss();
|
||||
// emit("cancel:hideform");
|
||||
q.notify({
|
||||
type: "positive",
|
||||
message: `Destination saved successfully.`,
|
||||
});
|
||||
selectedDestination.value = {
|
||||
label: formData.value.name,
|
||||
value: formData.value.name,
|
||||
};
|
||||
createNewDestination.value = false;
|
||||
|
||||
getDestinations();
|
||||
})
|
||||
.catch((err: any) => {
|
||||
if (err.response?.status == 403) {
|
||||
return;
|
||||
}
|
||||
dismiss();
|
||||
q.notify({
|
||||
type: "negative",
|
||||
message: err.response?.data?.error || err.response?.data?.message,
|
||||
});
|
||||
});
|
||||
};
|
||||
const addApiHeader = (key: string = "", value: string = "") => {
|
||||
apiHeaders.value.push({ key: key, value: value, uuid: getUUID() });
|
||||
};
|
||||
const deleteApiHeader = (header: any) => {
|
||||
apiHeaders.value = apiHeaders.value.filter(
|
||||
(_header) => _header.uuid !== header.uuid,
|
||||
);
|
||||
if (formData.value?.headers?.[header.key])
|
||||
delete formData.value?.headers?.[header.key];
|
||||
if (!apiHeaders.value.length) addApiHeader();
|
||||
};
|
||||
|
||||
const getFormattedDestinations = computed(() => {
|
||||
return destinations.value.map((destination: any) => {
|
||||
const truncatedUrl =
|
||||
@@ -492,16 +217,6 @@ const getFormattedDestinations = computed(() => {
|
||||
});
|
||||
});
|
||||
|
||||
const createEmailTemplate = () => {
|
||||
router.push({
|
||||
name: "alertTemplates",
|
||||
query: {
|
||||
action: "add",
|
||||
type: "email",
|
||||
org_identifier: store.state.selectedOrganization.identifier,
|
||||
},
|
||||
});
|
||||
};
|
||||
const getDestinations = () => {
|
||||
const dismiss = q.notify({
|
||||
spinner: true,
|
||||
@@ -555,12 +270,27 @@ const saveDestination = () => {
|
||||
emit("cancel:hideform");
|
||||
};
|
||||
|
||||
const handleDestinationCreated = (destinationName: string) => {
|
||||
// Switch back to selection mode and select the newly created destination
|
||||
selectedDestination.value = {
|
||||
label: destinationName,
|
||||
value: destinationName,
|
||||
};
|
||||
createNewDestination.value = false;
|
||||
getDestinations();
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
emit("cancel:hideform");
|
||||
};
|
||||
|
||||
const openDeleteDialog = () => {
|
||||
dialog.value.show = true;
|
||||
dialog.value.title = "Delete Node";
|
||||
dialog.value.message = "Are you sure you want to delete stream routing?";
|
||||
dialog.value.okCallback = deleteRoute;
|
||||
};
|
||||
|
||||
const deleteRoute = () => {
|
||||
deletePipelineNode(pipelineObj.currentSelectedNodeID);
|
||||
emit("cancel:hideform");
|
||||
@@ -568,27 +298,18 @@ const deleteRoute = () => {
|
||||
|
||||
// Expose functions for testing
|
||||
defineExpose({
|
||||
getUUID,
|
||||
createDestination,
|
||||
addApiHeader,
|
||||
deleteApiHeader,
|
||||
createEmailTemplate,
|
||||
getDestinations,
|
||||
saveDestination,
|
||||
formData,
|
||||
apiHeaders,
|
||||
selectedDestination,
|
||||
destinations,
|
||||
isValidDestination,
|
||||
getFormattedDestinations,
|
||||
createNewDestination,
|
||||
isUpdatingDestination,
|
||||
retries,
|
||||
apiMethods,
|
||||
outputFormats,
|
||||
pipelineObj
|
||||
pipelineObj,
|
||||
handleDestinationCreated,
|
||||
handleCancel,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.destination-method-select {
|
||||
.q-field__native > :first-child {
|
||||
@@ -596,19 +317,9 @@ defineExpose({
|
||||
}
|
||||
}
|
||||
|
||||
.no-case .q-field__native span {
|
||||
text-transform: none !important;
|
||||
}
|
||||
.headers-btns {
|
||||
.q-btn {
|
||||
&.icon-dark {
|
||||
filter: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
.truncate-url {
|
||||
display: inline-block;
|
||||
max-width: calc(40vw - 200px); /* Adjust the width as needed */
|
||||
max-width: calc(40vw - 200px);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
<!-- Copyright 2023 OpenObserve Inc.
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<q-page
|
||||
class="q-pa-none o2-custom-bg"
|
||||
>
|
||||
<div
|
||||
class="row items-center no-wrap card-container q-px-md tw-mb-[0.675rem]"
|
||||
>
|
||||
<div class="flex items-center tw-h-[60px]">
|
||||
<div
|
||||
no-caps
|
||||
padding="xs"
|
||||
outline
|
||||
icon="arrow_back_ios_new"
|
||||
class="el-border tw-w-6 tw-h-6 flex items-center justify-center cursor-pointer el-border-radius q-mr-sm"
|
||||
title="Go Back"
|
||||
@click="$emit('cancel')"
|
||||
>
|
||||
<q-icon name="arrow_back_ios_new" size="14px" />
|
||||
</div>
|
||||
<div class="col" data-test="pipeline-destination-editor-title">
|
||||
<div v-if="destination" class="text-h6">
|
||||
{{ t("alert_destinations.updateTitle") }} - {{ destination.name }}
|
||||
</div>
|
||||
<div v-else class="text-h6">
|
||||
{{ t("alert_destinations.addTitle") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-container tw-py-2 q-px-md tw-h-[calc(100vh-120px)] tw-overflow-auto">
|
||||
<div class="tw-w-[50vw]">
|
||||
<CreateDestinationForm
|
||||
:destination="destination"
|
||||
@created="handleDestinationCreated"
|
||||
@updated="handleDestinationUpdated"
|
||||
@cancel="handleCancel"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineProps, defineEmits } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import CreateDestinationForm from "./NodeForm/CreateDestinationForm.vue";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
// Props
|
||||
const props = defineProps<{
|
||||
destination?: any;
|
||||
}>();
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits(["cancel", "created", "updated"]);
|
||||
|
||||
// Handle destination creation
|
||||
const handleDestinationCreated = (destinationName: string) => {
|
||||
emit("created", destinationName);
|
||||
};
|
||||
|
||||
// Handle destination update
|
||||
const handleDestinationUpdated = (destinationName: string) => {
|
||||
emit("updated", destinationName);
|
||||
};
|
||||
|
||||
// Handle cancel
|
||||
const handleCancel = () => {
|
||||
emit("cancel");
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.card-container {
|
||||
border-radius: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -83,10 +83,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
<template #bottom="scope">
|
||||
<q-btn
|
||||
data-test="qm-multiple-cancel-query-btn"
|
||||
class="text-bold"
|
||||
class="o2-secondary-button no-border tw-h-[36px]"
|
||||
outline
|
||||
flat
|
||||
padding="sm lg"
|
||||
color="red"
|
||||
:disable="selectedRowsModel?.length === 0"
|
||||
@click="handleMultiQueryCancel"
|
||||
no-caps
|
||||
|
||||
@@ -73,10 +73,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
<template #bottom="scope">
|
||||
<q-btn
|
||||
data-test="qm-multiple-cancel-query-btn"
|
||||
class="text-bold"
|
||||
class="o2-secondary-button no-border tw-h-[36px]"
|
||||
flat
|
||||
outline
|
||||
padding="sm lg"
|
||||
color="red"
|
||||
:disable="selectedRow.length === 0"
|
||||
@click="handleMultiQueryCancel"
|
||||
no-caps
|
||||
|
||||
@@ -562,7 +562,8 @@
|
||||
"view": "View",
|
||||
"delay": "Delay",
|
||||
"history": "Pipeline History",
|
||||
"searchHistory": "Enter Pipeline Name"
|
||||
"searchHistory": "Enter Pipeline Name",
|
||||
"destination_type": "Destination Type"
|
||||
},
|
||||
"function": {
|
||||
"header": "Functions",
|
||||
|
||||
@@ -374,6 +374,8 @@ $json-object-dark: #D1D5DB; // Light gray
|
||||
--o2-json-null: #{$json-null-light};
|
||||
--o2-json-object: #{$json-object-light};
|
||||
|
||||
--o2-input-label-text-color: #636464;
|
||||
|
||||
}
|
||||
|
||||
// Semantic Colors - Dark Mode
|
||||
@@ -462,4 +464,6 @@ $json-object-dark: #D1D5DB; // Light gray
|
||||
--o2-json-boolean: #{$json-boolean-dark};
|
||||
--o2-json-null: #{$json-null-dark};
|
||||
--o2-json-object: #{$json-object-dark};
|
||||
|
||||
--o2-input-label-text-color: #BDBDBD;
|
||||
}
|
||||
|
||||
@@ -2121,4 +2121,8 @@ body {
|
||||
padding: 0.3125rem !important; // 5px
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.q-field--labeled.q-field--dense .q-field__suffix {
|
||||
padding-top: 3px;
|
||||
}
|
||||
@@ -92,9 +92,24 @@ export interface TemplateData extends Template {
|
||||
export interface Headers {
|
||||
[key: string]: string;
|
||||
}
|
||||
export interface DestinationMetadata {
|
||||
// Splunk specific fields
|
||||
source?: string;
|
||||
sourcetype?: string;
|
||||
hostname?: string;
|
||||
// Elasticsearch specific fields
|
||||
_index?: string;
|
||||
// Datadog specific fields
|
||||
ddsource?: string;
|
||||
ddtags?: string;
|
||||
service?: string;
|
||||
// hostname is shared between Splunk and Datadog
|
||||
}
|
||||
|
||||
export interface Destination {
|
||||
name: string;
|
||||
url?: string;
|
||||
url_endpoint?: string; // Frontend only - used for display/editing
|
||||
method?: string;
|
||||
skip_tls_verify?: boolean;
|
||||
headers?: Headers;
|
||||
@@ -102,12 +117,16 @@ export interface Destination {
|
||||
emails?: string;
|
||||
type: "http" | "email" | "sns" | "action";
|
||||
action_id?: string;
|
||||
output_format?: "json" | "ndjson";
|
||||
output_format?: "json" | "ndjson" | "nestedevent" | string; // string allows esbulk with dynamic index
|
||||
destination_type?: string; // Frontend internal use
|
||||
destination_type_name?: string; // From backend
|
||||
esbulk_index?: string; // For esbulk format index name
|
||||
metadata?: DestinationMetadata; // Destination-specific metadata as JSON object
|
||||
}
|
||||
|
||||
export interface DestinationPayload {
|
||||
name: string;
|
||||
url?: string;
|
||||
url?: string; // Full URL (merged with endpoint before sending)
|
||||
method?: string;
|
||||
skip_tls_verify?: boolean;
|
||||
headers?: Headers;
|
||||
@@ -115,7 +134,8 @@ export interface DestinationPayload {
|
||||
emails?: string[];
|
||||
type: "http" | "email" | "sns" | "action";
|
||||
action_id?: string;
|
||||
output_format?: "json" | "ndjson";
|
||||
output_format?: "json" | "ndjson" | "nestedevent" | string; // string allows esbulk with dynamic index
|
||||
destination_type?: string; // New field added
|
||||
}
|
||||
|
||||
// Destination object which is modified in frontend to display in table and form
|
||||
|
||||