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>
This commit is contained in:
Bhargav
2025-11-25 14:07:37 +05:30
committed by GitHub
co-authored by Yashodhan Joshi mmosarafO2 Yashodhan Joshi
parent 152218942e
commit de5edadb9e
25 changed files with 3444 additions and 1253 deletions
+79
View File
@@ -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"),
}
+15
View File
@@ -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)]
Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

+5 -7
View File
@@ -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')"
>
+5 -7
View File
@@ -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')"
>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -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
+2 -2
View File
@@ -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
+2 -1
View File
@@ -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",
+4
View File
@@ -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;
}
+4
View File
@@ -2121,4 +2121,8 @@ body {
padding: 0.3125rem !important; // 5px
}
}
}
.q-field--labeled.q-field--dense .q-field__suffix {
padding-top: 3px;
}
+23 -3
View File
@@ -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