mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-04 16:09:05 +00:00
### **PR Type**
Enhancement
___
### **Description**
- Upgrade Tailwind CSS from v3.4.4 to v4.1.18
- Bump rudder-sdk-js from 2.48.43 to 2.52.7
___
<details> <summary><h3> File Walkthrough</h3></summary>
<table><thead><tr><th></th><th align="left">Relevant
files</th></tr></thead><tbody><tr><td><strong>Dependencies</strong></td><td><table>
<tr>
<td>
<details>
<summary><strong>package.json</strong><dd><code>Bump dependency versions
in package.json</code>
</dd></summary>
<hr>
web/package.json
<ul><li>Updated <code>tailwindcss</code> version to ^4.1.18<br> <li>
Updated <code>rudder-sdk-js</code> version to ^2.52.7</ul>
</details>
</td>
<td><a
href="https://github.com/openobserve/openobserve/pull/9773/files#diff-b861012a5dd72b8a9f3281b7cf09f5a779c98569d040b1bbc1db50f1b15e7cce">+2/-2</a>
</td>
</tr>
</table></td></tr></tr></tbody></table>
</details>
___
606 lines
18 KiB
Vue
606 lines
18 KiB
Vue
<!-- 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>
|
|
<div class="tw:w-full tw:h-full tw:pr-[0.625rem] tw:pb-[0.625rem]">
|
|
<div class="card-container tw:mb-[0.8rem]">
|
|
<div class="tw:flex tw:items-center tw:justify-between tw:py-3 tw:pl-4 tw:pr-2 tw:h-[68px]">
|
|
<FunctionsToolbar
|
|
v-model:name="formData.name"
|
|
ref="functionsToolbarRef"
|
|
:disable-name="beingUpdated"
|
|
@test="onTestFunction"
|
|
@save="onSubmit"
|
|
@back="closeAddFunction"
|
|
@cancel="cancelAddFunction"
|
|
@open:chat="openChat"
|
|
:is-add-function-component="isAddFunctionComponent"
|
|
class="tw:pr-4"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="tw:flex">
|
|
|
|
|
|
<div
|
|
class="tw:flex tw:overflow-auto "
|
|
:style="{
|
|
width: store.state.isAiChatEnabled && !isAddFunctionComponent ? '75%' : '100%',
|
|
}"
|
|
>
|
|
<q-splitter
|
|
v-model="splitterModel"
|
|
:limits="[30, Infinity]"
|
|
class="tw:overflow-hidden tw:w-full"
|
|
reverse
|
|
>
|
|
<template v-slot:before>
|
|
<div class="q-px-md q-pt-sm q-pb-md tw:h-max card-container tw:h-[calc(100vh-128px)]">
|
|
<q-form id="addFunctionForm" ref="addJSTransformForm">
|
|
<div class="add-function-name-input q-pb-sm o2-input">
|
|
<FullViewContainer
|
|
name="function"
|
|
v-model:is-expanded="expandState.functions"
|
|
:label="t('function.jsfunction') + '*'"
|
|
class="tw:mt-1"
|
|
/>
|
|
<div
|
|
v-show="expandState.functions"
|
|
class="tw:border-[1px] tw:border-gray-200"
|
|
>
|
|
<query-editor
|
|
data-test="logs-vrl-function-editor"
|
|
ref="editorRef"
|
|
editor-id="add-function-editor"
|
|
class="monaco-editor"
|
|
:style="{ height: `calc(100vh - (180px + ${heightOffset}px))` }"
|
|
v-model:query="formData.function"
|
|
language="vrl"
|
|
/>
|
|
</div>
|
|
<div class="text-subtitle2">
|
|
<div v-if="vrlFunctionError">
|
|
<FullViewContainer
|
|
name="function"
|
|
v-model:is-expanded="expandState.functionError"
|
|
:label="t('function.errorDetails')"
|
|
labelClass="tw:text-red-600"
|
|
/>
|
|
<div
|
|
v-if="expandState.functionError"
|
|
class="q-px-sm q-pb-sm"
|
|
:class="
|
|
store.state.theme === 'dark'
|
|
? 'bg-grey-10'
|
|
: 'bg-grey-2'
|
|
"
|
|
>
|
|
<pre class="q-my-none" style="white-space: pre-wrap">{{
|
|
vrlFunctionError
|
|
}}</pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</q-form>
|
|
</div>
|
|
</template>
|
|
<template v-slot:after>
|
|
<div class="q-px-md q-pt-sm q-pb-md tw:h-max q-ml-sm card-container">
|
|
<TestFunction
|
|
ref="testFunctionRef"
|
|
:vrlFunction="formData"
|
|
@function-error="handleFunctionError"
|
|
:heightOffset="heightOffset"
|
|
@sendToAiChat="sendToAiChat"
|
|
/>
|
|
</div>
|
|
</template>
|
|
</q-splitter>
|
|
</div>
|
|
<div v-if="store.state.isAiChatEnabled && !isAddFunctionComponent" style="width: 25%; max-width: 100%; min-width: 75px; " :class="store.state.theme == 'dark' ? 'dark-mode-chat-container' : 'light-mode-chat-container'" >
|
|
<O2AIChat :style="{
|
|
height: `calc(100vh - (112px + ${heightOffset}px))`
|
|
}" :is-open="store.state.isAiChatEnabled" @close="store.state.isAiChatEnabled = false" :aiChatInputContext="aiChatInputContext" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<confirm-dialog
|
|
:title="confirmDialogMeta.title"
|
|
:message="confirmDialogMeta.message"
|
|
@update:ok="confirmDialogMeta.onConfirm()"
|
|
@update:cancel="resetConfirmDialog"
|
|
v-model="confirmDialogMeta.show"
|
|
/>
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
import {
|
|
defineComponent,
|
|
ref,
|
|
onMounted,
|
|
computed,
|
|
watch,
|
|
onUnmounted,
|
|
defineAsyncComponent,
|
|
nextTick,
|
|
} from "vue";
|
|
|
|
import jsTransformService from "../../services/jstransform";
|
|
import { useI18n } from "vue-i18n";
|
|
import { useStore } from "vuex";
|
|
import { useQuasar } from "quasar";
|
|
import segment from "../../services/segment_analytics";
|
|
import TestFunction from "@/components/functions/TestFunction.vue";
|
|
import FunctionsToolbar from "@/components/functions/FunctionsToolbar.vue";
|
|
import FullViewContainer from "@/components/functions/FullViewContainer.vue";
|
|
import ConfirmDialog from "@/components/ConfirmDialog.vue";
|
|
import { onBeforeRouteLeave } from "vue-router";
|
|
import O2AIChat from "@/components/O2AIChat.vue";
|
|
import { useRouter } from "vue-router";
|
|
import { useReo } from "@/services/reodotdev_analytics";
|
|
const defaultValue: any = () => {
|
|
return {
|
|
name: "",
|
|
function: "",
|
|
params: "row",
|
|
transType: "0",
|
|
};
|
|
};
|
|
|
|
let callTransform: Promise<{ data: any }>;
|
|
|
|
export default defineComponent({
|
|
name: "ComponentAddUpdateFunction",
|
|
props: {
|
|
modelValue: {
|
|
type: Object,
|
|
default: () => defaultValue(),
|
|
},
|
|
isUpdated: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
heightOffset: {
|
|
type: Number,
|
|
default: 0,
|
|
},
|
|
},
|
|
components: {
|
|
QueryEditor: defineAsyncComponent(
|
|
() => import("@/components/CodeQueryEditor.vue"),
|
|
),
|
|
FunctionsToolbar,
|
|
FullViewContainer,
|
|
TestFunction,
|
|
ConfirmDialog,
|
|
O2AIChat,
|
|
},
|
|
emits: ["update:list", "cancel:hideform", "sendToAiChat"],
|
|
setup(props, { emit }) {
|
|
const store: any = useStore();
|
|
const router = useRouter();
|
|
const { track } = useReo();
|
|
|
|
// let beingUpdated: boolean = false;
|
|
const addJSTransformForm: any = ref(null);
|
|
const disableColor: any = ref("");
|
|
const formData: any = ref({
|
|
name: "",
|
|
function: "",
|
|
params: "row",
|
|
transType: "0",
|
|
});
|
|
const indexOptions = ref([]);
|
|
const { t } = useI18n();
|
|
const $q = useQuasar();
|
|
const editorRef: any = ref(null);
|
|
let editorobj: any = null;
|
|
const streams: any = ref({});
|
|
const isFetchingStreams = ref(false);
|
|
const testFunctionRef = ref<typeof TestFunction>();
|
|
const functionsToolbarRef = ref<typeof FunctionsToolbar>();
|
|
const splitterModel = ref(50);
|
|
const aiChatInputContext = ref("");
|
|
const confirmDialogMeta = ref({
|
|
title: "",
|
|
message: "",
|
|
show: false,
|
|
onConfirm: () => {},
|
|
data: null,
|
|
});
|
|
|
|
const expandState = ref({
|
|
functions: true,
|
|
functionError: false,
|
|
});
|
|
|
|
const vrlFunctionError = ref("");
|
|
|
|
let compilationErr = ref("");
|
|
|
|
const beingUpdated = computed(() => props.isUpdated);
|
|
|
|
const streamTypes = ["logs", "metrics", "traces"];
|
|
|
|
const isFunctionDataChanged = ref(false);
|
|
const isAddFunctionComponent = computed(() => router.currentRoute.value.path.includes('functions'))
|
|
|
|
|
|
watch(
|
|
() => formData.value.name + formData.value.function,
|
|
() => {
|
|
isFunctionDataChanged.value = true;
|
|
},
|
|
);
|
|
|
|
onMounted(() => {
|
|
window.addEventListener("beforeunload", beforeUnloadHandler);
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
window.removeEventListener("beforeunload", beforeUnloadHandler);
|
|
});
|
|
|
|
const beforeUnloadHandler = (e: any) => {
|
|
//check is data updated or not
|
|
if (isFunctionDataChanged.value) {
|
|
// Display a confirmation message
|
|
const confirmMessage = t("dashboard.unsavedMessage"); // Some browsers require a return statement to display the message
|
|
e.returnValue = confirmMessage;
|
|
return confirmMessage;
|
|
}
|
|
return;
|
|
};
|
|
|
|
let forceSkipBeforeUnloadListener = false;
|
|
|
|
onBeforeRouteLeave((to, from, next) => {
|
|
// check if it is a force navigation, then allow
|
|
if (forceSkipBeforeUnloadListener) {
|
|
next();
|
|
return;
|
|
}
|
|
// else continue to warn user
|
|
const actions = ["add", "update"];
|
|
|
|
if (
|
|
from.path === "/pipeline/functions" &&
|
|
actions.includes((from.query?.action as string) || "none") &&
|
|
isFunctionDataChanged.value
|
|
) {
|
|
const confirmMessage = t("pipeline.unsavedMessage");
|
|
if (window.confirm(confirmMessage)) {
|
|
// User confirmed, allow navigation
|
|
next();
|
|
} else {
|
|
// User canceled, prevent navigation
|
|
next(false);
|
|
}
|
|
} else {
|
|
// No unsaved changes or not leaving the edit route, allow navigation
|
|
next();
|
|
}
|
|
});
|
|
|
|
const editorUpdate = (e: any) => {
|
|
formData.value.function = e.target.value;
|
|
};
|
|
const prefixCode = ref("");
|
|
const suffixCode = ref("");
|
|
|
|
const isValidParam = () => {
|
|
const methodPattern = /^[A-Za-z0-9]+(?:,[A-Za-z0-9]+)*$/g;
|
|
return methodPattern.test(formData.value.params) || "Invalid params.";
|
|
};
|
|
|
|
const isValidMethodName = () => {
|
|
const methodPattern = /^[$A-Z_][0-9A-Z_$]*$/i;
|
|
return (
|
|
methodPattern.test(formData.value.name) || "Invalid Function name."
|
|
);
|
|
};
|
|
const updateEditorContent = () => {
|
|
if (formData.value.transType == "1") {
|
|
prefixCode.value = `function(row)`;
|
|
suffixCode.value = `
|
|
end`;
|
|
} else {
|
|
prefixCode.value = ``;
|
|
suffixCode.value = ``;
|
|
}
|
|
|
|
formData.value.function = `${prefixCode.value}
|
|
${formData.value.function}
|
|
${suffixCode.value}`;
|
|
};
|
|
|
|
const isValidFnName = () => {
|
|
return formData.value.name.trim().length > 0;
|
|
};
|
|
|
|
const onSubmit = () => {
|
|
if (!functionsToolbarRef.value) return;
|
|
|
|
functionsToolbarRef.value.addFunctionForm
|
|
.validate()
|
|
.then((valid: any) => {
|
|
if (!valid) {
|
|
return false;
|
|
}
|
|
|
|
const loadingNotification = $q.notify({
|
|
spinner: true,
|
|
message: "Please wait...",
|
|
timeout: 0,
|
|
});
|
|
|
|
try {
|
|
if (!beingUpdated.value) {
|
|
formData.value.transType = parseInt(formData.value.transType);
|
|
//trans type is lua remove params from form
|
|
if (formData.value.transType == 1) {
|
|
formData.value.params = "";
|
|
}
|
|
|
|
callTransform = jsTransformService.create(
|
|
store.state.selectedOrganization.identifier,
|
|
formData.value,
|
|
);
|
|
} else {
|
|
formData.value.transType = parseInt(formData.value.transType);
|
|
//trans type is lua remove params from form
|
|
if (formData.value.transType == 1) {
|
|
formData.value.params = "";
|
|
}
|
|
|
|
callTransform = jsTransformService.update(
|
|
store.state.selectedOrganization.identifier,
|
|
formData.value,
|
|
);
|
|
}
|
|
|
|
forceSkipBeforeUnloadListener = true;
|
|
|
|
callTransform
|
|
.then((res: { data: any }) => {
|
|
const data = res.data;
|
|
const _formData: any = { ...formData.value };
|
|
formData.value = { ...defaultValue() };
|
|
|
|
emit("update:list", _formData);
|
|
addJSTransformForm?.value?.resetValidation();
|
|
|
|
loadingNotification();
|
|
$q.notify({
|
|
type: "positive",
|
|
message: res.data.message || "Function saved successfully",
|
|
});
|
|
})
|
|
.catch((err) => {
|
|
compilationErr.value = err?.response?.data["message"];
|
|
$q.notify({
|
|
type: "negative",
|
|
message:
|
|
err.response?.data?.message ?? "Function creation failed",
|
|
});
|
|
loadingNotification();
|
|
});
|
|
} catch (error) {
|
|
console.error("Error while saving function:", error);
|
|
loadingNotification();
|
|
}
|
|
|
|
segment.track("Button Click", {
|
|
button: "Save Function",
|
|
user_org: store.state.selectedOrganization.identifier,
|
|
user_id: store.state.userInfo.email,
|
|
function_name: formData.value.name,
|
|
page: "Add/Update Function",
|
|
});
|
|
track("Button Click", {
|
|
button: "Save Function",
|
|
page: "Add Function"
|
|
});
|
|
});
|
|
};
|
|
|
|
const onTestFunction = () => {
|
|
if (testFunctionRef.value) testFunctionRef.value.testFunction();
|
|
};
|
|
|
|
const handleFunctionError = (err: string) => {
|
|
vrlFunctionError.value = err;
|
|
};
|
|
|
|
const closeAddFunction = () => {
|
|
if (isFunctionDataChanged.value) {
|
|
confirmDialogMeta.value.show = true;
|
|
confirmDialogMeta.value.title = t("common.unsavedTitle");
|
|
confirmDialogMeta.value.message = t("common.unsavedMessage");
|
|
confirmDialogMeta.value.onConfirm = () => {
|
|
emit("cancel:hideform");
|
|
resetConfirmDialog();
|
|
};
|
|
} else {
|
|
emit("cancel:hideform");
|
|
}
|
|
};
|
|
|
|
const cancelAddFunction = () => {
|
|
if (isFunctionDataChanged.value) {
|
|
confirmDialogMeta.value.show = true;
|
|
confirmDialogMeta.value.title = t("common.cancelTitle");
|
|
confirmDialogMeta.value.message = t("common.cancelMessage");
|
|
confirmDialogMeta.value.onConfirm = () => {
|
|
emit("cancel:hideform");
|
|
resetConfirmDialog();
|
|
};
|
|
} else {
|
|
emit("cancel:hideform");
|
|
}
|
|
track("Button Click", {
|
|
button: "Cancel Function",
|
|
page: "Add Function"
|
|
});
|
|
};
|
|
|
|
const resetConfirmDialog = () => {
|
|
confirmDialogMeta.value.show = false;
|
|
confirmDialogMeta.value.title = "";
|
|
confirmDialogMeta.value.message = "";
|
|
confirmDialogMeta.value.onConfirm = () => {};
|
|
confirmDialogMeta.value.data = null;
|
|
};
|
|
const openChat = (val: boolean) => {
|
|
store.dispatch("setIsAiChatEnabled", val);
|
|
};
|
|
|
|
const sendToAiChat = (value: any) => {
|
|
//this is for when user in pipeline add function page and click on ai chat button
|
|
//here we reset the value befoere setting it because if user clears the input then again click on the same value it wont trigger the watcher that is there in the child component
|
|
//so to force trigger we do this
|
|
aiChatInputContext.value = '';
|
|
nextTick(() => {
|
|
aiChatInputContext.value = value;
|
|
});
|
|
store.dispatch("setIsAiChatEnabled", true);
|
|
//this is for when user in functions page and click on ai chat button
|
|
emit("sendToAiChat", value);
|
|
};
|
|
|
|
return {
|
|
t,
|
|
$q,
|
|
emit,
|
|
disableColor,
|
|
beingUpdated,
|
|
formData,
|
|
addJSTransformForm,
|
|
store,
|
|
compilationErr,
|
|
indexOptions,
|
|
editorRef,
|
|
editorobj,
|
|
prefixCode,
|
|
suffixCode,
|
|
editorUpdate,
|
|
updateEditorContent,
|
|
streamTypes,
|
|
isFetchingStreams,
|
|
isValidParam,
|
|
isValidMethodName,
|
|
onSubmit,
|
|
expandState,
|
|
testFunctionRef,
|
|
onTestFunction,
|
|
handleFunctionError,
|
|
vrlFunctionError,
|
|
functionsToolbarRef,
|
|
splitterModel,
|
|
closeAddFunction,
|
|
confirmDialogMeta,
|
|
resetConfirmDialog,
|
|
cancelAddFunction,
|
|
openChat,
|
|
isAddFunctionComponent,
|
|
sendToAiChat,
|
|
aiChatInputContext
|
|
};
|
|
},
|
|
created() {
|
|
this.formData = { ...defaultValue(), ...this.modelValue };
|
|
this.beingUpdated = this.isUpdated;
|
|
|
|
if (
|
|
this.modelValue &&
|
|
this.modelValue.name != undefined &&
|
|
this.modelValue.name != ""
|
|
) {
|
|
this.beingUpdated = true;
|
|
this.disableColor = "grey-5";
|
|
this.formData = this.modelValue;
|
|
}
|
|
},
|
|
});
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
.monaco-editor {
|
|
width: 100%;
|
|
border-radius: 5px;
|
|
}
|
|
|
|
.add-function-name-input {
|
|
:deep(.q-field--dense .q-field__control) {
|
|
height: 36px;
|
|
min-height: auto;
|
|
border-radius: 3px;
|
|
|
|
.q-field__control-container {
|
|
height: 32px;
|
|
|
|
.q-field__native {
|
|
height: 32px !important;
|
|
}
|
|
}
|
|
|
|
.q-field__marginal {
|
|
height: 32px;
|
|
min-height: auto;
|
|
}
|
|
}
|
|
|
|
:deep(.q-field__bottom) {
|
|
padding-top: 4px !important;
|
|
min-height: auto;
|
|
}
|
|
}
|
|
|
|
.function-stream-select-input {
|
|
:deep(.q-field--auto-height .q-field__control) {
|
|
height: 32px;
|
|
min-height: auto;
|
|
|
|
.q-field__control-container {
|
|
height: 32px;
|
|
|
|
.q-field__native {
|
|
min-height: 32px !important;
|
|
height: 32px !important;
|
|
}
|
|
}
|
|
|
|
.q-field__marginal {
|
|
height: 32px;
|
|
min-height: auto;
|
|
}
|
|
}
|
|
}
|
|
|
|
.functions-duration-input {
|
|
:deep(.date-time-button) {
|
|
width: 100%;
|
|
}
|
|
}
|
|
</style>
|
|
<style>
|
|
.no-case .q-field__native span {
|
|
text-transform: none !important;
|
|
}
|
|
</style>
|