mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-05 00:09:35 +00:00
feat: ux improvement main (#11838)
#11720 Design at: https://github.com/openobserve/designs/blob/main/dashboards/ux-improvement-dialog/design.md --------- --------- Co-authored-by: ktx-vaidehi <134508096+ktx-vaidehi@users.noreply.github.com> Co-authored-by: Abhay Padamani <141122205+ktx-abhay@users.noreply.github.com> Co-authored-by: Sai Nikhil <nikhil@openobserve.ai> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: ktx-akshay <akshay.patil@kiara.tech> Co-authored-by: Bhargav <BJP232004@GMAIL.COM> Co-authored-by: ktx-mihir <mihir.patel@kiara.tech> Co-authored-by: ktx-riya <riya.kukadiya@kiara.tech> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Omkar Kesarkhane <127078473+omkarK06@users.noreply.github.com> Co-authored-by: omkark06 <omkar@zinclabs.io> Co-authored-by: ktx-vaidehi <vaidehi.akhani@kiara.tech> Co-authored-by: ktx-abhay <abhay.padamani@kiara.tech> Co-authored-by: Shrinath Rao <shnath@openobserve.ai>
This commit is contained in:
co-authored by
ktx-vaidehi
Abhay Padamani
Sai Nikhil
Claude Sonnet 4.6
ktx-akshay
Bhargav
ktx-mihir
ktx-riya
github-actions[bot]
Omkar Kesarkhane
omkark06
ktx-vaidehi
ktx-abhay
Shrinath Rao
parent
4466ff2250
commit
ae22a74e62
@@ -15,7 +15,6 @@
|
||||
|
||||
import { describe, expect, it, beforeEach, afterEach, vi } from "vitest";
|
||||
import { mount, flushPromises } from "@vue/test-utils";
|
||||
import { installQuasar } from "@/test/unit/helpers/install-quasar-plugin";
|
||||
import * as quasar from "quasar";
|
||||
import ActionScripts from "@/components/actionScripts/ActionScripts.vue";
|
||||
import i18n from "@/locales";
|
||||
@@ -35,9 +34,6 @@ vi.mock("@/composables/useStreams", () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
installQuasar({
|
||||
plugins: [quasar.Dialog, quasar.Notify],
|
||||
});
|
||||
|
||||
const mockActionScripts = [
|
||||
{
|
||||
@@ -124,6 +120,72 @@ describe("ActionScripts", () => {
|
||||
],
|
||||
emits: ["update:changeRecordPerPage"],
|
||||
},
|
||||
OTable: {
|
||||
name: "OTable",
|
||||
inheritAttrs: false,
|
||||
template: `
|
||||
<div data-test="action-scripts-table">
|
||||
<table>
|
||||
<tbody>
|
||||
<tr v-for="(row, i) in data" :key="i">
|
||||
<td><slot name="cell-actions" :row="row" /></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<slot name="bottom" />
|
||||
<slot name="empty" />
|
||||
</div>
|
||||
`,
|
||||
props: [
|
||||
"data",
|
||||
"columns",
|
||||
"rowKey",
|
||||
"loading",
|
||||
"selectedIds",
|
||||
"selection",
|
||||
"pagination",
|
||||
"pageSize",
|
||||
"pageSizeOptions",
|
||||
"sorting",
|
||||
"filterMode",
|
||||
"defaultColumns",
|
||||
"showGlobalFilter",
|
||||
],
|
||||
emits: ["update:selected-ids"],
|
||||
},
|
||||
ODialog: {
|
||||
name: "ODialog",
|
||||
inheritAttrs: false,
|
||||
template:
|
||||
'<div data-test="o-dialog" :data-open="open ? \'true\' : \'false\'"><slot name="header-left" /><slot name="header" /><slot /><slot name="footer" /></div>',
|
||||
props: [
|
||||
"open",
|
||||
"persistent",
|
||||
"size",
|
||||
"title",
|
||||
"subTitle",
|
||||
"showClose",
|
||||
"width",
|
||||
"primaryButtonLabel",
|
||||
"secondaryButtonLabel",
|
||||
"neutralButtonLabel",
|
||||
"primaryButtonVariant",
|
||||
"secondaryButtonVariant",
|
||||
"neutralButtonVariant",
|
||||
"primaryButtonDisabled",
|
||||
"secondaryButtonDisabled",
|
||||
"neutralButtonDisabled",
|
||||
"primaryButtonLoading",
|
||||
"secondaryButtonLoading",
|
||||
"neutralButtonLoading",
|
||||
],
|
||||
emits: [
|
||||
"update:open",
|
||||
"click:primary",
|
||||
"click:secondary",
|
||||
"click:neutral",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -507,17 +569,36 @@ describe("ActionScripts", () => {
|
||||
});
|
||||
|
||||
describe("Pagination", () => {
|
||||
it("should handle pagination changes", async () => {
|
||||
const mockVal = { label: "10", value: 10 };
|
||||
wrapper.vm.changePagination(mockVal);
|
||||
// The component uses OTable with built-in client-side pagination (pagination="client").
|
||||
// There is no changePagination method or QTablePagination component in this version.
|
||||
// The OTable component handles all pagination state internally.
|
||||
|
||||
expect(wrapper.vm.selectedPerPage).toBe(10);
|
||||
expect(wrapper.vm.pagination.rowsPerPage).toBe(10);
|
||||
it("should display the action scripts table which owns pagination", () => {
|
||||
// Pagination is handled by OTable internally; verify the table is rendered
|
||||
const table = wrapper.find('[data-test="action-scripts-table"]');
|
||||
expect(table.exists()).toBe(true);
|
||||
});
|
||||
|
||||
it("should display pagination components", () => {
|
||||
const topPagination = wrapper.find('[data-test="table-pagination"]');
|
||||
expect(topPagination.exists()).toBe(true);
|
||||
it("should show correct row count in footer when rows are present", async () => {
|
||||
wrapper.vm.actionsScriptRows = [
|
||||
{
|
||||
"#": "01",
|
||||
id: "test-action-1",
|
||||
name: "Test Action 1",
|
||||
uuid: "uuid-1",
|
||||
created_by: "test@example.com",
|
||||
created_at: "01/01/2023, 12:00:00 AM",
|
||||
execution_details_type: "Cron Job",
|
||||
last_run_at: "-",
|
||||
last_successful_at: "-",
|
||||
status: "active",
|
||||
},
|
||||
];
|
||||
wrapper.vm.filterQuery = "";
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
// resultTotal should be 1
|
||||
expect(wrapper.vm.resultTotal).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -911,6 +992,102 @@ describe("ActionScripts", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Clone ODialog migration", () => {
|
||||
const findODialog = () => wrapper.findComponent({ name: "ODialog" });
|
||||
|
||||
it("should mount the ODialog component (Quasar q-dialog migration)", () => {
|
||||
const dialog = findODialog();
|
||||
expect(dialog.exists()).toBe(true);
|
||||
// The stub renders regardless of `open` so we can verify slot wiring.
|
||||
expect(wrapper.find('[data-test="o-dialog"]').exists()).toBe(true);
|
||||
});
|
||||
|
||||
it("should pass the persistent, size and showClose migration props", () => {
|
||||
const dialog = findODialog();
|
||||
// `persistent` is a boolean shorthand attribute → Vue passes "" to a stub
|
||||
// that declares it as an untyped prop. Both truthy values are acceptable.
|
||||
expect(["", true]).toContain(dialog.props("persistent"));
|
||||
expect(dialog.props("size")).toBe("md");
|
||||
expect(dialog.props("showClose")).toBe(false);
|
||||
});
|
||||
|
||||
it("should pass i18n-resolved title and button label props", () => {
|
||||
const dialog = findODialog();
|
||||
expect(typeof dialog.props("title")).toBe("string");
|
||||
expect((dialog.props("title") as string).length).toBeGreaterThan(0);
|
||||
expect(typeof dialog.props("primaryButtonLabel")).toBe("string");
|
||||
expect((dialog.props("primaryButtonLabel") as string).length).toBeGreaterThan(0);
|
||||
expect(typeof dialog.props("secondaryButtonLabel")).toBe("string");
|
||||
expect((dialog.props("secondaryButtonLabel") as string).length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("should reflect isSubmitting via primaryButtonDisabled prop", async () => {
|
||||
wrapper.vm.isSubmitting = true;
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(findODialog().props("primaryButtonDisabled")).toBe(true);
|
||||
|
||||
wrapper.vm.isSubmitting = false;
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(findODialog().props("primaryButtonDisabled")).toBe(false);
|
||||
});
|
||||
|
||||
it("should declare the expected ODialog migration emits", () => {
|
||||
const dialog = findODialog();
|
||||
const emitsOption = (dialog.vm.$options as any).emits;
|
||||
expect(emitsOption).toEqual(
|
||||
expect.arrayContaining([
|
||||
"update:open",
|
||||
"click:primary",
|
||||
"click:secondary",
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("should not throw when click:primary is emitted from ODialog", async () => {
|
||||
const dialog = findODialog();
|
||||
expect(() => dialog.vm.$emit("click:primary")).not.toThrow();
|
||||
await wrapper.vm.$nextTick();
|
||||
// Component should remain mounted regardless
|
||||
expect(wrapper.exists()).toBe(true);
|
||||
});
|
||||
|
||||
it("should not throw when click:secondary is emitted from ODialog", async () => {
|
||||
const dialog = findODialog();
|
||||
await dialog.vm.$emit("click:secondary");
|
||||
expect(wrapper.exists()).toBe(true);
|
||||
});
|
||||
|
||||
it("should not throw when update:open is emitted from ODialog", async () => {
|
||||
const dialog = findODialog();
|
||||
await dialog.vm.$emit("update:open", false);
|
||||
expect(wrapper.exists()).toBe(true);
|
||||
});
|
||||
|
||||
it("should render the back button inside the header-left slot", () => {
|
||||
const backBtn = wrapper.find('[data-test="add-action-back-btn"]');
|
||||
expect(backBtn.exists()).toBe(true);
|
||||
});
|
||||
|
||||
it("should keep component mounted when back button in header-left is clicked", async () => {
|
||||
const backBtn = wrapper.find('[data-test="add-action-back-btn"]');
|
||||
expect(backBtn.exists()).toBe(true);
|
||||
await backBtn.trigger("click");
|
||||
expect(wrapper.exists()).toBe(true);
|
||||
});
|
||||
|
||||
it("should render clone form fields inside the ODialog default slot", () => {
|
||||
expect(
|
||||
wrapper.find('[data-test="to-be-clone-action-name"]').exists(),
|
||||
).toBe(true);
|
||||
expect(
|
||||
wrapper.find('[data-test="to-be-clone-stream-type"]').exists(),
|
||||
).toBe(true);
|
||||
expect(
|
||||
wrapper.find('[data-test="to-be-clone-stream-name"]').exists(),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("execution type display names", () => {
|
||||
it("should convert 'repeat' execution type to 'Cron Job'", async () => {
|
||||
store.state.organizationData.actions = [
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- Copyright 2026 OpenObserve Inc.
|
||||
<!-- Copyright 2026 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
|
||||
@@ -20,7 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
<div data-test="action-scripts-list-page">
|
||||
<div
|
||||
v-if="!showAddActionScriptDialog"
|
||||
class="tw:w-full tw:h-full tw:px-[0.625rem] tw:pb-[0.625rem] q-pt-xs"
|
||||
class="tw:w-full tw:h-full tw:px-[0.625rem] tw:pb-[0.625rem] tw:pt-1"
|
||||
>
|
||||
<div class="card-container tw:mb-[0.625rem]">
|
||||
<div
|
||||
@@ -35,18 +35,16 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
<div
|
||||
class="tw:full-width tw:flex tw:items-center tw:justify-end tw:gap-3"
|
||||
>
|
||||
<q-input
|
||||
<OInput
|
||||
v-model="filterQuery"
|
||||
borderless
|
||||
dense
|
||||
class="q-ml-auto no-border o2-search-input"
|
||||
class="tw:ml-auto no-border o2-search-input"
|
||||
:placeholder="t('actions.search')"
|
||||
data-test="action-list-search-input"
|
||||
>
|
||||
<template #prepend>
|
||||
<q-icon class="o2-search-input-icon" name="search" />
|
||||
<template #icon-left>
|
||||
<OIcon class="o2-search-input-icon" name="search" size="sm" />
|
||||
</template>
|
||||
</q-input>
|
||||
</OInput>
|
||||
<OButton
|
||||
data-test="action-list-add-btn"
|
||||
variant="primary"
|
||||
@@ -59,79 +57,55 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
</div>
|
||||
<div class="tw:w-full tw:h-full tw:pb-[0.625rem]">
|
||||
<div class="card-container tw:h-[calc(100vh-124px)]">
|
||||
<q-table
|
||||
<OTable
|
||||
data-test="action-scripts-table"
|
||||
ref="qTable"
|
||||
:rows="visibleRows"
|
||||
:data="visibleRows"
|
||||
:columns="columns"
|
||||
row-key="id"
|
||||
:pagination="pagination"
|
||||
style="width: 100%"
|
||||
:style="{
|
||||
height: hasVisibleRows
|
||||
? 'calc(100vh - var(--navbar-height) - 77px)'
|
||||
: '',
|
||||
}"
|
||||
class="o2-quasar-table o2-row-md o2-quasar-table-header-sticky o2-last-row-border"
|
||||
:loading="loading"
|
||||
:selected-ids="selectedActionScriptIds"
|
||||
selection="multiple"
|
||||
v-model:selected="selectedActionScripts"
|
||||
pagination="client"
|
||||
:page-size="20"
|
||||
:page-size-options="[5, 10, 20, 50, 100]"
|
||||
sorting="client"
|
||||
filter-mode="client"
|
||||
:default-columns="false"
|
||||
:show-global-filter="false"
|
||||
@update:selected-ids="handleSelectedIdsUpdate"
|
||||
>
|
||||
<template #no-data>
|
||||
<template #empty>
|
||||
<NoData />
|
||||
</template>
|
||||
<template v-slot:body-selection="scope">
|
||||
<q-checkbox
|
||||
v-model="scope.selected"
|
||||
size="sm"
|
||||
class="o2-table-checkbox"
|
||||
/>
|
||||
</template>
|
||||
<template v-slot:body-cell-actions="props">
|
||||
<q-td :props="props">
|
||||
<div
|
||||
data-test="action-scripts-loading"
|
||||
v-if="alertStateLoadingMap[props.row.uuid]"
|
||||
style="display: inline-block; width: 33.14px; height: auto"
|
||||
class="flex justify-center items-center q-ml-xs"
|
||||
:title="`Turning ${props.row.enabled ? 'Off' : 'On'}`"
|
||||
>
|
||||
<q-circular-progress
|
||||
indeterminate
|
||||
rounded
|
||||
size="16px"
|
||||
:value="1"
|
||||
color="secondary"
|
||||
/>
|
||||
</div>
|
||||
<OButton
|
||||
:data-test="`alert-list-${props.row.name}-update-alert`"
|
||||
variant="ghost"
|
||||
size="icon-circle-sm"
|
||||
:title="t('alerts.edit')"
|
||||
@click="showAddUpdateFn(props)"
|
||||
><q-icon name="edit" size="16px"
|
||||
/></OButton>
|
||||
<OButton
|
||||
:data-test="`alert-list-${props.row.name}-delete-alert`"
|
||||
variant="ghost"
|
||||
size="icon-circle-sm"
|
||||
:title="t('alerts.delete')"
|
||||
@click="showDeleteDialogFn(props)"
|
||||
><q-icon :name="outlinedDelete" size="16px"
|
||||
/></OButton>
|
||||
</q-td>
|
||||
<template #cell-actions="{ row }">
|
||||
<div
|
||||
data-test="action-scripts-loading"
|
||||
v-if="alertStateLoadingMap[row.uuid]"
|
||||
style="display: inline-block; width: 33.14px; height: auto"
|
||||
class="tw:flex tw:justify-center tw:items-center tw:ml-1"
|
||||
:title="`Turning ${row.enabled ? 'Off' : 'On'}`"
|
||||
>
|
||||
<OSpinner size="xs" />
|
||||
</div>
|
||||
<OButton
|
||||
:data-test="`alert-list-${row.name}-update-alert`"
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
:title="t('alerts.edit')"
|
||||
@click="showAddUpdateFn({ row })"
|
||||
><OIcon name="edit" size="sm"
|
||||
/></OButton>
|
||||
<OButton
|
||||
:data-test="`alert-list-${row.name}-delete-alert`"
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
:title="t('alerts.delete')"
|
||||
@click="showDeleteDialogFn({ row })"
|
||||
><OIcon name="delete" size="sm"
|
||||
/></OButton>
|
||||
</template>
|
||||
|
||||
<template v-slot:body-cell-function="props">
|
||||
<q-td :props="props">
|
||||
<q-tooltip>
|
||||
<pre>{{ props.row.sql }}</pre>
|
||||
</q-tooltip>
|
||||
<pre style="white-space: break-spaces">{{ props.row.sql }}</pre>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template #bottom="scope">
|
||||
<template #bottom>
|
||||
<div
|
||||
class="tw:flex tw:items-center tw:justify-between tw:w-full tw:h-[48px]"
|
||||
>
|
||||
@@ -147,51 +121,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
@click="openBulkDeleteDialog"
|
||||
><q-icon name="delete" size="16px" /><span class="tw:ml-1.5"
|
||||
><OIcon name="delete" size="sm" /><span class="tw:ml-1.5"
|
||||
>Delete</span
|
||||
></OButton
|
||||
>
|
||||
</div>
|
||||
<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 -->
|
||||
<!-- here we can add the classes class so that the head will be sticky -->
|
||||
<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>
|
||||
</OTable>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -219,80 +156,54 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
@update:cancel="confirmBulkDelete = false"
|
||||
v-model="confirmBulkDelete"
|
||||
/>
|
||||
<template>
|
||||
<q-dialog class="q-pa-md" v-model="showForm" persistent>
|
||||
<q-card class="clone-alert-popup">
|
||||
<div class="row items-center no-wrap q-mx-md q-my-sm">
|
||||
<div class="flex items-center">
|
||||
<div
|
||||
data-test="add-action-back-btn"
|
||||
class="flex justify-center items-center q-mr-md cursor-pointer"
|
||||
style="
|
||||
border: 1.5px solid;
|
||||
border-radius: 50%;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
"
|
||||
title="Go Back"
|
||||
@click="showForm = false"
|
||||
>
|
||||
<q-icon name="arrow_back_ios_new" size="14px" />
|
||||
</div>
|
||||
<div class="text-h6" data-test="clone-alert-title">
|
||||
{{ t("alerts.cloneTitle") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<q-card-section>
|
||||
<q-form @submit="submitForm">
|
||||
<q-input
|
||||
data-test="to-be-clone-action-name"
|
||||
v-model="toBeCloneAlertName"
|
||||
label="Alert Name"
|
||||
/>
|
||||
<q-select
|
||||
data-test="to-be-clone-stream-type"
|
||||
v-model="toBeClonestreamType"
|
||||
label="Stream Type"
|
||||
:options="streamTypes"
|
||||
@update:model-value="updateStreams()"
|
||||
/>
|
||||
<q-select
|
||||
data-test="to-be-clone-stream-name"
|
||||
v-model="toBeClonestreamName"
|
||||
:loading="isFetchingStreams"
|
||||
:disable="!toBeClonestreamType"
|
||||
label="Stream Name"
|
||||
:options="streamNames"
|
||||
@change="updateStreamName"
|
||||
@filter="filterStreams"
|
||||
use-input
|
||||
fill-input
|
||||
hide-selected
|
||||
:input-debounce="400"
|
||||
/>
|
||||
<div class="tw:flex tw:gap-2 tw:justify-center q-mt-lg">
|
||||
<OButton
|
||||
data-test="clone-action-cancel-btn"
|
||||
variant="outline"
|
||||
size="sm-action"
|
||||
@click="showForm = false"
|
||||
>{{ t("alerts.cancel") }}</OButton
|
||||
>
|
||||
<OButton
|
||||
data-test="clone-action-submit-btn"
|
||||
variant="primary"
|
||||
size="sm-action"
|
||||
type="submit"
|
||||
:disabled="isSubmitting"
|
||||
>{{ t("alerts.save") }}</OButton
|
||||
>
|
||||
</div>
|
||||
</q-form>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</template>
|
||||
<ODialog
|
||||
data-test="action-scripts-form-dialog"
|
||||
v-model:open="showForm"
|
||||
persistent
|
||||
size="md"
|
||||
:show-close="false"
|
||||
:title="t('alerts.cloneTitle')"
|
||||
:secondary-button-label="t('alerts.cancel')"
|
||||
:primary-button-label="t('alerts.save')"
|
||||
:primary-button-disabled="isSubmitting"
|
||||
@click:secondary="showForm = false"
|
||||
@click:primary="submitForm"
|
||||
>
|
||||
<template #header-left>
|
||||
<div
|
||||
data-test="add-action-back-btn"
|
||||
class="tw:flex tw:justify-center tw:items-center tw:cursor-pointer"
|
||||
style="border: 1.5px solid; border-radius: 50%; width: 22px; height: 22px;"
|
||||
title="Go Back"
|
||||
@click="showForm = false"
|
||||
>
|
||||
<OIcon name="arrow-back-ios-new" size="xs" />
|
||||
</div>
|
||||
</template>
|
||||
<OForm id="action-script-clone-form" :default-values="{}" @submit="submitForm">
|
||||
<OInput
|
||||
data-test="to-be-clone-action-name"
|
||||
v-model="toBeCloneAlertName"
|
||||
label="Alert Name"
|
||||
/>
|
||||
<OSelect
|
||||
data-test="to-be-clone-stream-type"
|
||||
v-model="toBeClonestreamType"
|
||||
label="Stream Type"
|
||||
:options="streamTypes"
|
||||
@update:model-value="updateStreams()"
|
||||
/>
|
||||
<OSelect
|
||||
data-test="to-be-clone-stream-name"
|
||||
v-model="toBeClonestreamName"
|
||||
:loading="isFetchingStreams"
|
||||
:disabled="!toBeClonestreamType"
|
||||
label="Stream Name"
|
||||
:options="streamNames"
|
||||
@update:model-value="updateStreamName"
|
||||
/>
|
||||
</OForm>
|
||||
</ODialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -311,9 +222,7 @@ import { useStore } from "vuex";
|
||||
import { useRouter } from "vue-router";
|
||||
import useStreams from "@/composables/useStreams";
|
||||
|
||||
import { QTable, date, useQuasar, type QTableProps } from "quasar";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import QTablePagination from "@/components/shared/grid/Pagination.vue";
|
||||
import alertsService from "@/services/alerts";
|
||||
import destinationService from "@/services/alert_destination";
|
||||
import templateService from "@/services/alert_templates";
|
||||
@@ -328,41 +237,52 @@ import {
|
||||
convertUnixToQuasarFormat,
|
||||
} from "@/utils/zincutils";
|
||||
import type { Alert, AlertListItem } from "@/ts/interfaces/index";
|
||||
import {
|
||||
outlinedDelete,
|
||||
outlinedPause,
|
||||
outlinedPlayArrow,
|
||||
} from "@quasar/extras/material-icons-outlined";
|
||||
import OIcon from "@/lib/core/Icon/OIcon.vue";
|
||||
import actions from "@/services/action_scripts";
|
||||
import useActions from "@/composables/useActions";
|
||||
import { useReo } from "@/services/reodotdev_analytics";
|
||||
import OButton from "@/lib/core/Button/OButton.vue";
|
||||
import ODialog from "@/lib/overlay/Dialog/ODialog.vue";
|
||||
import OSpinner from "@/lib/feedback/Spinner/OSpinner.vue";
|
||||
import OInput from "@/lib/forms/Input/OInput.vue";
|
||||
import OCheckbox from "@/lib/forms/Checkbox/OCheckbox.vue";
|
||||
import OTooltip from "@/lib/overlay/Tooltip/OTooltip.vue";
|
||||
import OSelect from "@/lib/forms/Select/OSelect.vue";
|
||||
import OForm from "@/lib/forms/Form/OForm.vue";
|
||||
import OTable from "@/lib/core/Table/OTable.vue";
|
||||
import type { OTableColumnDef } from "@/lib/core/Table/OTable.types";
|
||||
import { toast } from "@/lib/feedback/Toast/useToast";
|
||||
|
||||
interface ActionScriptList {
|
||||
"#": string | number; // If this represents a serial number or row index
|
||||
id: any; // The unique identifier, specify the type (e.g., string, number) if known
|
||||
name: any; // Name of the action script
|
||||
uuid: any; // Unique UUID for the action script
|
||||
created_by: any; // The user who created the script
|
||||
created_at: string; // Creation timestamp, in ISO format or a specific format
|
||||
last_run_at: string; // Timestamp of the last run, in ISO format or a specific format
|
||||
last_successful_at: string; // Timestamp of the last successful run
|
||||
status: any; // Current status of the action script
|
||||
"#": string | number;
|
||||
id: any;
|
||||
name: any;
|
||||
uuid: any;
|
||||
created_by: any;
|
||||
created_at: string;
|
||||
last_run_at: string;
|
||||
last_successful_at: string;
|
||||
status: any;
|
||||
}
|
||||
|
||||
// import alertList from "./alerts";
|
||||
|
||||
// TODO code clean up needs to be done
|
||||
export default defineComponent({
|
||||
name: "AlertList",
|
||||
components: {
|
||||
QTablePagination,
|
||||
OIcon,
|
||||
EditScript: defineAsyncComponent(
|
||||
() => import("@/components/actionScripts/EditScript.vue"),
|
||||
),
|
||||
NoData,
|
||||
ConfirmDialog,
|
||||
OButton,
|
||||
ODialog,
|
||||
OSpinner,
|
||||
OInput,
|
||||
OCheckbox,
|
||||
OTooltip,
|
||||
OSelect,
|
||||
OForm,
|
||||
OTable,
|
||||
},
|
||||
emits: [
|
||||
"updated:fields",
|
||||
@@ -372,14 +292,12 @@ export default defineComponent({
|
||||
setup() {
|
||||
const store = useStore();
|
||||
const { t } = useI18n();
|
||||
const $q = useQuasar();
|
||||
const router = useRouter();
|
||||
const alerts: Ref<Alert[]> = ref([]);
|
||||
const actionsScriptRows: Ref<ActionScriptList[]> = ref([]);
|
||||
const formData: Ref<Alert | {}> = ref({});
|
||||
const toBeClonedAlert: Ref<Alert | {}> = ref({});
|
||||
const showAddActionScriptDialog: any = ref(false);
|
||||
const qTable: Ref<InstanceType<typeof QTable> | null> = ref(null);
|
||||
const selectedDelete: any = ref(null);
|
||||
const isUpdated: any = ref(false);
|
||||
const confirmDelete = ref<boolean>(false);
|
||||
@@ -390,6 +308,7 @@ export default defineComponent({
|
||||
const schemaList = ref([]);
|
||||
const streams: any = ref({});
|
||||
const isFetchingStreams = ref(false);
|
||||
const loading = ref(false);
|
||||
const isSubmitting = ref(false);
|
||||
const resultTotal = ref<number>(0);
|
||||
const filterQuery = ref("");
|
||||
@@ -413,83 +332,98 @@ export default defineComponent({
|
||||
name: "folder2",
|
||||
},
|
||||
]);
|
||||
const columns: any = ref<QTableProps["columns"]>([
|
||||
|
||||
const columns: OTableColumnDef[] = [
|
||||
{
|
||||
name: "#",
|
||||
label: "#",
|
||||
field: "#",
|
||||
align: "center",
|
||||
style: "width: 67px;",
|
||||
id: "#",
|
||||
header: "#",
|
||||
accessorKey: "#",
|
||||
size: 67,
|
||||
meta: { align: "center" },
|
||||
},
|
||||
{
|
||||
name: "name",
|
||||
field: "name",
|
||||
label: t("alerts.name"),
|
||||
align: "left",
|
||||
id: "name",
|
||||
header: t("alerts.name"),
|
||||
accessorKey: "name",
|
||||
sortable: true,
|
||||
meta: { align: "left" },
|
||||
},
|
||||
{
|
||||
name: "created_by",
|
||||
field: "created_by",
|
||||
label: t("alerts.createdBy"),
|
||||
align: "center",
|
||||
id: "created_by",
|
||||
header: t("alerts.createdBy"),
|
||||
accessorKey: "created_by",
|
||||
sortable: true,
|
||||
meta: { align: "center" },
|
||||
},
|
||||
{
|
||||
name: "created_at",
|
||||
field: "created_at",
|
||||
label: t("alerts.createdAt"),
|
||||
align: "left",
|
||||
id: "created_at",
|
||||
header: t("alerts.createdAt"),
|
||||
accessorKey: "created_at",
|
||||
sortable: true,
|
||||
meta: { align: "left" },
|
||||
},
|
||||
{
|
||||
name: "execution_details_type",
|
||||
field: "execution_details_type",
|
||||
label: t("actions.type"),
|
||||
align: "left",
|
||||
id: "execution_details_type",
|
||||
header: t("actions.type"),
|
||||
accessorKey: "execution_details_type",
|
||||
sortable: true,
|
||||
meta: { align: "left" },
|
||||
},
|
||||
{
|
||||
name: "last_run_at",
|
||||
field: "last_run_at",
|
||||
label: t("alerts.lastRunAt"),
|
||||
align: "left",
|
||||
id: "last_run_at",
|
||||
header: t("alerts.lastRunAt"),
|
||||
accessorKey: "last_run_at",
|
||||
sortable: true,
|
||||
meta: { align: "left" },
|
||||
},
|
||||
{
|
||||
name: "last_successful_at",
|
||||
field: "last_successful_at",
|
||||
label: t("alerts.lastSuccessfulAt"),
|
||||
align: "left",
|
||||
id: "last_successful_at",
|
||||
header: t("alerts.lastSuccessfulAt"),
|
||||
accessorKey: "last_successful_at",
|
||||
sortable: true,
|
||||
meta: { align: "left" },
|
||||
},
|
||||
{
|
||||
name: "status",
|
||||
field: "status",
|
||||
label: t("alerts.status"),
|
||||
align: "left",
|
||||
id: "status",
|
||||
header: t("alerts.status"),
|
||||
accessorKey: "status",
|
||||
sortable: true,
|
||||
meta: { align: "left" },
|
||||
},
|
||||
{
|
||||
name: "actions",
|
||||
field: "actions",
|
||||
label: t("alerts.actions"),
|
||||
align: "center",
|
||||
sortable: false,
|
||||
classes: "actions-column",
|
||||
style: "width: 100px",
|
||||
id: "actions",
|
||||
header: t("alerts.actions"),
|
||||
isAction: true,
|
||||
pinned: "right",
|
||||
size: 100,
|
||||
meta: { align: "center", actionCount: 2 },
|
||||
},
|
||||
]);
|
||||
];
|
||||
|
||||
const selectedActionScriptIds = computed(() =>
|
||||
selectedActionScripts.value.map((s: any) => s.id),
|
||||
);
|
||||
|
||||
const handleSelectedIdsUpdate = (ids: string[]) => {
|
||||
const map = new Map(
|
||||
actionsScriptRows.value.map((r: any) => [r.id, r]),
|
||||
);
|
||||
selectedActionScripts.value = ids
|
||||
.map((id: any) => map.get(id))
|
||||
.filter(Boolean);
|
||||
};
|
||||
|
||||
const activeTab: any = ref("alerts");
|
||||
const destinations = ref([0]);
|
||||
const templates = ref([0]);
|
||||
|
||||
const getActionScripts = () => {
|
||||
const dismiss = $q.notify({
|
||||
spinner: true,
|
||||
const dismiss = toast({
|
||||
variant: "loading",
|
||||
message: "Please wait while loading actions...",
|
||||
});
|
||||
|
||||
loading.value = true;
|
||||
getAllActions()
|
||||
.then(() => {
|
||||
var counter = 1;
|
||||
@@ -545,11 +479,14 @@ export default defineComponent({
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
dismiss();
|
||||
$q.notify({
|
||||
type: "negative",
|
||||
toast({
|
||||
variant: "error",
|
||||
message: "Error while pulling Actions.",
|
||||
timeout: 2000,
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -568,24 +505,7 @@ export default defineComponent({
|
||||
},
|
||||
);
|
||||
|
||||
const perPageOptions: any = [
|
||||
{ label: "5", value: 5 },
|
||||
{ label: "10", value: 10 },
|
||||
{ label: "20", value: 20 },
|
||||
{ label: "50", value: 50 },
|
||||
{ label: "100", value: 100 },
|
||||
{ label: "All", value: 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 addAlert = () => {
|
||||
track("Button Click", {
|
||||
@@ -599,10 +519,6 @@ export default defineComponent({
|
||||
formData.value = alerts.value.find(
|
||||
(alert: any) => alert.uuid === props.row?.uuid,
|
||||
) as Alert;
|
||||
//use this comment for testing multi_time_range shifts
|
||||
// if( formData.value){
|
||||
// formData.value.query_condition.multi_time_range = [{offSet:"30m"}];
|
||||
// }
|
||||
let action;
|
||||
if (!props.row) {
|
||||
isUpdated.value = false;
|
||||
@@ -658,15 +574,15 @@ export default defineComponent({
|
||||
)
|
||||
.then((res: any) => {
|
||||
if (res.data.code == 200) {
|
||||
$q.notify({
|
||||
type: "positive",
|
||||
toast({
|
||||
variant: "success",
|
||||
message: res.data.message,
|
||||
timeout: 2000,
|
||||
});
|
||||
getActionScripts();
|
||||
} else {
|
||||
$q.notify({
|
||||
type: "negative",
|
||||
toast({
|
||||
variant: "error",
|
||||
message: res.data.message,
|
||||
timeout: 2000,
|
||||
});
|
||||
@@ -676,8 +592,8 @@ export default defineComponent({
|
||||
if (err.response?.status == 403) {
|
||||
return;
|
||||
}
|
||||
$q.notify({
|
||||
type: "negative",
|
||||
toast({
|
||||
variant: "error",
|
||||
message: err?.data?.message || "Error while deleting alert.",
|
||||
timeout: 2000,
|
||||
});
|
||||
@@ -705,8 +621,8 @@ export default defineComponent({
|
||||
const bulkDeleteActionScripts = async () => {
|
||||
try {
|
||||
if (selectedActionScripts.value.length === 0) {
|
||||
$q.notify({
|
||||
type: "warning",
|
||||
toast({
|
||||
variant: "warning",
|
||||
message: "No action scripts selected",
|
||||
timeout: 2000,
|
||||
});
|
||||
@@ -728,20 +644,20 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
if (successful.length > 0 && unsuccessful.length === 0) {
|
||||
$q.notify({
|
||||
type: "positive",
|
||||
toast({
|
||||
variant: "success",
|
||||
message: `Successfully deleted ${successful.length} action script(s)`,
|
||||
timeout: 2000,
|
||||
});
|
||||
} else if (successful.length > 0 && unsuccessful.length > 0) {
|
||||
$q.notify({
|
||||
type: "warning",
|
||||
toast({
|
||||
variant: "warning",
|
||||
message: `Deleted ${successful.length} action script(s). Failed to delete ${unsuccessful.length} action script(s)`,
|
||||
timeout: 3000,
|
||||
});
|
||||
} else if (unsuccessful.length > 0) {
|
||||
$q.notify({
|
||||
type: "negative",
|
||||
toast({
|
||||
variant: "error",
|
||||
message: `Failed to delete ${unsuccessful.length} action script(s)`,
|
||||
timeout: 2000,
|
||||
});
|
||||
@@ -752,8 +668,8 @@ export default defineComponent({
|
||||
confirmBulkDelete.value = false;
|
||||
} catch (error: any) {
|
||||
if (error.response?.status != 403 || error?.status != 403) {
|
||||
$q.notify({
|
||||
type: "negative",
|
||||
toast({
|
||||
variant: "error",
|
||||
message:
|
||||
error.response?.data?.message ||
|
||||
error?.message ||
|
||||
@@ -818,30 +734,6 @@ export default defineComponent({
|
||||
streamNames.value = filterColumns(indexOptions.value, val, update);
|
||||
};
|
||||
|
||||
// const toggleAlertState = (row: any) => {
|
||||
// alertStateLoadingMap.value[row.uuid] = true;
|
||||
// const alert: Alert = alerts.value.find(
|
||||
// (alert) => alert.uuid === row.uuid,
|
||||
// ) as Alert;
|
||||
// alertsService
|
||||
// .toggleState(
|
||||
// store.state.selectedOrganization.identifier,
|
||||
// alert.stream_name,
|
||||
// alert.name,
|
||||
// !alert?.enabled,
|
||||
// alert.stream_type,
|
||||
// )
|
||||
// .then(() => {
|
||||
// alert.enabled = !alert.enabled;
|
||||
// actionsScriptRows.value.forEach((alert) => {
|
||||
// alert.uuid === row.uuid ? (alert.enabled = !alert.enabled) : null;
|
||||
// });
|
||||
// })
|
||||
// .finally(() => {
|
||||
// alertStateLoadingMap.value[row.uuid] = false;
|
||||
// });
|
||||
// };
|
||||
|
||||
const routeTo = (name: string) => {
|
||||
router.push({
|
||||
name: name,
|
||||
@@ -852,10 +744,6 @@ export default defineComponent({
|
||||
});
|
||||
};
|
||||
|
||||
// const refreshDestination = async () => {
|
||||
// await getDestinations();
|
||||
// };
|
||||
|
||||
const filterData = (rows: any, terms: any) => {
|
||||
var filtered = [];
|
||||
terms = terms.toLowerCase();
|
||||
@@ -891,7 +779,6 @@ export default defineComponent({
|
||||
|
||||
return {
|
||||
t,
|
||||
qTable,
|
||||
store,
|
||||
router,
|
||||
alerts,
|
||||
@@ -901,17 +788,13 @@ export default defineComponent({
|
||||
confirmDelete,
|
||||
selectedDelete,
|
||||
getActionScripts,
|
||||
pagination,
|
||||
resultTotal,
|
||||
refreshList,
|
||||
perPageOptions,
|
||||
selectedPerPage,
|
||||
addAlert,
|
||||
deleteAlert,
|
||||
isUpdated,
|
||||
showAddUpdateFn,
|
||||
showDeleteDialogFn,
|
||||
changePagination,
|
||||
maxRecordToReturn,
|
||||
showAddActionScriptDialog,
|
||||
toBeCloneAlertName,
|
||||
@@ -924,8 +807,8 @@ export default defineComponent({
|
||||
indexOptions,
|
||||
streams,
|
||||
isFetchingStreams,
|
||||
loading,
|
||||
isSubmitting,
|
||||
outlinedDelete,
|
||||
filterQuery,
|
||||
filterData,
|
||||
getImageURL,
|
||||
@@ -934,8 +817,6 @@ export default defineComponent({
|
||||
verifyOrganizationStatus,
|
||||
folders,
|
||||
splitterModel,
|
||||
outlinedPause,
|
||||
outlinedPlayArrow,
|
||||
actionsScriptRows,
|
||||
alertStateLoadingMap,
|
||||
templates,
|
||||
@@ -943,6 +824,8 @@ export default defineComponent({
|
||||
hasVisibleRows,
|
||||
confirmBulkDelete,
|
||||
selectedActionScripts,
|
||||
selectedActionScriptIds,
|
||||
handleSelectedIdsUpdate,
|
||||
openBulkDeleteDialog,
|
||||
bulkDeleteActionScripts,
|
||||
getAlertByName,
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
|
||||
import { describe, expect, it, beforeEach, afterEach, vi } from "vitest";
|
||||
import { mount, flushPromises } from "@vue/test-utils";
|
||||
import { installQuasar } from "@/test/unit/helpers/install-quasar-plugin";
|
||||
import * as quasar from "quasar";
|
||||
import EditScript from "@/components/actionScripts/EditScript.vue";
|
||||
import i18n from "@/locales";
|
||||
@@ -66,9 +65,6 @@ vi.mock("@/services/service_accounts", () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
installQuasar({
|
||||
plugins: [quasar.Dialog, quasar.Notify],
|
||||
});
|
||||
|
||||
describe("EditScript", () => {
|
||||
let wrapper: any;
|
||||
@@ -179,21 +175,26 @@ describe("EditScript", () => {
|
||||
});
|
||||
|
||||
it("should validate name field", async () => {
|
||||
const form = wrapper.find("form");
|
||||
// EditScript uses OButton @click (not a <form> submit).
|
||||
// Validation is driven via wrapper.vm.nameError and the save button click.
|
||||
wrapper.vm.formData.name = "";
|
||||
|
||||
await form.trigger("submit");
|
||||
// Form validation should prevent submission with empty name
|
||||
// Clicking save triggers inline validation and sets nameError
|
||||
const saveBtn = wrapper.find('[data-test="add-action-script-save-btn"]');
|
||||
await saveBtn.trigger("click");
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
// With empty name the nameError ref should be populated
|
||||
expect(wrapper.vm.formData.name).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Step navigation", () => {
|
||||
it("should display stepper component", () => {
|
||||
const stepper = wrapper.find(
|
||||
'[data-cy="stepper"], q-stepper, .q-stepper',
|
||||
);
|
||||
expect(stepper.exists()).toBe(true);
|
||||
// The component uses OStepper (O2 lib), not Quasar q-stepper.
|
||||
// Verify the stepper is present by checking its first step.
|
||||
const step1 = wrapper.find('[data-test="add-action-script-step-1"]');
|
||||
expect(step1.exists()).toBe(true);
|
||||
});
|
||||
|
||||
it("should display step 1: Upload Code ZIP", () => {
|
||||
@@ -475,9 +476,18 @@ describe("EditScript", () => {
|
||||
});
|
||||
|
||||
it("should validate form data before submission", async () => {
|
||||
// The component does not use a Quasar q-form with addActionScriptFormRef.
|
||||
// Validation is performed inline in saveActionScript via nameError/typeError etc.
|
||||
// Verify that clicking save with an empty name does NOT navigate away (nameError is set).
|
||||
wrapper.vm.formData.name = "";
|
||||
const isValid = await wrapper.vm.addActionScriptFormRef?.validate();
|
||||
expect(isValid).toBe(false);
|
||||
wrapper.vm.formData.type = "scheduled";
|
||||
|
||||
const saveBtn = wrapper.find('[data-test="add-action-script-save-btn"]');
|
||||
await saveBtn.trigger("click");
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
// nameError should now be set to signal a validation failure
|
||||
expect(wrapper.vm.nameError).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -704,7 +714,8 @@ describe("EditScript", () => {
|
||||
});
|
||||
|
||||
describe("Form Validation", () => {
|
||||
it("should validate action script data via form submission", async () => {
|
||||
it("should validate action script data via save button click", async () => {
|
||||
// EditScript uses OButton @click, not a <form> submit — there is no <form> element.
|
||||
// Set invalid form data
|
||||
wrapper.vm.formData = {
|
||||
name: "", // Invalid - empty name
|
||||
@@ -717,15 +728,14 @@ describe("EditScript", () => {
|
||||
conditions: [],
|
||||
};
|
||||
|
||||
const form = wrapper.find("form");
|
||||
expect(form.exists()).toBe(true);
|
||||
const saveBtn = wrapper.find('[data-test="add-action-script-save-btn"]');
|
||||
expect(saveBtn.exists()).toBe(true);
|
||||
|
||||
// Try to submit form - should trigger validation
|
||||
await form.trigger("submit");
|
||||
// Clicking save with empty name triggers inline validation
|
||||
await saveBtn.trigger("click");
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
// Should not proceed with save due to validation errors
|
||||
// The form validation will prevent submission
|
||||
// nameError should signal validation failure
|
||||
expect(wrapper.vm.formData.name).toBe("");
|
||||
});
|
||||
|
||||
|
||||
@@ -16,28 +16,28 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
<template>
|
||||
<div data-test="add-action-script-section">
|
||||
<div class="tw:w-full tw:h-full tw:px-[0.625rem] tw:pb-[0.625rem] q-pt-xs">
|
||||
<div class="tw:w-full tw:h-full tw:px-[0.625rem] tw:pb-[0.625rem] tw:pt-1">
|
||||
<div class="card-container">
|
||||
<div
|
||||
class="tw:flex tw:items-center tw:justify-between tw:py-3 tw:pl-4 tw:pr-2 tw:h-[68px]"
|
||||
>
|
||||
<div
|
||||
data-test="add-action-script-back-btn"
|
||||
class="flex justify-center items-center q-mr-md cursor-pointer"
|
||||
class="tw:flex tw:justify-center tw:items-center tw:mr-3 tw:cursor-pointer"
|
||||
title="Go Back"
|
||||
@click="router.back()"
|
||||
>
|
||||
<q-icon name="arrow_back_ios_new" size="14px" />
|
||||
<OIcon name="arrow-back-ios-new" size="xs" />
|
||||
<div
|
||||
v-if="isEditingActionScript"
|
||||
class="text-h6 q-pl-sm"
|
||||
class="tw:text-xl tw:font-semibold tw:pl-2"
|
||||
data-test="add-action-script-title"
|
||||
>
|
||||
{{ t("actions.update") }}
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="text-h6 q-pl-sm"
|
||||
class="tw:text-xl tw:font-semibold tw:pl-2"
|
||||
data-test="add-action-script-title"
|
||||
>
|
||||
{{ t("actions.add") }}
|
||||
@@ -54,126 +54,97 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
>
|
||||
<div
|
||||
ref="addAlertFormRef"
|
||||
class="q-px-lg q-pb-md"
|
||||
class="tw:px-4 tw:pb-3"
|
||||
style="width: 1024px"
|
||||
>
|
||||
<q-form
|
||||
<div
|
||||
class="create-report-form"
|
||||
ref="addActionScriptFormRef"
|
||||
@submit="onSubmit"
|
||||
>
|
||||
<div
|
||||
data-test="add-action-script-name-input"
|
||||
class="report-name-input"
|
||||
style="padding-top: 12px"
|
||||
>
|
||||
<q-input
|
||||
<OInput
|
||||
v-model.trim="formData.name"
|
||||
:label="t('alerts.name') + ' *'"
|
||||
class="showLabelOnTop"
|
||||
stack-label
|
||||
borderless
|
||||
dense
|
||||
:rules="[
|
||||
(val: any) =>
|
||||
!!val
|
||||
? isValidResourceName(val) ||
|
||||
`Characters like :, ?, /, #, and spaces are not allowed.`
|
||||
: t('common.nameRequired'),
|
||||
]"
|
||||
:error="!!nameError"
|
||||
:error-message="nameError"
|
||||
tabindex="0"
|
||||
style="width: 400px"
|
||||
@update:model-value="(v: string) => {
|
||||
if (!v) { nameError = t('common.nameRequired'); }
|
||||
else if (!isValidResourceName(v)) { nameError = 'Characters like :, ?, /, #, and spaces are not allowed.'; }
|
||||
else { nameError = ''; }
|
||||
}"
|
||||
>
|
||||
<template v-slot:hint>
|
||||
<template #hint>
|
||||
Characters like :, ?, /, #, and spaces are not allowed.
|
||||
</template>
|
||||
</q-input>
|
||||
</OInput>
|
||||
</div>
|
||||
<div
|
||||
data-test="add-action-script-description-input"
|
||||
class="report-name-input q-pb-sm"
|
||||
>
|
||||
<q-input
|
||||
<div data-test="add-action-script-description-input" class="report-name-input tw:pb-2">
|
||||
<OInput
|
||||
v-model="formData.description"
|
||||
:label="t('reports.description')"
|
||||
class="showLabelOnTop"
|
||||
stack-label
|
||||
borderless
|
||||
dense
|
||||
tabindex="0"
|
||||
style="width: 800px"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div data-test="add-action-script-type" class="report-name-input">
|
||||
<q-select
|
||||
<div data-test="add-action-script-type" class="report-name-input tw:mb-3">
|
||||
<OSelect
|
||||
data-test="add-action-script-type-select"
|
||||
v-model="formData.type"
|
||||
:label="t('common.type') + ' *'"
|
||||
:options="actionTypes"
|
||||
labelKey="label"
|
||||
valueKey="value"
|
||||
class="showLabelOnTop no-case tw:w-[400px]"
|
||||
stack-label
|
||||
map-options
|
||||
emit-value
|
||||
borderless
|
||||
dense
|
||||
:rules="[(val: any) => !!val || 'Field is required!']"
|
||||
:disable="isEditingActionScript"
|
||||
:readonly="isEditingActionScript"
|
||||
:error="!!typeError"
|
||||
:error-message="typeError"
|
||||
:disabled="isEditingActionScript"
|
||||
@update:model-value="(v: any) => { typeError = v ? '' : 'Field is required!'; }"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<q-stepper
|
||||
<OStepper
|
||||
v-model="step"
|
||||
vertical
|
||||
color="primary"
|
||||
orientation="vertical"
|
||||
animated
|
||||
class="q-mb-md"
|
||||
header-nav
|
||||
navigable
|
||||
class="tw:mb-3"
|
||||
>
|
||||
<q-step
|
||||
<OStep
|
||||
data-test="add-action-script-step-1"
|
||||
:name="1"
|
||||
:title="t('actions.uploadCodeZip')"
|
||||
:icon="outlinedDashboard"
|
||||
icon="edit"
|
||||
:done="step > 1"
|
||||
>
|
||||
<div
|
||||
data-test="add-action-script-file-input"
|
||||
class="flex items-center"
|
||||
class="tw:flex tw:items-center"
|
||||
>
|
||||
<q-file
|
||||
<OFile
|
||||
v-if="
|
||||
!isEditingActionScript || formData.fileNameToShow == ''
|
||||
"
|
||||
ref="fileInput"
|
||||
color="primary"
|
||||
filled
|
||||
v-model="formData.codeZip"
|
||||
:label="t('actions.zipFile') + ' *'"
|
||||
bg-color="input-bg"
|
||||
class="tw:w-[300px] q-pt-md q-pb-sm showLabelOnTop lookup-table-file-uploader"
|
||||
stack-label
|
||||
outlined
|
||||
dense
|
||||
accept=".zip"
|
||||
:rules="[
|
||||
(val: any) => {
|
||||
if (!isEditingActionScript) {
|
||||
return !!val || 'ZIP File is required!';
|
||||
}
|
||||
return true;
|
||||
},
|
||||
]"
|
||||
data-test="add-action-script-file-input"
|
||||
:error-message="!isEditingActionScript && !formData.codeZip ? 'ZIP File is required!' : undefined"
|
||||
:error="!isEditingActionScript && !formData.codeZip && step > 1"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<q-icon name="attachment" />
|
||||
</template>
|
||||
<template v-slot:hint>
|
||||
<template #hint>
|
||||
Note: Only .zip files are accepted and it may contain
|
||||
various resources such as .py, .txt and main.py file etc.
|
||||
</template>
|
||||
</q-file>
|
||||
</OFile>
|
||||
|
||||
<div
|
||||
v-else-if="
|
||||
@@ -186,14 +157,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
@click="editFileToUpload"
|
||||
><q-icon name="edit" size="16px"
|
||||
><OIcon name="edit" size="sm"
|
||||
/></OButton>
|
||||
</div>
|
||||
<div
|
||||
v-if="
|
||||
isEditingActionScript && formData.fileNameToShow == ''
|
||||
"
|
||||
class="q-pt-md q-mt-xs q-pl-md"
|
||||
class="tw:pt-3 tw:mt-1 tw:pl-3"
|
||||
>
|
||||
<OButton
|
||||
data-test="cancel-upload-new-btn-file"
|
||||
@@ -204,38 +175,35 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tw:flex tw:gap-2 tw:mt-8">
|
||||
<OButton
|
||||
data-test="add-action-script-step1-continue-btn"
|
||||
variant="primary"
|
||||
size="sm"
|
||||
@click="step++"
|
||||
>Continue</OButton
|
||||
>
|
||||
</div>
|
||||
</OStep>
|
||||
|
||||
<q-stepper-navigation>
|
||||
<div class="q-mt-sm">
|
||||
<OButton
|
||||
data-test="add-action-script-step1-continue-btn"
|
||||
variant="primary"
|
||||
size="sm"
|
||||
@click="step = 2"
|
||||
>Continue</OButton
|
||||
>
|
||||
</div>
|
||||
</q-stepper-navigation>
|
||||
</q-step>
|
||||
|
||||
<q-step
|
||||
<OStep
|
||||
v-if="formData.type === 'scheduled'"
|
||||
data-test="add-action-script-step-2"
|
||||
:name="2"
|
||||
title="Schedule"
|
||||
icon="schedule"
|
||||
:done="step > 2"
|
||||
class="q-mt-md"
|
||||
class="tw:mt-3"
|
||||
>
|
||||
<div class="q-my-sm q-px-sm">
|
||||
<div class="tw:my-2 tw:px-2">
|
||||
<div
|
||||
style="font-size: 14px"
|
||||
class="text-bold text-grey-8 q-mb-sm"
|
||||
class="tw:font-bold tw:text-gray-500 tw:mb-2"
|
||||
data-test="add-action-script-frequency-title"
|
||||
>
|
||||
{{ t("actions.frequency") }} *
|
||||
</div>
|
||||
<div class="q-pa-xs el-border-radius el-border tw:w-fit">
|
||||
<div class="tw:p-1 el-border-radius el-border tw:w-fit">
|
||||
<template
|
||||
v-for="visual in frequencyTabs"
|
||||
:key="visual.value"
|
||||
@@ -254,249 +222,207 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
<div
|
||||
v-if="frequency.type === 'once'"
|
||||
class="flex justify-start items-center q-mt-md"
|
||||
class="tw:flex tw:justify-start tw:items-center tw:mt-3"
|
||||
data-test="add-action-script-frequency-info"
|
||||
>
|
||||
<q-icon name="event" class="q-mr-sm" />
|
||||
<OIcon name="event" size="sm" class="tw:mr-2" />
|
||||
<div style="font-size: 14px">
|
||||
The script will be triggered immediately after it is saved
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="frequency.type === 'repeat'">
|
||||
<div class="flex">
|
||||
<div class="tw:flex">
|
||||
<div
|
||||
data-test="add-action-script-cron-input"
|
||||
class="q-mr-sm"
|
||||
class="tw:mr-2"
|
||||
style="padding-top: 8px; width: 320px"
|
||||
>
|
||||
<div
|
||||
class="q-mb-xs text-bold text-grey-8"
|
||||
class="tw:mb-1 tw:font-bold tw:text-gray-500"
|
||||
data-test="add-action-script-cron-expression-title"
|
||||
>
|
||||
{{ t("reports.cronExpression") + " *" }}
|
||||
<q-icon
|
||||
<OIcon
|
||||
data-test="add-action-script-cron-info"
|
||||
:name="outlinedInfo"
|
||||
size="17px"
|
||||
class="q-ml-xs cursor-pointer"
|
||||
:class="
|
||||
store.state.theme === 'dark'
|
||||
? 'text-grey-5'
|
||||
: 'text-grey-7'
|
||||
"
|
||||
name="info"
|
||||
size="sm"
|
||||
class="tw:ml-1 tw:cursor-pointer tw:text-gray-400"
|
||||
>
|
||||
<q-tooltip anchor="center right" self="center left">
|
||||
<span style="font-size: 14px">
|
||||
Pattern: * * * * * means every minute .
|
||||
<br />
|
||||
Format: [Minute 0-59] [Hour 0-23] [Day of Month
|
||||
1-31, 'L'] [Month 1-12] [Day of Week 0-7 or
|
||||
'1L-7L', 0 and 7 for Sunday].
|
||||
<br />
|
||||
Use '*' to represent any value, 'L' for the last
|
||||
day/weekday. <br />
|
||||
Example: 0 12 * * ? - Triggers at 12:00 PM
|
||||
daily. It specifies minute, hour, day of month,
|
||||
month, and day of week, respectively.</span
|
||||
>
|
||||
</q-tooltip>
|
||||
</q-icon>
|
||||
<OTooltip side="right" align="center">
|
||||
<template #content>
|
||||
<span style="font-size: 14px">
|
||||
Pattern: * * * * * means every minute .
|
||||
<br />
|
||||
Format: [Minute 0-59] [Hour 0-23] [Day of Month
|
||||
1-31, 'L'] [Month 1-12] [Day of Week 0-7 or
|
||||
'1L-7L', 0 and 7 for Sunday].
|
||||
<br />
|
||||
Use '*' to represent any value, 'L' for the last
|
||||
day/weekday. <br />
|
||||
Example: 0 12 * * ? - Triggers at 12:00 PM
|
||||
daily. It specifies minute, hour, day of month,
|
||||
month, and day of week, respectively.
|
||||
</span>
|
||||
</template>
|
||||
</OTooltip>
|
||||
</OIcon>
|
||||
</div>
|
||||
|
||||
<q-input
|
||||
<OInput
|
||||
v-model="frequency.cron"
|
||||
class="showLabelOnTop"
|
||||
type="text"
|
||||
borderless
|
||||
:rules="[
|
||||
(val: any) =>
|
||||
!!val.length
|
||||
? cronError.length
|
||||
? cronError
|
||||
: true
|
||||
: 'Field is required!',
|
||||
]"
|
||||
dense
|
||||
:error="!!cronFieldError"
|
||||
:error-message="cronFieldError"
|
||||
debounce="300"
|
||||
style="width: 100%"
|
||||
@update:model-value="
|
||||
validateFrequency(frequency.cron)
|
||||
"
|
||||
:disable="isEditingActionScript"
|
||||
@update:model-value="(v: string) => {
|
||||
cronFieldError = !v?.length ? 'Field is required!' : (cronError.length ? cronError : '');
|
||||
validateFrequency(frequency.cron);
|
||||
}"
|
||||
:disabled="isEditingActionScript"
|
||||
:readonly="isEditingActionScript"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<q-select
|
||||
<div class="tw:flex">
|
||||
<OSelect
|
||||
data-test="add-action-script-timezone-select"
|
||||
v-model="formData.timezone"
|
||||
:options="['UTC']"
|
||||
:label="t('actions.timezone') + ' *'"
|
||||
:loading="isFetchingServiceAccounts"
|
||||
:popup-content-style="{ textTransform: 'lowercase' }"
|
||||
class="showLabelOnTop no-case tw:mb-[2.4rem]"
|
||||
borderless
|
||||
stack-label
|
||||
dense
|
||||
use-input
|
||||
hide-selected
|
||||
fill-input
|
||||
:input-debounce="400"
|
||||
behavior="menu"
|
||||
disable
|
||||
:rules="[(val: any) => !!val || 'Field is required!']"
|
||||
style="
|
||||
min-width: 250px !important;
|
||||
width: 250px !important;
|
||||
"
|
||||
disabled
|
||||
style="min-width: 250px !important; width: 250px !important;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="tw:flex tw:gap-2 tw:mt-4">
|
||||
<OButton
|
||||
data-test="add-action-script-step2-back-btn"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@click="step--"
|
||||
>Back</OButton
|
||||
>
|
||||
<OButton
|
||||
data-test="add-action-script-step2-continue-btn"
|
||||
variant="primary"
|
||||
size="sm"
|
||||
@click="step++"
|
||||
>Continue</OButton
|
||||
>
|
||||
</div>
|
||||
</OStep>
|
||||
|
||||
<q-stepper-navigation>
|
||||
<div class="tw:flex tw:gap-2">
|
||||
<OButton
|
||||
data-test="add-action-script-step2-back-btn"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@click="step = 1"
|
||||
>Back</OButton
|
||||
>
|
||||
<OButton
|
||||
data-test="add-action-script-step2-continue-btn"
|
||||
variant="primary"
|
||||
size="sm"
|
||||
@click="step = 3"
|
||||
>Continue</OButton
|
||||
>
|
||||
</div>
|
||||
</q-stepper-navigation>
|
||||
</q-step>
|
||||
|
||||
<q-step
|
||||
<OStep
|
||||
data-test="add-action-script-step-3"
|
||||
:name="3"
|
||||
title="Select Service Account"
|
||||
:icon="outlinedDashboard"
|
||||
icon="dashboard"
|
||||
:done="step > 3"
|
||||
class="q-mt-md"
|
||||
class="tw:mt-3"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<div class="tw:flex tw:items-center">
|
||||
<div class="service-account-selector">
|
||||
<div
|
||||
data-test="add-action-script-service-account-title"
|
||||
class="q-mb-xs text-bold text-grey-8"
|
||||
class="tw:mb-1 tw:font-bold tw:text-gray-500"
|
||||
>
|
||||
{{ t("actions.serviceAccount") + " *" }}
|
||||
<q-icon
|
||||
:name="outlinedInfo"
|
||||
size="17px"
|
||||
class="q-ml-xs cursor-pointer"
|
||||
:class="
|
||||
store.state.theme === 'dark'
|
||||
? 'text-grey-5'
|
||||
: 'text-grey-7'
|
||||
"
|
||||
<OIcon
|
||||
name="info"
|
||||
size="sm"
|
||||
class="tw:ml-1 tw:cursor-pointer tw:text-gray-400"
|
||||
>
|
||||
<q-tooltip anchor="center right" self="center left">
|
||||
<span style="font-size: 14px">
|
||||
Make sure service account has permissions to access
|
||||
Actions.
|
||||
</span>
|
||||
</q-tooltip>
|
||||
</q-icon>
|
||||
<OTooltip side="right" align="center">
|
||||
<template #content>
|
||||
<span style="font-size: 14px">
|
||||
Make sure service account has permissions to access
|
||||
Actions.
|
||||
</span>
|
||||
</template>
|
||||
</OTooltip>
|
||||
</OIcon>
|
||||
</div>
|
||||
<q-select
|
||||
<OSelect
|
||||
data-test="add-action-script-service-account-select"
|
||||
v-model="formData.service_account"
|
||||
:options="filteredServiceAccounts"
|
||||
:loading="isFetchingServiceAccounts"
|
||||
:popup-content-style="{ textTransform: 'lowercase' }"
|
||||
class="q-py-sm no-case"
|
||||
borderless
|
||||
dense
|
||||
use-input
|
||||
hide-selected
|
||||
fill-input
|
||||
:input-debounce="400"
|
||||
@filter="filterServiceAccounts"
|
||||
behavior="menu"
|
||||
:rules="[(val: any) => !!val || 'Field is required!']"
|
||||
style="
|
||||
min-width: 250px !important;
|
||||
width: 250px !important;
|
||||
"
|
||||
class="tw:py-2 no-case"
|
||||
labelKey="label"
|
||||
valueKey="value"
|
||||
:error="!!serviceAccountError"
|
||||
:error-message="serviceAccountError"
|
||||
@update:model-value="(v: any) => { serviceAccountError = v ? '' : 'Field is required!'; }"
|
||||
@search="(val: string) => {
|
||||
filteredServiceAccounts.value = val
|
||||
? serviceAccountsOptions.filter((s: any) => s.label.toLowerCase().includes(val.toLowerCase()))
|
||||
: [...serviceAccountsOptions];
|
||||
}"
|
||||
style="min-width: 250px !important; width: 250px !important;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-stepper-navigation>
|
||||
<div class="tw:flex tw:gap-2">
|
||||
<OButton
|
||||
data-test="add-action-script-step3-back-btn"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@click="step = formData.type === 'scheduled' ? 2 : 1"
|
||||
>Back</OButton
|
||||
>
|
||||
<OButton
|
||||
data-test="add-action-script-step3-continue-btn"
|
||||
variant="primary"
|
||||
size="sm"
|
||||
@click="step = 4"
|
||||
>Continue</OButton
|
||||
>
|
||||
</div>
|
||||
</q-stepper-navigation>
|
||||
</q-step>
|
||||
<q-step
|
||||
<div class="tw:flex tw:gap-2 tw:mt-4">
|
||||
<OButton
|
||||
data-test="add-action-script-step3-back-btn"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@click="step === 3 ? (step = formData.type === 'scheduled' ? 2 : 1) : step--"
|
||||
>Back</OButton
|
||||
>
|
||||
<OButton
|
||||
data-test="add-action-script-step3-continue-btn"
|
||||
variant="primary"
|
||||
size="sm"
|
||||
@click="step++"
|
||||
>Continue</OButton
|
||||
>
|
||||
</div>
|
||||
</OStep>
|
||||
<OStep
|
||||
data-test="add-action-script-step-4"
|
||||
:name="4"
|
||||
title="Environmental Variables"
|
||||
icon="lock"
|
||||
:done="step > 4"
|
||||
class="q-mt-md"
|
||||
class="tw:mt-3"
|
||||
>
|
||||
<div
|
||||
v-for="(header, index) in environmentalVariables"
|
||||
:key="header.uuid"
|
||||
class="row q-col-gutter-sm"
|
||||
class="tw:flex tw:gap-2"
|
||||
data-test="add-action-script-env-variable"
|
||||
>
|
||||
<div class="col-5 q-ml-none">
|
||||
<q-input
|
||||
<div class="tw:w-5/12 tw:ml-0">
|
||||
<OInput
|
||||
:data-test="`add-action-script-header-${header['key']}-key-input`"
|
||||
v-model="header.key"
|
||||
stack-label
|
||||
borderless
|
||||
:placeholder="'Key'"
|
||||
dense
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-5 q-ml-none q-mb-sm">
|
||||
<q-input
|
||||
<div class="tw:w-5/12 tw:ml-0 tw:mb-2">
|
||||
<OInput
|
||||
:data-test="`add-action-script-header-${header['key']}-value-input`"
|
||||
v-model="header.value"
|
||||
:placeholder="t('alert_destinations.api_header_value')"
|
||||
stack-label
|
||||
borderless
|
||||
dense
|
||||
isUpdatingDestination
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-2 q-ml-none">
|
||||
<div class="tw:w-1/6 tw:ml-0">
|
||||
<OButton
|
||||
:data-test="`add-action-script-header-${header['key']}-delete-btn`"
|
||||
variant="ghost"
|
||||
size="icon-circle-sm"
|
||||
:title="t('alert_templates.delete')"
|
||||
@click="deleteApiHeader(header)"
|
||||
><q-icon name="delete" size="16px"
|
||||
><OIcon name="delete" size="sm"
|
||||
/></OButton>
|
||||
<OButton
|
||||
data-test="add-action-script-add-header-btn"
|
||||
@@ -505,28 +431,29 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
size="icon-circle-sm"
|
||||
:title="t('alert_templates.edit')"
|
||||
@click="addApiHeader()"
|
||||
><q-icon name="add" size="16px"
|
||||
><OIcon name="add" size="sm"
|
||||
/></OButton>
|
||||
</div>
|
||||
</div>
|
||||
<q-stepper-navigation>
|
||||
<div class="tw:flex tw:gap-2 tw:mt-4">
|
||||
<OButton
|
||||
data-test="add-action-script-step4-back-btn"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@click="step = 3"
|
||||
@click="step--"
|
||||
>Back</OButton
|
||||
>
|
||||
</q-stepper-navigation>
|
||||
</q-step>
|
||||
</q-stepper>
|
||||
</q-form>
|
||||
</div>
|
||||
</OStep>
|
||||
</OStepper>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tw:mx-2">
|
||||
<div
|
||||
class="tw:flex tw:justify-end tw:gap-2 q-px-md full-width tw:py-[0.625rem] card-container"
|
||||
class="tw:flex tw:justify-end tw:gap-2 tw:px-3 tw:w-full tw:py-[0.625rem] card-container"
|
||||
style="position: sticky; bottom: 0px; z-index: 2"
|
||||
>
|
||||
<OButton
|
||||
@@ -571,19 +498,24 @@ import {
|
||||
} from "@/utils/zincutils";
|
||||
import VariablesInput from "@/components/alerts/VariablesInput.vue";
|
||||
import { useStore } from "vuex";
|
||||
import { outlinedDashboard } from "@quasar/extras/material-icons-outlined";
|
||||
import dashboardService from "@/services/dashboards";
|
||||
import { onBeforeMount } from "vue";
|
||||
import type { Ref } from "vue";
|
||||
import { DateTime as _DateTime } from "luxon";
|
||||
import actions from "@/services/action_scripts";
|
||||
import { useQuasar } from "quasar";
|
||||
import ConfirmDialog from "@/components/ConfirmDialog.vue";
|
||||
import CronExpressionParser from "cron-parser";
|
||||
import { outlinedInfo } from "@quasar/extras/material-icons-outlined";
|
||||
import { convertDateToTimestamp } from "@/utils/date";
|
||||
import service_accounts from "@/services/service_accounts";
|
||||
import OButton from "@/lib/core/Button/OButton.vue";
|
||||
import OIcon from "@/lib/core/Icon/OIcon.vue";
|
||||
import OInput from "@/lib/forms/Input/OInput.vue";
|
||||
import OSelect from "@/lib/forms/Select/OSelect.vue";
|
||||
import OTooltip from "@/lib/overlay/Tooltip/OTooltip.vue";
|
||||
import OStepper from "@/lib/navigation/Stepper/OStepper.vue";
|
||||
import OStep from "@/lib/navigation/Stepper/OStep.vue";
|
||||
import OFile from "@/lib/forms/File/OFile.vue";
|
||||
import { toast } from "@/lib/feedback/Toast/useToast";
|
||||
|
||||
defineProps({
|
||||
report: {
|
||||
@@ -627,13 +559,10 @@ const fileInput = ref(null);
|
||||
|
||||
const originalActionScriptData: Ref<string> = ref("");
|
||||
|
||||
const addActionScriptFormRef: Ref<any> = ref(null);
|
||||
|
||||
const step = ref(1);
|
||||
|
||||
const formData = ref(defaultActionScript);
|
||||
|
||||
const q = useQuasar();
|
||||
|
||||
const actionTypes = [
|
||||
{
|
||||
@@ -699,6 +628,10 @@ const isFetchingActionScript = ref(false);
|
||||
|
||||
const environmentalVariables = ref([{ key: "", value: "", uuid: getUUID() }]);
|
||||
const cronError = ref("");
|
||||
const nameError = ref("");
|
||||
const typeError = ref("");
|
||||
const cronFieldError = ref("");
|
||||
const serviceAccountError = ref("");
|
||||
|
||||
const frequency = ref({
|
||||
type: "once",
|
||||
@@ -720,8 +653,6 @@ onBeforeMount(async () => {
|
||||
await handleActionScript();
|
||||
});
|
||||
|
||||
const onSubmit = () => {};
|
||||
|
||||
const scheduling = ref({
|
||||
date: "",
|
||||
time: "",
|
||||
@@ -882,9 +813,18 @@ const saveActionScript = async () => {
|
||||
try {
|
||||
validateActionScriptData();
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
const isValidForm = await addActionScriptFormRef.value.validate();
|
||||
if (!isValidForm) return;
|
||||
// Inline validation for migrated O2 fields
|
||||
nameError.value = !formData.value.name
|
||||
? t('common.nameRequired')
|
||||
: !isValidResourceName(formData.value.name)
|
||||
? 'Characters like :, ?, /, #, and spaces are not allowed.'
|
||||
: '';
|
||||
typeError.value = formData.value.type ? '' : 'Field is required!';
|
||||
serviceAccountError.value = formData.value.service_account ? '' : 'Field is required!';
|
||||
if (formData.value.execution_details === 'repeat') {
|
||||
cronFieldError.value = !frequency.value.cron?.length ? 'Field is required!' : (cronError.value.length ? cronError.value : '');
|
||||
}
|
||||
if (nameError.value || typeError.value || serviceAccountError.value || cronFieldError.value) return;
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
@@ -894,8 +834,8 @@ const saveActionScript = async () => {
|
||||
? actions.update
|
||||
: actions.create;
|
||||
|
||||
const dismiss = q.notify({
|
||||
spinner: true,
|
||||
const dismiss = toast({
|
||||
variant: "loading",
|
||||
message: "Please wait...",
|
||||
timeout: 2000,
|
||||
});
|
||||
@@ -904,8 +844,8 @@ const saveActionScript = async () => {
|
||||
|
||||
updateAction(store.state.selectedOrganization.identifier, actionId, form)
|
||||
.then(() => {
|
||||
q.notify({
|
||||
type: "positive",
|
||||
toast({
|
||||
variant: "success",
|
||||
message: `Action ${
|
||||
isEditingActionScript.value ? "updated" : "saved"
|
||||
} successfully.`,
|
||||
@@ -917,8 +857,8 @@ const saveActionScript = async () => {
|
||||
.catch((error) => {
|
||||
step.value = 3;
|
||||
if (error.response.status != 403) {
|
||||
q.notify({
|
||||
type: "negative",
|
||||
toast({
|
||||
variant: "error",
|
||||
message:
|
||||
error?.response?.data?.message ||
|
||||
`Error while ${
|
||||
@@ -1129,8 +1069,8 @@ const handleActionScript = async () => {
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.response.status != 403) {
|
||||
q.notify({
|
||||
type: "negative",
|
||||
toast({
|
||||
variant: "error",
|
||||
message: err?.data?.message || "Error while fetching Action!",
|
||||
timeout: 4000,
|
||||
});
|
||||
@@ -1171,8 +1111,8 @@ const getServiceAccounts = async () => {
|
||||
filteredServiceAccounts.value = [...serviceAccountsOptions];
|
||||
} catch (err: any) {
|
||||
if (err.response?.status != 403) {
|
||||
q.notify({
|
||||
type: "negative",
|
||||
toast({
|
||||
variant: "error",
|
||||
message:
|
||||
err.response?.data?.message ||
|
||||
"Error while fetching service accounts.",
|
||||
@@ -1189,14 +1129,6 @@ onMounted(async () => {
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.create-report-page {
|
||||
.q-expansion-item .q-item {
|
||||
padding: 0 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.lookup-table-file-uploader {
|
||||
:deep(.q-field__label) {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { mount, VueWrapper } from "@vue/test-utils";
|
||||
import { nextTick } from "vue";
|
||||
import FileItem from "@/components/actionScripts/FileItem.vue";
|
||||
import { Quasar } from "quasar";
|
||||
|
||||
|
||||
describe("FileItem.vue", () => {
|
||||
let wrapper: VueWrapper;
|
||||
@@ -24,9 +24,9 @@ describe("FileItem.vue", () => {
|
||||
return mount(FileItem, {
|
||||
props: { ...defaultProps, ...props },
|
||||
global: {
|
||||
plugins: [Quasar],
|
||||
plugins: [],
|
||||
stubs: {
|
||||
"q-icon": true,
|
||||
"OIcon": true,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -65,8 +65,9 @@ describe("FileItem.vue", () => {
|
||||
const editButton = buttons[0];
|
||||
const deleteButton = buttons[1];
|
||||
|
||||
expect(editButton.find("q-icon-stub").attributes("name")).toBe("edit");
|
||||
expect(deleteButton.find("q-icon-stub").attributes("name")).toBe(
|
||||
// When stubs: { OIcon: true }, Vue renders the stub as <o-icon-stub> (lowercase kebab)
|
||||
expect(editButton.find("o-icon-stub").attributes("name")).toBe("edit");
|
||||
expect(deleteButton.find("o-icon-stub").attributes("name")).toBe(
|
||||
"delete",
|
||||
);
|
||||
});
|
||||
@@ -284,7 +285,7 @@ describe("FileItem.vue", () => {
|
||||
describe("Button Styling", () => {
|
||||
it("applies light gray styling to buttons when active", () => {
|
||||
wrapper = createWrapper({ isActive: true });
|
||||
const icons = wrapper.findAll(".file-actions .q-icon");
|
||||
const icons = wrapper.findAll(".file-actions .OIcon");
|
||||
|
||||
icons.forEach((icon) => {
|
||||
expect(icon.classes()).toContain("tw:text-gray-100");
|
||||
@@ -294,7 +295,7 @@ describe("FileItem.vue", () => {
|
||||
|
||||
it("applies dark gray styling to buttons when not active", () => {
|
||||
wrapper = createWrapper({ isActive: false });
|
||||
const icons = wrapper.findAll(".file-actions .q-icon");
|
||||
const icons = wrapper.findAll(".file-actions .OIcon");
|
||||
|
||||
icons.forEach((icon) => {
|
||||
expect(icon.classes()).toContain("tw:text-gray-600");
|
||||
|
||||
@@ -24,10 +24,10 @@
|
||||
class="tw:w-[36px] tw:flex tw:items-center tw:space-x-2 tw:ml-auto file-actions"
|
||||
>
|
||||
<OButton variant="ghost" size="icon-xs-sq" @click.stop="editFile">
|
||||
<q-icon name="edit" :class="isActive ? 'tw:text-gray-100' : 'tw:text-gray-600'" />
|
||||
<OIcon name="edit" size="sm" :class="isActive ? 'tw:text-gray-100' : 'tw:text-gray-600'" />
|
||||
</OButton>
|
||||
<OButton variant="ghost" size="icon-xs-sq" @click.stop="deleteFile">
|
||||
<q-icon name="delete" :class="isActive ? 'tw:text-gray-100' : 'tw:text-gray-600'" />
|
||||
<OIcon name="delete" size="sm" :class="isActive ? 'tw:text-gray-100' : 'tw:text-gray-600'" />
|
||||
</OButton>
|
||||
</div>
|
||||
</li>
|
||||
@@ -36,6 +36,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, nextTick, watch } from "vue";
|
||||
import OButton from "@/lib/core/Button/OButton.vue";
|
||||
import OIcon from "@/lib/core/Icon/OIcon.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
name: string;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { mount, VueWrapper } from '@vue/test-utils';
|
||||
import ScriptEditor from '@/components/actionScripts/ScriptEditor.vue';
|
||||
import { Quasar } from 'quasar';
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('axios');
|
||||
@@ -11,6 +10,7 @@ vi.mock('vue-i18n', () => ({
|
||||
})
|
||||
}));
|
||||
|
||||
|
||||
describe('ScriptEditor.vue', () => {
|
||||
let wrapper: VueWrapper;
|
||||
|
||||
@@ -35,7 +35,7 @@ describe('ScriptEditor.vue', () => {
|
||||
return mount(ScriptEditor, {
|
||||
props: { ...defaultProps, ...props },
|
||||
global: {
|
||||
plugins: [Quasar],
|
||||
plugins: [],
|
||||
stubs: {
|
||||
'FullViewContainer': {
|
||||
template: '<div><slot name="left"></slot><slot></slot></div>',
|
||||
@@ -45,17 +45,18 @@ describe('ScriptEditor.vue', () => {
|
||||
template: '<div data-test="vrl-function-test-events-editor"></div>',
|
||||
props: ['editorId', 'language', 'query']
|
||||
},
|
||||
'q-spinner-hourglass': {
|
||||
template: '<div class="spinner"></div>',
|
||||
props: ['size']
|
||||
},
|
||||
'q-icon': {
|
||||
template: '<div class="icon"><q-tooltip-stub v-if="$slots.default"><slot></slot></q-tooltip-stub></div>',
|
||||
'OIcon': {
|
||||
template: '<div class="icon" :data-name="name"><slot></slot></div>',
|
||||
props: ['name', 'size']
|
||||
},
|
||||
'q-tooltip': {
|
||||
template: '<div class="tooltip"><slot></slot></div>',
|
||||
props: ['anchor', 'self', 'offset']
|
||||
// The source uses OTooltip (O2 lib), not q-tooltip.
|
||||
'OTooltip': {
|
||||
template: '<div class="o-tooltip" :data-content="content"><slot></slot></div>',
|
||||
props: ['side', 'align', 'sideOffset', 'content']
|
||||
},
|
||||
'OSpinner': {
|
||||
template: '<div data-test="script-editor-loading-indicator"></div>',
|
||||
props: ['size']
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -149,7 +150,7 @@ describe('ScriptEditor.vue', () => {
|
||||
it('shows loading spinner when loading is true', () => {
|
||||
wrapper = createWrapper({ loading: true });
|
||||
|
||||
const spinner = wrapper.find('.spinner');
|
||||
const spinner = wrapper.find('[data-test="script-editor-loading-indicator"]');
|
||||
expect(spinner.exists()).toBe(true);
|
||||
});
|
||||
|
||||
@@ -162,7 +163,7 @@ describe('ScriptEditor.vue', () => {
|
||||
it('does not show loading elements when loading is false', () => {
|
||||
wrapper = createWrapper({ loading: false });
|
||||
|
||||
const spinner = wrapper.find('.spinner');
|
||||
const spinner = wrapper.find('[data-test="script-editor-loading-indicator"]');
|
||||
expect(spinner.exists()).toBe(false);
|
||||
});
|
||||
|
||||
@@ -191,8 +192,9 @@ describe('ScriptEditor.vue', () => {
|
||||
it('shows error tooltip with correct message', () => {
|
||||
const errorMessage = 'Test error message';
|
||||
wrapper = createWrapper({ error: errorMessage });
|
||||
|
||||
const tooltip = wrapper.find('.tooltip');
|
||||
|
||||
// OTooltip stub renders with class "o-tooltip" and data-content attribute
|
||||
const tooltip = wrapper.find('.o-tooltip');
|
||||
expect(tooltip.exists()).toBe(true);
|
||||
});
|
||||
|
||||
@@ -205,8 +207,9 @@ describe('ScriptEditor.vue', () => {
|
||||
|
||||
it('configures tooltip positioning correctly', () => {
|
||||
wrapper = createWrapper({ error: 'Error' });
|
||||
|
||||
const tooltip = wrapper.find('.tooltip');
|
||||
|
||||
// OTooltip stub renders with class "o-tooltip"
|
||||
const tooltip = wrapper.find('.o-tooltip');
|
||||
expect(tooltip.exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -367,7 +370,7 @@ describe('ScriptEditor.vue', () => {
|
||||
it('handles both loading and error states simultaneously', () => {
|
||||
wrapper = createWrapper({ loading: true, error: 'Test error' });
|
||||
|
||||
const spinner = wrapper.find('.spinner');
|
||||
const spinner = wrapper.find('[data-test="script-editor-loading-indicator"]');
|
||||
const errorIcon = wrapper.find('.icon');
|
||||
|
||||
expect(spinner.exists()).toBe(true);
|
||||
@@ -377,8 +380,9 @@ describe('ScriptEditor.vue', () => {
|
||||
it('handles very long error messages', () => {
|
||||
const longError = 'A very long error message that might cause layout issues. '.repeat(10);
|
||||
wrapper = createWrapper({ error: longError });
|
||||
|
||||
const tooltip = wrapper.find('.tooltip');
|
||||
|
||||
// OTooltip stub renders with class "o-tooltip"
|
||||
const tooltip = wrapper.find('.o-tooltip');
|
||||
expect(tooltip.exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,26 +11,18 @@
|
||||
v-if="loading"
|
||||
class="text-weight-bold tw:flex tw:items-center tw:text-gray-500 tw:ml-2 tw:text-[13px]"
|
||||
>
|
||||
<q-spinner-hourglass size="18px" />
|
||||
<OSpinner size="xs" data-test="script-editor-loading-indicator" />
|
||||
<div class="tw:relative tw:top-[2px]">
|
||||
{{ t("confirmDialog.loading") }}
|
||||
</div>
|
||||
</div>
|
||||
<q-icon
|
||||
<OIcon
|
||||
v-if="!!error"
|
||||
name="info"
|
||||
class="tw:text-red-600 tw:mx-1 tw:cursor-pointer"
|
||||
size="16px"
|
||||
>
|
||||
<q-tooltip
|
||||
anchor="center right"
|
||||
self="center left"
|
||||
:offset="[10, 10]"
|
||||
class="tw:text-[12px]"
|
||||
>
|
||||
{{ error }}
|
||||
</q-tooltip>
|
||||
</q-icon>
|
||||
size="sm"
|
||||
/>
|
||||
<OTooltip side="right" align="center" :sideOffset="10" :content="error" />
|
||||
</template>
|
||||
</FullViewContainer>
|
||||
<div
|
||||
@@ -56,6 +48,9 @@ import FullViewContainer from "@/components/functions/FullViewContainer.vue";
|
||||
const QueryEditor = defineAsyncComponent(() => import("@/components/CodeQueryEditor.vue"));
|
||||
import axios from "axios";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import OSpinner from "@/lib/feedback/Spinner/OSpinner.vue";
|
||||
import OIcon from "@/lib/core/Icon/OIcon.vue";
|
||||
import OTooltip from "@/lib/overlay/Tooltip/OTooltip.vue";
|
||||
|
||||
const props = defineProps({
|
||||
loading: {
|
||||
|
||||
@@ -3,7 +3,6 @@ import { mount, VueWrapper } from '@vue/test-utils';
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
import { createStore } from 'vuex';
|
||||
import ScriptToolbar from '@/components/actionScripts/ScriptToolbar.vue';
|
||||
import { Quasar } from 'quasar';
|
||||
|
||||
// Mock vue-i18n
|
||||
vi.mock('vue-i18n', () => ({
|
||||
@@ -12,6 +11,13 @@ vi.mock('vue-i18n', () => ({
|
||||
})
|
||||
}));
|
||||
|
||||
// ScriptToolbar calls toggleFullscreen() from @/utils/dom directly — mock at the module boundary.
|
||||
const mockToggleFullscreen = vi.fn();
|
||||
vi.mock('@/utils/dom', () => ({
|
||||
toggleFullscreen: () => mockToggleFullscreen(),
|
||||
}));
|
||||
|
||||
|
||||
describe('ScriptToolbar.vue', () => {
|
||||
let wrapper: VueWrapper;
|
||||
let router: any;
|
||||
@@ -75,8 +81,8 @@ describe('ScriptToolbar.vue', () => {
|
||||
_q_: mockQuasar
|
||||
},
|
||||
stubs: {
|
||||
'q-icon': {
|
||||
template: '<div class="q-icon-stub" :name="name"><q-tooltip-stub v-if="$slots.default"><slot></slot></q-tooltip-stub></div>',
|
||||
'OIcon': {
|
||||
template: '<div class="OIcon-stub" :name="name"><q-tooltip-stub v-if="$slots.default"><slot></slot></q-tooltip-stub></div>',
|
||||
props: ['name', 'size']
|
||||
},
|
||||
'q-input': {
|
||||
@@ -118,18 +124,19 @@ describe('ScriptToolbar.vue', () => {
|
||||
it('renders back button with correct styling', () => {
|
||||
wrapper = createWrapper();
|
||||
const backBtn = wrapper.find('[data-test="add-script-back-btn"]');
|
||||
|
||||
|
||||
expect(backBtn.exists()).toBe(true);
|
||||
expect(backBtn.attributes('title')).toBe('Go Back');
|
||||
expect(backBtn.classes()).toContain('cursor-pointer');
|
||||
// The back button uses Tailwind tw: prefix classes, not bare cursor-pointer
|
||||
expect(backBtn.classes()).toContain('tw:cursor-pointer');
|
||||
});
|
||||
|
||||
it('renders name input field', () => {
|
||||
wrapper = createWrapper();
|
||||
const nameInput = wrapper.find('.q-input-stub');
|
||||
|
||||
// The component uses OInput (O2 lib), queried by its data-test attribute
|
||||
const nameInput = wrapper.find('[data-test="add-script-name-input"]');
|
||||
|
||||
expect(nameInput.exists()).toBe(true);
|
||||
expect(nameInput.attributes('data-test')).toBe('add-script-name-input');
|
||||
});
|
||||
|
||||
it('renders action buttons', () => {
|
||||
@@ -161,10 +168,10 @@ describe('ScriptToolbar.vue', () => {
|
||||
|
||||
it('passes disableName to input field', () => {
|
||||
wrapper = createWrapper({ disableName: true });
|
||||
const nameInput = wrapper.find('.q-input-stub');
|
||||
|
||||
// The component uses OInput (O2 lib), queried by its data-test attribute
|
||||
const nameInput = wrapper.find('[data-test="add-script-name-input"]');
|
||||
|
||||
expect(nameInput.exists()).toBe(true);
|
||||
// In actual component, these props would be passed through
|
||||
});
|
||||
});
|
||||
|
||||
@@ -265,11 +272,11 @@ describe('ScriptToolbar.vue', () => {
|
||||
it('handles fullscreen toggle', async () => {
|
||||
wrapper = createWrapper();
|
||||
const fullscreenBtn = wrapper.find('[data-test="add-script-fullscreen-btn"]');
|
||||
const vm = wrapper.vm as any;
|
||||
|
||||
|
||||
await fullscreenBtn.trigger('click');
|
||||
|
||||
expect(mockQuasar.fullscreen.toggle).toHaveBeenCalled();
|
||||
|
||||
// Component calls toggleFullscreen() from @/utils/dom — verified via module mock
|
||||
expect(mockToggleFullscreen).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('emits save event and shows input error when save button is clicked', async () => {
|
||||
@@ -317,8 +324,9 @@ describe('ScriptToolbar.vue', () => {
|
||||
describe('Input Field Configuration', () => {
|
||||
it('configures input field', () => {
|
||||
wrapper = createWrapper();
|
||||
const nameInput = wrapper.find('.q-input-stub');
|
||||
|
||||
// The component uses OInput (O2 lib), queried by its data-test attribute
|
||||
const nameInput = wrapper.find('[data-test="add-script-name-input"]');
|
||||
|
||||
expect(nameInput.exists()).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<div class="tw:mr-2 add-script-back-btn">
|
||||
<div
|
||||
data-test="add-script-back-btn"
|
||||
class="flex justify-center items-center cursor-pointer"
|
||||
class="tw:flex tw:justify-center tw:items-center tw:cursor-pointer"
|
||||
style="
|
||||
border: 1.5px solid;
|
||||
border-radius: 50%;
|
||||
@@ -16,57 +16,39 @@
|
||||
title="Go Back"
|
||||
@click="redirectToScripts"
|
||||
>
|
||||
<q-icon name="arrow_back_ios_new" size="14px" />
|
||||
<OIcon name="arrow-back-ios-new" size="xs" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="tw:text-lg tw:w-full add-script-title q-mr-md">
|
||||
<div class="tw:text-lg tw:w-full add-script-title tw:mr-3">
|
||||
Add Action
|
||||
</div>
|
||||
<q-form ref="addScriptForm" class="o2-input">
|
||||
<div class="o2-input">
|
||||
<div class="tw:flex tw:items-center">
|
||||
<q-input
|
||||
<OInput
|
||||
data-test="add-script-name-input"
|
||||
v-model.trim="actionName"
|
||||
:label="t('actions.name')"
|
||||
color="input-border"
|
||||
bg-color="input-bg"
|
||||
class="q-pa-none tw:w-full"
|
||||
stack-label
|
||||
outlined
|
||||
filled
|
||||
dense
|
||||
v-bind:readonly="disableName"
|
||||
v-bind:disable="disableName"
|
||||
:rules="[
|
||||
(val: any) => !!val || 'Field is required!',
|
||||
isValidMethodName,
|
||||
]"
|
||||
no-error-icon
|
||||
tabindex="0"
|
||||
style="min-width: 300px"
|
||||
class="tw:p-0 tw:w-full"
|
||||
:readonly="disableName"
|
||||
:disabled="disableName"
|
||||
:error="!!scriptNameError"
|
||||
:error-message="scriptNameError"
|
||||
@update:model-value="onUpdate"
|
||||
@blur="onUpdate"
|
||||
style="min-width: 300px"
|
||||
/>
|
||||
<q-icon
|
||||
<OIcon
|
||||
:key="actionName"
|
||||
v-if="isValidMethodName() !== true && showInputError"
|
||||
:name="outlinedInfo"
|
||||
size="20px"
|
||||
class="q-ml-xs cursor-pointer"
|
||||
name="info"
|
||||
size="md"
|
||||
class="tw:ml-1 tw:cursor-pointer"
|
||||
:class="store.state.theme === 'dark' ? 'text-red-5' : 'text-red-7'"
|
||||
>
|
||||
<q-tooltip
|
||||
anchor="center right"
|
||||
self="center left"
|
||||
max-width="300px"
|
||||
:offset="[2, 0]"
|
||||
class="tw:text-[12px]"
|
||||
>
|
||||
{{ isValidMethodName() }}
|
||||
</q-tooltip>
|
||||
</q-icon>
|
||||
<OTooltip side="right" :content="String(isValidMethodName())" />
|
||||
</OIcon>
|
||||
</div>
|
||||
</q-form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="add-script-actions tw:flex tw:items-center tw:gap-2">
|
||||
<OButton
|
||||
@@ -75,7 +57,7 @@
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@click="handleFullScreen"
|
||||
><q-icon name="fullscreen" size="18px" class="tw:mr-1" />{{
|
||||
><OIcon name="fullscreen" size="sm" class="tw:mr-1" />{{
|
||||
t("common.fullscreen")
|
||||
}}</OButton
|
||||
>
|
||||
@@ -100,16 +82,16 @@
|
||||
<script setup lang="ts">
|
||||
import { defineComponent, ref, onMounted, computed, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useQuasar } from "quasar";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useStore } from "vuex";
|
||||
import { outlinedInfo } from "@quasar/extras/material-icons-outlined";
|
||||
import OButton from "@/lib/core/Button/OButton.vue";
|
||||
import OInput from "@/lib/forms/Input/OInput.vue";
|
||||
import OTooltip from "@/lib/overlay/Tooltip/OTooltip.vue";
|
||||
import OIcon from "@/lib/core/Icon/OIcon.vue";
|
||||
import { toggleFullscreen } from "@/utils/dom";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const q = useQuasar();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const store = useStore();
|
||||
@@ -129,6 +111,13 @@ const emit = defineEmits(["test", "save", "update:name", "back", "cancel"]);
|
||||
|
||||
const addScriptForm = ref(null);
|
||||
|
||||
const scriptNameError = computed(() => {
|
||||
if (!showInputError.value) return '';
|
||||
if (!actionName.value) return 'Field is required!';
|
||||
const result = isValidMethodName();
|
||||
return result === true ? '' : result;
|
||||
});
|
||||
|
||||
const showInputError = ref(false);
|
||||
|
||||
const isValidMethodName = () => {
|
||||
@@ -150,7 +139,7 @@ const actionName = computed({
|
||||
});
|
||||
|
||||
const handleFullScreen = () => {
|
||||
q.fullscreen.toggle();
|
||||
toggleFullscreen();
|
||||
};
|
||||
|
||||
const redirectToScripts = () => {
|
||||
|
||||
Reference in New Issue
Block a user