fix: Actions UI issues (#6325)

1. Action Issues
2. #6288

---------

Co-authored-by: udev <uddhavdave1@gmail.com>
This commit is contained in:
Omkar Kesarkhane
2025-03-20 09:39:54 +08:00
committed by GitHub
co-authored by udev
parent f3fe0d4ba9
commit 28106fc564
9 changed files with 339 additions and 116 deletions
+2
View File
@@ -79,6 +79,8 @@ pub struct Endpoint {
pub skip_tls_verify: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub headers: Option<HashMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub action_id: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
+32 -14
View File
@@ -47,8 +47,17 @@ impl From<meta_dest::Destination> for Destination {
method: endpoint.method,
skip_tls_verify: endpoint.skip_tls_verify,
headers: endpoint.headers,
#[cfg(feature = "enterprise")]
destination_type: if endpoint.action_id.is_some() {
DestinationType::Action
} else {
DestinationType::Http
},
#[cfg(not(feature = "enterprise"))]
destination_type: DestinationType::Http,
template: Some(template),
#[cfg(feature = "enterprise")]
action_id: endpoint.action_id,
..Default::default()
},
meta_dest::DestinationType::Sns(aws_sns) => Self {
@@ -87,6 +96,7 @@ impl Destination {
method: self.method,
skip_tls_verify: self.skip_tls_verify,
headers: self.headers,
action_id: None,
})
}
DestinationType::Sns => meta_dest::DestinationType::Sns(meta_dest::AwsSns {
@@ -95,18 +105,25 @@ impl Destination {
}),
#[cfg(feature = "enterprise")]
DestinationType::Action => {
let action_endpoint = ActionEndpoint::new(&org_id, &self.action_id)
.map_err(DestinationError::InvalidActionId)?;
meta_dest::DestinationType::Http(meta_dest::Endpoint {
url: action_endpoint.url,
method: if action_endpoint.method == reqwest::Method::POST {
meta_dest::HTTPType::POST
} else {
meta_dest::HTTPType::GET
},
skip_tls_verify: action_endpoint.skip_tls,
headers: None,
})
if let Some(action_id) = self.action_id {
let action_endpoint = ActionEndpoint::new(&org_id, &action_id)
.map_err(DestinationError::InvalidActionId)?;
meta_dest::DestinationType::Http(meta_dest::Endpoint {
url: action_endpoint.url,
method: if action_endpoint.method == reqwest::Method::POST {
meta_dest::HTTPType::POST
} else {
meta_dest::HTTPType::GET
},
skip_tls_verify: action_endpoint.skip_tls,
headers: None,
action_id: Some(action_id),
})
} else {
return Err(DestinationError::InvalidActionId(anyhow::anyhow!(
"Action id is required for action destination"
)));
}
}
};
Ok(meta_dest::Destination {
@@ -125,6 +142,7 @@ impl Destination {
method: self.method,
skip_tls_verify: self.skip_tls_verify,
headers: self.headers,
action_id: None,
};
Ok(meta_dest::Destination {
id: None,
@@ -204,8 +222,8 @@ pub struct Destination {
pub destination_type: DestinationType,
/// Required when `destination_type` is `Action`
#[cfg(feature = "enterprise")]
#[serde(default)]
pub action_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub action_id: Option<String>,
}
#[derive(Serialize, Debug, Default, PartialEq, Eq, Deserialize, Clone, ToSchema)]
@@ -160,7 +160,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
:rules="[
(val: any) => {
if (!isEditingActionScript) {
return !!val || 'CSV File is required!';
return !!val || 'ZIP File is required!';
}
return true;
},
@@ -847,7 +847,10 @@ const saveActionScript = async () => {
// );
// Add cron expression if needed
if (frequency.value.type === "repeat") {
if (
formData.value.type === "scheduled" &&
frequency.value.type === "repeat"
) {
commonFields.cron_expr = frequency.value.cron.toString().trim();
// commonFields.timezoneOffset = convertedDateTime.offset.toString();
// commonFields.timezone = scheduling.value.timezone;
@@ -165,11 +165,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</div>
<div v-else>
<ImportDestination
:destinations="destinations"
:templates="templates"
@update:destinations="getDestinations"
/>
:destinations="destinations"
:templates="templates"
@update:destinations="getDestinations"
/>
</div>
<ConfirmDialog
@@ -182,7 +181,14 @@ 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 } from "vue";
import {
ref,
onBeforeMount,
onActivated,
watch,
defineComponent,
onMounted,
} from "vue";
import type { Ref } from "vue";
import { useI18n } from "vue-i18n";
import { useQuasar, type QTableProps } from "quasar";
@@ -200,6 +206,7 @@ import type { Template } from "@/ts/interfaces/index";
import { outlinedDelete } from "@quasar/extras/material-icons-outlined";
import ImportDestination from "./ImportDestination.vue";
import useActions from "@/composables/useActions";
interface ConformDelete {
visible: boolean;
@@ -207,13 +214,20 @@ interface ConformDelete {
}
export default defineComponent({
name: "PageAlerts",
components: { AddDestination, NoData, ConfirmDialog, QTablePagination ,ImportDestination},
components: {
AddDestination,
NoData,
ConfirmDialog,
QTablePagination,
ImportDestination,
},
setup() {
const qTable = ref();
const store = useStore();
const editingDestination: Ref<DestinationPayload | null> = ref(null);
const { t } = useI18n();
const q = useQuasar();
const { getAllActions } = useActions();
const columns: any = ref<QTableProps["columns"]>([
{
name: "#",
@@ -283,21 +297,39 @@ export default defineComponent({
onBeforeMount(() => {
getDestinations();
getTemplates();
getActions();
});
watch(
() => router.currentRoute.value.query.action,
(action) => {
if (!action) {
showDestinationEditor.value = false
showImportDestination.value = false
};
}
showDestinationEditor.value = false;
showImportDestination.value = false;
}
},
);
onMounted(()=>{
onMounted(() => {
updateRoute();
})
});
const getActions = async () => {
const dismiss = q.notify({
spinner: true,
message: "Please wait while loading actions...",
});
if (store.state.organizationData.actions.length == 0) {
await getAllActions()
.catch(() => {
q.notify({
type: "negative",
message: "Error while loading actions.",
});
})
.finally(() => dismiss());
}
};
const getDestinations = () => {
const dismiss = q.notify({
@@ -316,7 +348,9 @@ export default defineComponent({
.then((res) => {
res.data = res.data.filter(
(destination: any) =>
destination.type == "http" || destination.type == "email",
destination.type == "http" ||
destination.type == "email" ||
destination.type === "action",
);
resultTotal.value = res.data.length;
destinations.value = res.data.map((data: any, index: number) => ({
@@ -326,7 +360,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.",
@@ -349,14 +383,14 @@ 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),
);
if(router.currentRoute.value.query.action === "import")
if (router.currentRoute.value.query.action === "import")
showImportDestination.value = true;
};
const getDestinationByName = (name: string) => {
return destinations.value.find(
(destination) => destination.name === name
(destination) => destination.name === name,
);
};
const editDestination = (destination: any) => {
@@ -459,28 +493,27 @@ export default defineComponent({
});
};
const exportDestination = (row: any) =>{
const exportDestination = (row: any) => {
const findDestination: any = getDestinationByName(row.name);
const destinationByName = {...findDestination}
if(destinationByName.hasOwnProperty("#")) delete destinationByName["#"];
const destinationJson = JSON.stringify(destinationByName,null,2);
const blob = new Blob([destinationJson], { type: 'application/json' });
const url = URL.createObjectURL(blob);
// Create an anchor element to trigger the download
const link = document.createElement('a');
link.href = url;
const destinationByName = { ...findDestination };
if (destinationByName.hasOwnProperty("#")) delete destinationByName["#"];
const destinationJson = JSON.stringify(destinationByName, null, 2);
const blob = new Blob([destinationJson], { type: "application/json" });
const url = URL.createObjectURL(blob);
// Create an anchor element to trigger the download
const link = document.createElement("a");
link.href = url;
// Set the filename of the download
link.download = `${destinationByName.name}.json`;
// Set the filename of the download
link.download = `${destinationByName.name}.json`;
// Trigger the download by simulating a click
link.click();
// Trigger the download by simulating a click
link.click();
// Clean up the URL object after download
URL.revokeObjectURL(url);
}
const importDestination = () =>{
// Clean up the URL object after download
URL.revokeObjectURL(url);
};
const importDestination = () => {
showImportDestination.value = true;
router.push({
name: "alertDestinations",
@@ -489,7 +522,7 @@ export default defineComponent({
org_identifier: store.state.selectedOrganization.identifier,
},
});
}
};
return {
t,
+177 -47
View File
@@ -222,7 +222,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
dense
tabindex="0"
@update:model-value="
updateDestinationName(userSelectedDestinationName[index], index)
updateDestinationName(
userSelectedDestinationName[index],
index,
)
"
/>
</div>
@@ -250,7 +253,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
dense
tabindex="0"
@update:model-value="
updateDestinationUrl(userSelectedDestinationUrl[index], index)
updateDestinationUrl(
userSelectedDestinationUrl[index],
index,
)
"
/>
</div>
@@ -284,7 +290,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
fill-input
:input-debounce="400"
@update:model-value="
updateDestinationType(userSelectedDestinationType[index], index)
updateDestinationType(
userSelectedDestinationType[index],
index,
)
"
behavior="menu"
/>
@@ -318,7 +327,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
fill-input
:input-debounce="400"
@update:model-value="
updateDestinationMethod(userSelectedDestinationMethod[index], index)
updateDestinationMethod(
userSelectedDestinationMethod[index],
index,
)
"
behavior="menu"
/>
@@ -353,11 +365,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
hide-selected
fill-input
@filter="filterTemplates"
:rules="[
(val) => !!val || 'Field is required!',
]"
:rules="[(val) => !!val || 'Field is required!']"
style="width: 300px"
@update:model-value="updateDestinationTemplate($event, index)"
@update:model-value="
updateDestinationTemplate($event, index)
"
>
</q-select>
</div>
@@ -384,11 +396,54 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
dense
tabindex="0"
@update:model-value="
updateDestinationEmails(userSelectedEmails[index], index)
updateDestinationEmails(
userSelectedEmails[index],
index,
)
"
/>
</div>
</span>
<span
class="text-red"
v-else-if="
typeof errorMessage === 'object' &&
errorMessage.field == 'action_id'
"
>
{{ errorMessage.message }}
<div>
<q-select
data-test="destination-import-template-input"
v-model="userSelectedActionId[index]"
:options="filteredActions"
label="Templates *"
:popup-content-style="{
textTransform: 'lowercase',
}"
color="input-border"
bg-color="input-bg"
class="q-py-sm showLabelOnTop no-case"
filled
stack-label
dense
use-input
emit-value
map-options
input-debounce="400"
behavior="menu"
hide-selected
fill-input
@filter="filterActions"
:rules="[(val) => !!val || 'Field is required!']"
style="width: 300px"
@update:model-value="
updateDestinationAction($event, index)
"
>
</q-select>
</div>
</span>
<span
class="text-red"
v-else-if="
@@ -400,7 +455,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<div style="width: 300px">
<q-toggle
data-test="destination-import-skip-tls-verify-input"
:model-value="userSelectedSkipTlsVerify[index] ?? false"
:model-value="
userSelectedSkipTlsVerify[index] ?? false
"
:label="t('alert_destinations.skip_tls_verify')"
class="q-mt-sm"
@update:model-value="
@@ -416,7 +473,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</div>
<div class="error-section" v-if="destinationCreators.length > 0">
<div class="section-title text-primary" data-test="destination-import-creation-title">Destination Creation</div>
<div
class="section-title text-primary"
data-test="destination-import-creation-title"
>
Destination Creation
</div>
<div
class="error-list"
v-for="(val, index) in destinationCreators"
@@ -518,6 +580,7 @@ export default defineComponent({
const activeTab = ref("import_json_file");
const splitterModel = ref(60);
const filteredTemplates = ref<string[]>([]);
const filteredActions = ref<string[]>([]);
const userSelectedSkipTlsVerify = ref<boolean[]>([]);
@@ -525,8 +588,10 @@ export default defineComponent({
return props.templates
.filter((template: any) => {
// Get the current destination type
const currentDestinationType = jsonArrayOfObj.value[destinationErrorsToDisplay.value.length - 1]?.type;
const currentDestinationType =
jsonArrayOfObj.value[destinationErrorsToDisplay.value.length - 1]
?.type;
if (currentDestinationType === "email" && template.type === "email") {
return true;
} else if (currentDestinationType !== "email") {
@@ -539,6 +604,8 @@ export default defineComponent({
const userSelectedEmails = ref([]);
const userSelectedActionId = ref([]);
const updateDestinationType = (type: any, index: number) => {
jsonArrayOfObj.value[index].type = type;
jsonStr.value = JSON.stringify(jsonArrayOfObj.value, null, 2);
@@ -562,9 +629,14 @@ export default defineComponent({
jsonStr.value = JSON.stringify(jsonArrayOfObj.value, null, 2);
};
const updateDestinationAction = (id: string, index: number) => {
jsonArrayOfObj.value[index].action_id = id;
jsonStr.value = JSON.stringify(jsonArrayOfObj.value, null, 2);
};
const updateDestinationEmails = (emails: string, index: number) => {
// Split comma-separated emails and trim whitespace
const emailArray = emails.split(',').map(email => email.trim());
const emailArray = emails.split(",").map((email) => email.trim());
jsonArrayOfObj.value[index].emails = emailArray;
jsonStr.value = JSON.stringify(jsonArrayOfObj.value, null, 2);
};
@@ -586,15 +658,53 @@ export default defineComponent({
update(() => {
const needle = val.toLowerCase();
filteredTemplates.value = getFormattedTemplates.value.filter(
(template: string) => template.toLowerCase().includes(needle)
(template: string) => template.toLowerCase().includes(needle),
);
});
};
const filterActions = (val: string, update: Function) => {
if (val === "") {
update(() => {
filteredActions.value = store.state.organizationData.actions
.filter(
(action: any) => action.execution_details_type === "service",
)
.map((action: any) => ({
label: action.name,
value: action.id,
}));
});
return;
}
update(() => {
const needle = val.toLowerCase();
getServiceActions()
.map((action: any) => ({
label: action.name,
value: action.id,
}))
.filter(
(action: { name: string; id: string }) =>
action.name.toLowerCase().includes(needle) ||
action.id.toLowerCase().includes(needle),
);
});
};
const getServiceActions = () => {
return (
store.state.organizationData.actions.filter(
(action: any) => action.execution_details_type === "service",
) || []
);
};
watch(jsonFiles, async (newVal: any, oldVal: any) => {
if (newVal && newVal.length > 0) {
let combinedJson: any[] = [];
for (const file of newVal) {
try {
const result: any = await new Promise((resolve) => {
@@ -603,7 +713,9 @@ export default defineComponent({
try {
const parsedJson = JSON.parse(e.target.result);
// Convert to array if it's a single object
const jsonArray = Array.isArray(parsedJson) ? parsedJson : [parsedJson];
const jsonArray = Array.isArray(parsedJson)
? parsedJson
: [parsedJson];
resolve(jsonArray);
} catch (error) {
q.notify({
@@ -617,13 +729,13 @@ export default defineComponent({
};
reader.readAsText(file);
});
combinedJson = [...combinedJson, ...result];
} catch (error) {
console.error('Error reading file:', error);
console.error("Error reading file:", error);
}
}
// Update the refs with combined JSON data
jsonArrayOfObj.value = combinedJson;
jsonStr.value = JSON.stringify(combinedJson, null, 2);
@@ -697,7 +809,9 @@ export default defineComponent({
throw new Error("JSON string is empty");
} else {
const parsedJson = JSON.parse(jsonStr.value);
jsonArrayOfObj.value = Array.isArray(parsedJson) ? parsedJson : [parsedJson];
jsonArrayOfObj.value = Array.isArray(parsedJson)
? parsedJson
: [parsedJson];
}
} catch (e: any) {
q.notify({
@@ -801,10 +915,11 @@ export default defineComponent({
});
}
if (
input.type == "http" && (!input.method ||
(input.method !== "post" &&
input.method !== "get" &&
input.method !== "put"))
input.type == "http" &&
(!input.method ||
(input.method !== "post" &&
input.method !== "get" &&
input.method !== "put"))
) {
destinationErrors.push({
message: `Destination - ${index} 'method' is required and should be either 'post', 'get', or 'put'`,
@@ -828,22 +943,39 @@ export default defineComponent({
});
}
// Check type for email and it should be present only for email
if (input.type != "email" && input.type != "http") {
const availableActions = getServiceActions().map(
(action: any) => action.id,
);
if (
input.type === "action" &&
!availableActions.includes(input.action_id)
) {
destinationErrors.push({
message: `Destination - ${index} 'type' should be email or http`,
field: "type",
message: `Destination - ${index} action "${input.action_id}" does not exist for type "${input.type}"`,
field: "action_id",
});
}
// Check type for email and it should be present only for email
if (
input.type != "email" &&
input.type != "http" &&
input.type !== "action"
) {
destinationErrors.push({
message: `Destination - ${index} 'type' should be email, http or action`,
field: "type",
});
}
// Check if 'method' is required for both webhook and email
// Check if 'skip_tls_verify' is required for both webhook and email, and it should be a boolean
if (
input.type == "http" && (input.skip_tls_verify === undefined ||
typeof input.skip_tls_verify !== "boolean")
input.type == "http" &&
(input.skip_tls_verify === undefined ||
typeof input.skip_tls_verify !== "boolean")
) {
destinationErrors.push({
message: `Destination - ${index} 'skip_tls_verify' is required and should be a boolean value`,
@@ -851,27 +983,24 @@ export default defineComponent({
});
}
if(input.type == 'http' && input.headers != undefined) {
if(typeof input.headers !== 'object') {
if (input.type == "http" && input.headers != undefined) {
if (typeof input.headers !== "object") {
destinationErrors.push(
`Destination - ${index} 'headers' should be an object for webhook`,
);
}
}
if (input.type === "email" && input.hasOwnProperty('headers') && Object.keys(input.headers).length !== 0) {
if (
input.type === "email" &&
input.hasOwnProperty("headers") &&
Object.keys(input.headers).length !== 0
) {
destinationErrors.push(
`Destination - ${index} 'headers' should not be provided for email`,
);
}
// Check if 'name' is required for both webhook and email
if (!input.name || typeof input.name !== "string") {
destinationErrors.push(
@@ -879,7 +1008,6 @@ export default defineComponent({
);
}
// 'emails' should be required for email type and should be an array of strings
if (input.type === "email") {
if (
@@ -901,15 +1029,12 @@ export default defineComponent({
);
}
// Log all destination errors at the end if any exist
if (destinationErrors.length > 0) {
destinationErrorsToDisplay.value.push(destinationErrors);
return false;
}
// If all validations pass
return true;
};
@@ -943,7 +1068,7 @@ export default defineComponent({
// Emit update after each successful creation
emit("update:destinations");
return true;
} catch (error: any) {
destinationCreators.value.push({
@@ -967,6 +1092,7 @@ export default defineComponent({
};
return {
store,
t,
jsonStr,
importJson,
@@ -1003,6 +1129,10 @@ export default defineComponent({
filteredTemplates,
userSelectedSkipTlsVerify,
updateSkipTlsVerify,
userSelectedActionId,
filterActions,
filteredActions,
updateDestinationAction,
};
},
components: {
+18 -1
View File
@@ -125,7 +125,20 @@
>
{{ t("common.duration") + " *" }}
</div>
<DateTime label="Start Time" class="q-py-xs tw-w-full" />
<DateTime
label="Start Time"
class="q-py-xs tw-w-full"
auto-apply
:default-type="dateTime.type"
:default-absolute-time="{
startTime: dateTime.startTime,
endTime: dateTime.endTime,
}"
:default-relative-time="dateTime.relativeTimePeriod"
data-test="logs-search-bar-date-time-dropdown"
@on:date-change="updateDateTime"
/>
</div>
<div
@@ -493,6 +506,10 @@ const updateStreams = async (resetStream = true) => {
.finally(() => (isFetchingStreams.value = false));
};
const updateDateTime = (value: any) => {
dateTime.value = value;
};
const getResults = async () => {
loading.value.events = true;
@@ -68,7 +68,7 @@ const useEnterpriseRoutes = () => {
if (config.isCloud == "true" || config.isEnterprise == "true") {
routes.push({
path: "action-scripts",
path: "actions",
name: "actionScripts",
component: ActionScipts,
beforeEnter(to: any, from: any, next: any) {
+29 -10
View File
@@ -684,13 +684,6 @@ export default defineComponent({
link: "/alerts",
name: "alertList",
},
{
title: t("menu.actions"),
icon: outlinedCode,
link: "/action-scripts",
name: "actionScripts",
hide: !isActionsEnabled.value,
},
{
title: t("menu.ingestion"),
icon: outlinedFilterAlt,
@@ -822,8 +815,9 @@ export default defineComponent({
store.dispatch("setHiddenMenus", disableMenus);
linksList.value = linksList.value.filter((link) => {
const hide = link.hide === undefined ? false : link.hide; // Handle unknown hide values
linksList.value = linksList.value.filter((link: any) => {
const hide = link.hide === undefined ? false : link.hide;
return !disableMenus.has(link.name) && !hide;
});
};
@@ -1141,10 +1135,14 @@ export default defineComponent({
linksList.value = mainLayoutMixin
.setup()
.leftNavigationLinks(linksList, t);
filterMenus();
}
store.dispatch("setConfig", res.data);
await nextTick();
updateActionsMenu();
filterMenus();
// if rum enabled then setUser to capture session details.
if (res.data.rum.enabled) {
setRumUser();
@@ -1192,6 +1190,27 @@ export default defineComponent({
window.open(slackURL, "_blank");
};
const updateActionsMenu = () => {
if (isActionsEnabled.value) {
const alertIndex = linksList.value.findIndex(
(link) => link.name === "alertList",
);
const actionExists = linksList.value.some(
(link) => link.name === "actionScripts",
);
if (alertIndex !== -1 && !actionExists) {
linksList.value.splice(alertIndex + 1, 0, {
title: t("menu.actions"),
icon: outlinedCode,
link: "/actions",
name: "actionScripts",
});
}
}
};
return {
t,
router,
+5 -4
View File
@@ -1124,15 +1124,16 @@
"save": "Save",
"name": "Action Name",
"header": "Actions",
"add": "Create Action",
"add": "Add Action",
"deleteTitle": "Delete",
"deleteText": "Are you sure you want to delete file ?",
"uploadCodeZip": "Upload Code Zip",
"uploadCodeZip": "Upload Code ZIP File",
"update":"Update Action",
"type":"Execution Type",
"serviceAccount":"Service Account",
"search": "Search Actions",
"timezone": "Timezone"
"search": "Search Action",
"timezone": "Timezone",
"zipFileRequired": "ZIP File is required!"
},
"cipherKey": {
"header": "Cipher Keys",