Files
openobserve/web/src/components/functions/FunctionList.vue
T
Ashish KolheandClaude Opus 4.5 ce09cd0a4d feat: add js for claim parser (#9873)
### **User description**
Design at : security/sso-claim-parser/configuration.md


___

### **PR Type**
Enhancement, Tests


___

### **Description**
- Introduce rquickjs-based JS runtime for functions

- Add compile_js_function and apply_js_fn utilities

- Extend pipeline to support JS via CompiledFunctionRuntime

- Update web UI: select JS/VRL transforms, show JS errors


___

### Diagram Walkthrough


```mermaid
flowchart LR
  A["FunctionNode"] -->|JS| B["compile_js_function"]
  A -->|VRL| C["compile_vrl_function"]
  B --> D["apply_js_fn"]
  C --> E["apply_vrl_fn"]
  D --> F["send to children"]
  E --> F
```



<details> <summary><h3> File Walkthrough</h3></summary>

<table><thead><tr><th></th><th align="left">Relevant
files</th></tr></thead><tbody><tr><td><strong>Enhancement</strong></td><td><details><summary>13
files</summary><table>
<tr>
<td><strong>function.ts</strong><dd><code>Add optional `trans_type` to
TestFunctionPayload</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/9873/files#diff-c6766f6c671adce05f9eef5d4a57a32371fa7da35628ef5c142fa503d191cb16">+16/-0</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>js.rs</strong><dd><code>Introduce JS runtime, compile and
apply functions</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; </dd></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/9873/files#diff-88e490bed5fa6c9c8e0aa2738b95c5284bb97899298f54b9cd587713e3ef0109">+756/-0</a>&nbsp;
</td>

</tr>

<tr>
<td><strong>batch_execution.rs</strong><dd><code>Extend pipeline
execution to handle JS functions</code>&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/9873/files#diff-3f6824a6eddaf1603e5cb0f1a4311ac6a45e1ffb967266ccc1f0bba944625e69">+465/-96</a></td>

</tr>

<tr>
<td><strong>functions.rs</strong><dd><code>Validate and test-run JS
transforms, restrict to _meta</code>&nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/9873/files#diff-ed5611a9aa6b5c033e70d7631ba72fafad3cc842672077a7fcf965d11a4fef23">+319/-35</a></td>

</tr>

<tr>
<td><strong>mod.rs</strong><dd><code>Forbid JavaScript functions in
pipelines globally</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/9873/files#diff-c186c796a7bdfd9d1301b1429df11456430f1d0d4758783810b9e2e44384e3b2">+39/-1</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>jwt.rs</strong><dd><code>Add JS claim parser execution
function</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; </dd></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/9873/files#diff-232cab74f4cbf135af424744272e17732fbda8ac005d33c0acb4597e65479d02">+25/-2</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>mod.rs</strong><dd><code>Wrap JS compile/apply in ingestion
API</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/9873/files#diff-7c7b5bca85f0ced652965cae66540c96f56dfcca4f4c16b77bf7b45aac203d1b">+24/-1</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>mod.rs</strong><dd><code>Include `trans_type` in
test_function API payload</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/9873/files#diff-a5e1b3644cef9feb689dfe66382e44e5434ecadbff3469127346a333d4733102">+8/-3</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>AddFunction.vue</strong><dd><code>UI: add JS/VRL transform
type selector</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/9873/files#diff-ebfd6c538f3cd3ecb9ab17550fd76e2e89e7a1abccc83a7505077e4af3eee110">+42/-25</a>&nbsp;
</td>

</tr>

<tr>
<td><strong>FunctionsToolbar.vue</strong><dd><code>Toolbar: transform
type radio buttons</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/9873/files#diff-8283d2ff9c4a66b4b59eac306672aca2d54655a462e2ddb88e14e039d79dcdaf">+51/-3</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>DomainManagement.vue</strong><dd><code>Track claim-parser
function changes for save button</code>&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/9873/files#diff-5e47cd01fcb63f7ef27dc64d2ecf66c39e17097e29944951c3ae1bfe715bb97c">+17/-9</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>TestFunction.vue</strong><dd><code>Support JS error
messaging in test UI</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/9873/files#diff-b9af9a2d1bf04d23422c26a13e6a763c6953c8713357a2f64ed8073ce6c926a3">+15/-5</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>PipelineEditor.vue</strong><dd><code>Filter out JS functions
in pipeline editor UI</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/9873/files#diff-68c21f647336067eb79737ce9aca2d49632f2fa1c0878bcfdae55607e3473a15">+12/-4</a>&nbsp;
&nbsp; </td>

</tr>
</table></details></td></tr><tr><td><strong>Configuration
changes</strong></td><td><details><summary>2 files</summary><table>
<tr>
<td><strong>function.rs</strong><dd><code>Extend `Transform` model for
JS support</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/9873/files#diff-27c9f3d93723397a0e234bc45c9f543b43eff5fd518d76e735f78d95a58f3f1c">+16/-6</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>mod.rs</strong><dd><code>Register new `js` utils
module</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/9873/files#diff-2aa59ce4464e7dbabdb9367fc8bb8c1b489c914b8690577d4916870463ab12db">+1/-0</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>
</table></details></td></tr><tr><td><strong>Bug
fix</strong></td><td><details><summary>1 files</summary><table>
<tr>
<td><strong>errors.rs</strong><dd><code>Rename
`MemoryTableOverflowError` display message</code>&nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/9873/files#diff-bf45932466265b732f13d0cd9b9cffbaefbe37c5861ace9315449eeca9250713">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

</table></details></td></tr><tr><td><strong>Tests</strong></td><td><details><summary>3
files</summary><table>
<tr>
<td><strong>result-array-functions.spec.js</strong><dd><code>Add E2E
tests for `#ResultArray#` functions in UI</code>&nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/9873/files#diff-e9911625ad2bab76f2b760f0ad254791331993a6a874b96f6a11fc6d88038b10">+470/-0</a>&nbsp;
</td>

</tr>

<tr>
<td><strong>cleanup.spec.js</strong><dd><code>Clean up JS function test
data in UI tests</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/9873/files#diff-c214275a512fa285a7e4f2ab61dfcb42d71a0436b0e4241f94d7b45693587983">+31/-0</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>page-manager.js</strong><dd><code>Register `FunctionsPage`
in `PageManager`</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/9873/files#diff-565eab8b3c767ae50c9d05e591621d14f66b81af84d6b5ea6efe136c1c3933df">+6/-0</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

</table></details></td></tr><tr><td><strong>Dependencies</strong></td><td><details><summary>1
files</summary><table>
<tr>
<td><strong>Cargo.toml</strong><dd><code>Add `rquickjs` dependency for
JS runtime</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/9873/files#diff-2e9d962a08321605940b5a657135052fbcef87b5e360662bb527c96d9a615542">+2/-0</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>
</table></details></td></tr><tr><td><strong>Additional
files</strong></td><td><details><summary>18 files</summary><table>
<tr>
  <td><strong>test_functions_meta_org_restriction.py</strong></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/9873/files#diff-99ef1207d6ae6d7eeaed37d70c8cf0c4770ba78f6f51fbdc067d35492d2fe9ea">+401/-0</a>&nbsp;
</td>

</tr>

<tr>
  <td><strong>test_js_enrichment.py</strong></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/9873/files#diff-c5547d46bec60b888d947d1e9b7c5445aee72cf8e837182f7138e0fc903eedbb">+356/-0</a>&nbsp;
</td>

</tr>

<tr>
  <td><strong>test_js_result_array.py</strong></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/9873/files#diff-9ed1363a1f58c94e018bb2b0aaa9d08fa71f89e7e55a6ff6be4a82bbeed8985c">+390/-0</a>&nbsp;
</td>

</tr>

<tr>
  <td><strong>functionsPage.js</strong></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/9873/files#diff-7bab9cc2eb8f36d3ee5d0aab453c353ec6d6dfecbc401cf401322ff57faa8440">+326/-0</a>&nbsp;
</td>

</tr>

<tr>
  <td><strong>javascript-functions.spec.js</strong></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/9873/files#diff-bdae6529346a7222bdec19f75f490cb9d0f3578a260ba835225efcd27db3f64e">+314/-0</a>&nbsp;
</td>

</tr>

<tr>
  <td><strong>meta-org-js-restriction.spec.js</strong></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/9873/files#diff-16cc2373f6f1955f8344a59d5f2edcd6ce121c4fdee1d432b1f0ec8e27f25881">+483/-0</a>&nbsp;
</td>

</tr>

<tr>
  <td><strong>FunctionList.vue</strong></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/9873/files#diff-6ff9af50a1597504b7f0dca91e61cf30c800b4a17b4be2ed2c3a4f7a7b2d7068">+1/-0</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>de.json</strong></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/9873/files#diff-78f24ecb2dbcfc38fdf3ec22b2559a5b40d236ea689d1c55f5d3e87168310264">+30/-18</a>&nbsp;
</td>

</tr>

<tr>
  <td><strong>en.json</strong></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/9873/files#diff-892aa3d75ab6e129ef9116aa21bec1b9a15ba69ce5366d2a19128078c690d824">+15/-7</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td><strong>es.json</strong></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/9873/files#diff-ac2a2167b8b973e3f776f9fe3b43bef4576470d8ad33f3d7cfaff6edff0e5778">+30/-18</a>&nbsp;
</td>

</tr>

<tr>
  <td><strong>fr.json</strong></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/9873/files#diff-db80e762b88ddf183820c050cdc6b215e6b02b365b5e73f5152af46b0d26f1a2">+30/-18</a>&nbsp;
</td>

</tr>

<tr>
  <td><strong>it.json</strong></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/9873/files#diff-2de78074a5f887afddce60d694ead9322e6ab223eb70251c13819f3028bdc70d">+30/-18</a>&nbsp;
</td>

</tr>

<tr>
  <td><strong>ja.json</strong></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/9873/files#diff-092e49c5f9fbc1b539ab9c2866d108d0ce1d314022b4c294eb4611b285e7bbd3">+30/-18</a>&nbsp;
</td>

</tr>

<tr>
  <td><strong>ko.json</strong></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/9873/files#diff-50e27ca1b4825a5026f3a251bfcf79c97cab07cfade2ebd512d332453d707c48">+30/-18</a>&nbsp;
</td>

</tr>

<tr>
  <td><strong>nl.json</strong></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/9873/files#diff-a1706021687ae5d8a1c006c49eab3946174fecd895f9b70a89c2e76d97250906">+30/-18</a>&nbsp;
</td>

</tr>

<tr>
  <td><strong>pt.json</strong></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/9873/files#diff-38e804f64cb5e0e451849fc84e9b46e53c954a4a4263d51c18665ad45f4b4815">+30/-18</a>&nbsp;
</td>

</tr>

<tr>
  <td><strong>tr.json</strong></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/9873/files#diff-6f0f131e9e2591480c4c5d1eaffc72d02dc31cbd8d3f90d8f012f08cabac8818">+30/-18</a>&nbsp;
</td>

</tr>

<tr>
  <td><strong>zh.json</strong></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/9873/files#diff-8818055db58f83230aeea4ebfd4cec31d924e8c47b6f91298e356959ab25339e">+254/-242</a></td>

</tr>
</table></details></td></tr></tr></tbody></table>

</details>

___

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 22:47:35 +05:30

836 lines
25 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/>.
-->
<!-- eslint-disable vue/v-on-event-hyphenation -->
<!-- eslint-disable vue/attribute-hyphenation -->
<template>
<q-page>
<div v-if="!showAddJSTransformDialog">
<div class="tw:w-full tw:h-full tw:pr-[0.625rem] tw:pb-[0.625rem]">
<div class="card-container tw:mb-[0.625rem]">
<div class="tw:flex tw:items-center tw:justify-between tw:py-3 tw:px-4 tw:h-[68px]">
<div class="q-table__title tw:font-[600]">
{{ t("function.header") }}
</div>
<div class="q-ml-auto" data-test="functions-list-search-input">
<q-input
v-model="filterQuery"
borderless
dense
class="q-ml-auto no-border o2-search-input"
:placeholder="t('function.search')"
>
<template #prepend>
<q-icon class="o2-search-input-icon" name="search" />
</template>
</q-input>
</div>
<q-btn
class="q-ml-sm o2-primary-button tw:h-[36px]"
flat
no-caps
:label="t(`function.add`)"
data-test="function-list-add-function-btn"
@click="showAddUpdateFn({})"
/>
</div>
</div>
<div class="tw:w-full tw:h-full tw:pb-[0.625rem]">
<div class="card-container tw:h-[calc(100vh-127px)]">
<q-table
ref="qTable"
:rows="visibleRows"
:columns="columns"
row-key="name"
:pagination="pagination"
:filter="filterQuery"
selection="multiple"
v-model:selected="selectedFunctions"
style="width: 100%"
:style="hasVisibleRows
? 'width: 100%; height: calc(100vh - 130px)'
: 'width: 100%'"
class="o2-quasar-table o2-row-md o2-quasar-table-header-sticky"
>
<template #no-data>
<NoData />
</template>
<template v-slot:body-cell-actions="props">
<q-td :props="props">
<q-btn
padding="sm"
unelevated
size="sm"
icon="edit"
round
flat
:title="t('function.updateTitle')"
@click="showAddUpdateFn(props)"
>
</q-btn>
<q-btn
padding="sm"
unelevated
size="sm"
:icon="outlinedDelete"
round
flat
:title="t('function.delete')"
@click="showDeleteDialogFn(props)"
>
</q-btn>
<q-btn
padding="sm"
unelevated
size="sm"
:icon="outlinedAccountTree"
round
flat
:title="'Associated Pipelines'"
@click="getAssociatedPipelines(props)"
>
</q-btn>
</q-td>
</template>
<template v-slot:body-cell-function="props">
<q-td :props="props">
<q-tooltip>
<pre>{{ props.row.function }}</pre>
</q-tooltip>
<pre style="white-space: break-spaces">{{
props.row.function
}}</pre>
</q-td>
</template>
<template v-slot:body-selection="scope">
<q-checkbox v-model="scope.selected" size="sm" class="o2-table-checkbox" />
</template>
<template #bottom="scope">
<div class="tw:flex tw:items-center tw:justify-between tw:w-full tw:h-[48px]">
<div class="o2-table-footer-title tw:flex tw:items-center tw:w-[100px] tw:mr-md">
{{ resultTotal }} {{ t('function.header') }}
</div>
<q-btn
v-if="selectedFunctions.length > 0"
data-test="function-list-delete-functions-btn"
class="flex items-center q-mr-sm no-border o2-secondary-button tw:h-[36px]"
:class="
store.state.theme === 'dark'
? 'o2-secondary-button-dark'
: 'o2-secondary-button-light'
"
no-caps
dense
@click="openBulkDeleteDialog"
>
<q-icon name="delete" size="16px" />
<span class="tw:ml-2">Delete</span>
</q-btn>
<QTablePagination
:scope="scope"
:position="'bottom'"
:resultTotal="resultTotal"
:perPageOptions="perPageOptions"
@update:changeRecordPerPage="changePagination"
/>
</div>
</template>
<template v-slot:header="props">
<q-tr :props="props">
<!-- Adding this block to render the select-all checkbox -->
<q-th v-if="columns.length > 0" auto-width>
<q-checkbox
v-model="props.selected"
size="sm"
:class="store.state.theme === 'dark' ? 'o2-table-checkbox-dark' : 'o2-table-checkbox-light'"
class="o2-table-checkbox"
/>
</q-th>
<!-- Rendering the rest of the columns -->
<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>
</div>
</div>
<div v-else>
<AddFunction
v-model="formData"
:isUpdated="isUpdated"
class="q-pa-sm"
@update:list="refreshList"
@cancel:hideform="hideForm"
@sendToAiChat="sendToAiChat"
/>
</div>
<ConfirmDialog
title="Delete Transform"
message="Are you sure you want to delete transform?"
@update:ok="deleteFn"
@update:cancel="confirmDelete = false"
v-model="confirmDelete"
/>
<ConfirmDialog
title="Delete Functions"
:message="`Are you sure you want to delete ${selectedFunctions.length} function(s)?`"
@update:ok="bulkDeleteFunctions"
@update:cancel="confirmBulkDelete = false"
v-model="confirmBulkDelete"
/>
<q-dialog v-model="confirmForceDelete" persistent>
<q-card style="width: 40vw; max-height: 90vh; overflow-y: auto">
<q-card-section
class="text-h6 dialog-heading tw:flex tw:justify-between tw:items-center"
>
<div>
Pipelines Associated with
<strong> {{ selectedDelete.name }}</strong>
</div>
<q-icon
name="close"
size="18px"
@click="closeDialog"
style="cursor: pointer"
/>
</q-card-section>
<q-card-section>
<div
v-if="transformedPipelineList.length > 0"
class="pipeline-list-container"
>
<q-list class="scrollable-list">
<q-item
v-for="(pipeline, index) in transformedPipelineList"
:key="pipeline.value"
clickable
@click="onPipelineSelect(pipeline)"
>
<q-item-section>
{{ index + 1 }}. {{ pipeline.label }}
</q-item-section>
</q-item>
</q-list>
</div>
<div v-else>
<div class="text-h6 text-center">
No pipelines associated with this function
</div>
</div>
</q-card-section>
</q-card>
</q-dialog>
</q-page>
</template>
<script lang="ts">
import {
defineAsyncComponent,
defineComponent,
ref,
computed,
watch,
onMounted,
} from "vue";
import { useStore } from "vuex";
import { useRouter } from "vue-router";
import { useQuasar, type QTableProps } from "quasar";
import { useI18n } from "vue-i18n";
import QTablePagination from "../shared/grid/Pagination.vue";
import jsTransformService from "../../services/jstransform";
import NoData from "../shared/grid/NoData.vue";
import ConfirmDialog from "../ConfirmDialog.vue";
import segment from "../../services/segment_analytics";
import { getImageURL, verifyOrganizationStatus } from "../../utils/zincutils";
import {
outlinedDelete,
outlinedAccountTree,
} from "@quasar/extras/material-icons-outlined";
import useLogs from "@/composables/useLogs";
import { useReo } from "@/services/reodotdev_analytics";
export default defineComponent({
name: "functionList",
components: {
QTablePagination,
AddFunction: defineAsyncComponent(() => import("./AddFunction.vue")),
NoData,
ConfirmDialog,
},
emits: [
"updated:fields",
"update:changeRecordPerPage",
"update:maxRecordToReturn",
"sendToAiChat"
],
setup(props, { emit }) {
const store = useStore();
const { t } = useI18n();
const $q = useQuasar();
const router = useRouter();
const jsTransforms: any = ref([]);
const formData: any = ref({});
const showAddJSTransformDialog: any = ref(false);
const qTable: any = ref(null);
const selectedDelete: any = ref(null);
const isUpdated: any = ref(false);
const confirmDelete = ref<boolean>(false);
const confirmForceDelete = ref<boolean>(false);
const confirmBulkDelete = ref<boolean>(false);
const selectedFunctions = ref<any[]>([]);
const { searchObj } = useLogs();
const pipelineList = ref([]);
const selectedPipeline = ref("");
const filterQuery = ref("");
const { track } = useReo();
const columns: any = ref<QTableProps["columns"]>([
{
name: "#",
label: "#",
field: "#",
align: "left",
style: "width: 67px",
},
{
name: "name",
field: "name",
label: t("function.name"),
align: "left",
sortable: true,
},
{
name: "actions",
field: "actions",
label: t("function.actions"),
align: "center",
sortable: false,
classes:'actions-column'
},
]);
const onPipelineSelect = (pipeline: any) => {
const routeUrl = router.resolve({
name: "pipelineEditor",
query: {
id: pipeline.value,
name: pipeline.label,
org_identifier: store.state.selectedOrganization.identifier,
},
}).href;
window.open(routeUrl, "_blank");
};
const getJSTransforms = () => {
const dismiss = $q.notify({
spinner: true,
message: "Please wait while loading functions...",
});
jsTransformService
.list(
1,
100000,
"name",
false,
"",
store.state.selectedOrganization.identifier,
)
.then((res) => {
var counter = 1;
resultTotal.value = res.data.list.length;
if (router.currentRoute.value.query.action == "add") {
showAddUpdateFn({ row: undefined });
}
jsTransforms.value = res.data.list.map((data: any) => {
if (router.currentRoute.value.query.action == "update") {
if (router.currentRoute.value.query.name == data.name) {
showAddUpdateFn({ row: data });
}
}
return {
"#": counter <= 9 ? `0${counter++}` : counter++,
name: data.name,
function: data.function,
params: data.params,
// order: data.order ? data.order : 1,
// stream_name: data.stream_name ? data.stream_name : "--",
// stream_type: data.stream_type ? data.stream_type : "--",
transType: data.transType.toString(),
// ingest: data.stream_name ? true : false,
actions: "",
};
});
searchObj.data.transforms = jsTransforms.value;
dismiss();
})
.catch((err) => {
console.error("Error while pulling function", err);
dismiss();
if (err.response.status != 403) {
$q.notify({
type: "negative",
message: "Error while pulling function.",
timeout: 2000,
});
}
});
};
if (jsTransforms.value == "" || jsTransforms.value == undefined) {
getJSTransforms();
}
interface OptionType {
label: String;
value: number | String;
}
const perPageOptions: any = [
{ label: "20", value: 20 },
{ label: "50", value: 50 },
{ label: "100", value: 100 },
{ label: "250", value: 250 },
{ label: "500", value: 500 },
];
const resultTotal = ref<number>(0);
const maxRecordToReturn = ref<number>(100);
const selectedPerPage = ref<number>(20);
const pagination: any = ref({
rowsPerPage: 20,
});
const changePagination = (val: { label: string; value: any }) => {
selectedPerPage.value = val.value;
pagination.value.rowsPerPage = val.value;
qTable.value.setPagination(pagination.value);
};
const changeMaxRecordToReturn = (val: any) => {
maxRecordToReturn.value = val;
};
const addTransform = () => {
showAddJSTransformDialog.value = true;
};
const transformedPipelineList = computed(() => {
return pipelineList.value.map((pipeline: any) => ({
label: pipeline.name,
value: pipeline.id,
}));
});
const showAddUpdateFn = (props: any) => {
formData.value = props.row;
let action;
if (!props.row) {
isUpdated.value = false;
action = "Add Function";
router.push({
name: "functionList",
query: {
action: "add",
org_identifier: store.state.selectedOrganization.identifier,
},
});
track("Button Click", {
button: "Add Function",
page: "Functions"
});
} else {
isUpdated.value = true;
action = "Update Function";
router.push({
name: "functionList",
query: {
action: "update",
name: props.row.name,
org_identifier: store.state.selectedOrganization.identifier,
},
});
track("Button Click", {
button: "Update Function",
page: "Functions"
});
}
addTransform();
segment.track("Button Click", {
button: action,
user_org: store.state.selectedOrganization.identifier,
user_id: store.state.userInfo.email,
page: "Functions",
});
};
const refreshList = () => {
router.push({
name: "functionList",
query: {
org_identifier: store.state.selectedOrganization.identifier,
},
});
showAddJSTransformDialog.value = false;
getJSTransforms();
};
const hideForm = () => {
showAddJSTransformDialog.value = false;
router.replace({
name: "functionList",
query: {
org_identifier: store.state.selectedOrganization.identifier,
},
});
};
const deleteFn = () => {
jsTransformService
.delete(
store.state.selectedOrganization.identifier,
selectedDelete.value.name,
)
.then((res: any) => {
if (res.data.code == 200) {
$q.notify({
type: "positive",
message: res.data.message,
timeout: 2000,
});
getJSTransforms();
} else {
$q.notify({
type: "negative",
message: res.data.message,
timeout: 2000,
});
}
})
.catch((err) => {
if (err.response.data.code == 409) {
$q.notify({
type: "negative",
message:
"Function deletion failed as it is associated with pipelines. Click on view button to get associated pipelines.",
timeout: 10000,
actions: [
{
label: "View",
color: "white",
handler: () => {
forceRemoveFunction(err.response.data["message"]);
},
},
],
});
return;
}
if (err.response.status != 403) {
$q.notify({
type: "negative",
message:
JSON.stringify(err.response.data["message"]) ||
"Function deletion failed.",
});
}
});
segment.track("Button Click", {
button: "Delete Function",
user_org: store.state.selectedOrganization.identifier,
user_id: store.state.userInfo.email,
function_name: selectedDelete.value.name,
is_ingest_func: selectedDelete.value.ingest,
page: "Functions",
});
};
const showDeleteDialogFn = (props: any) => {
selectedDelete.value = props.row;
confirmDelete.value = true;
};
const getAssociatedPipelines = (props: any) => {
selectedDelete.value = props.row;
jsTransformService
.getAssociatedPipelines(
store.state.selectedOrganization.identifier,
props.row.name,
)
.then((res: any) => {
pipelineList.value = res.data.list;
confirmForceDelete.value = true;
})
.catch((err) => {
console.log(err);
});
};
const forceRemoveFunction = (message: any) => {
const match = message.match(/\[([^\]]+)\]/);
if (match) {
// Convert the matched string to an array of pipeline names
pipelineList.value = JSON.parse(match[0].replace(/'/g, '"'));
}
confirmForceDelete.value = true;
};
const closeDialog = () => {
confirmForceDelete.value = false;
};
const forceDeleteFn = () => {};
const sendToAiChat = (value: any) => {
emit("sendToAiChat", value);
};
const filterData = (rows: any, terms: any) => {
var filtered = [];
terms = terms.toLowerCase();
for (var i = 0; i < rows.length; i++) {
if (rows[i]["name"].toLowerCase().includes(terms)) {
filtered.push(rows[i]);
}
}
return filtered;
};
const visibleRows = computed(() => {
if (!filterQuery.value) return jsTransforms.value || []
return filterData(jsTransforms.value || [], filterQuery.value)
});
const hasVisibleRows = computed(() => visibleRows.value.length > 0);
// Watch visibleRows to sync resultTotal with search filter
watch(visibleRows, (newVisibleRows) => {
resultTotal.value = newVisibleRows.length;
}, { immediate: true });
const openBulkDeleteDialog = () => {
confirmBulkDelete.value = true;
};
const bulkDeleteFunctions = async () => {
const dismiss = $q.notify({
spinner: true,
message: "Deleting functions...",
timeout: 0,
});
try {
if (selectedFunctions.value.length === 0) {
$q.notify({
type: "negative",
message: "No functions selected for deletion",
timeout: 2000,
});
dismiss();
return;
}
// Extract function names for the API call (BE supports names)
const payload = {
ids: selectedFunctions.value.map((f: any) => f.name),
};
const response = await jsTransformService.bulkDelete(
store.state.selectedOrganization.identifier,
payload
);
dismiss();
// Handle response based on successful/unsuccessful arrays
if (response.data) {
const { successful = [], unsuccessful = [] } = response.data;
const successCount = successful.length;
const failCount = unsuccessful.length;
if (failCount > 0 && successCount > 0) {
// Partial success
$q.notify({
type: "warning",
message: `${successCount} function(s) deleted successfully, ${failCount} failed`,
timeout: 5000,
});
} else if (failCount > 0) {
// All failed
$q.notify({
type: "negative",
message: `Failed to delete ${failCount} function(s)`,
timeout: 3000,
});
} else {
// All successful
$q.notify({
type: "positive",
message: `${successCount} function(s) deleted successfully`,
timeout: 2000,
});
}
} else {
// Fallback success message
$q.notify({
type: "positive",
message: `${selectedFunctions.value.length} function(s) deleted successfully`,
timeout: 2000,
});
}
selectedFunctions.value = [];
// Refresh functions list
getJSTransforms();
} catch (error: any) {
dismiss();
console.error("Error deleting functions:", error);
// Show error message from response if available
const errorMessage = error.response?.data?.message || error?.message || "Error deleting functions. Please try again.";
if (error.response?.status != 403 || error?.status != 403) {
$q.notify({
type: "negative",
message: errorMessage,
timeout: 3000,
});
}
}
confirmBulkDelete.value = false;
};
return {
t,
qTable,
store,
router,
jsTransforms,
columns,
formData,
hideForm,
confirmDelete,
selectedDelete,
getJSTransforms,
pagination,
resultTotal,
refreshList,
perPageOptions,
selectedPerPage,
addTransform,
deleteFn,
isUpdated,
showAddUpdateFn,
showDeleteDialogFn,
changePagination,
maxRecordToReturn,
showAddJSTransformDialog,
changeMaxRecordToReturn,
outlinedDelete,
outlinedAccountTree,
forceDeleteFn,
confirmForceDelete,
pipelineList,
selectedPipeline,
closeDialog,
onPipelineSelect,
transformedPipelineList,
getAssociatedPipelines,
filterQuery,
filterData,
getImageURL,
verifyOrganizationStatus,
sendToAiChat,
visibleRows,
hasVisibleRows,
openBulkDeleteDialog,
bulkDeleteFunctions,
confirmBulkDelete,
selectedFunctions,
};
},
computed: {
// selectedOrg() {
// return this.store.state.selectedOrganization.identifier;
// },
},
watch: {
// selectedOrg(newVal: any, oldVal: any) {
// this.verifyOrganizationStatus(
// this.store.state.organizations,
// this.router
// );
// if (
// (newVal != oldVal || this.jsTransforms.value == undefined) &&
// this.router.currentRoute.value.name == "AppFunctions"
// ) {
// this.resultTotal = 0;
// this.jsTransforms = [];
// this.getJSTransforms();
// }
// },
},
});
</script>
<style lang="scss">
.pipeline-list-container {
max-height: 200px; /* Adjust based on item height to fit 5 items */
overflow-y: auto;
}
.dialog-heading {
border-bottom: 1px solid $border-color;
}
.scrollable-list::-webkit-scrollbar {
width: 8px;
}
.scrollable-list::-webkit-scrollbar-thumb {
background-color: #888; /* Desired thumb color */
border-radius: 4px;
}
.scrollable-list::-webkit-scrollbar-thumb:hover {
background-color: blue; /* Darker shade on hover */
}
.scrollable-list::-webkit-scrollbar-track {
background-color: blue; /* Track color */
}
</style>