diff --git a/ui/src/components/filter/KestraFilter.vue b/ui/src/components/filter/KestraFilter.vue index 4932e6ff17b..e4068190e72 100644 --- a/ui/src/components/filter/KestraFilter.vue +++ b/ui/src/components/filter/KestraFilter.vue @@ -7,7 +7,6 @@ :model-value="current" value-key="label" :placeholder="t('filters.label')" - allow-create default-first-option filterable clearable @@ -18,6 +17,7 @@ popper-class="filters-select" :class="{settings: settings.shown, refresh: refresh.shown}" @change="(value) => changeCallback(value)" + @keyup="(e) => handleInputChange(e.key)" @keyup.enter="() => handleEnterKey(select?.hoverOption?.value)" @remove-tag="(item) => removeItem(item)" @visible-change="(visible) => dropdownClosedCallback(visible)" @@ -124,7 +124,7 @@ import DateRange from "../layout/DateRange.vue"; - const emits = defineEmits(["dashboard"]); + const emits = defineEmits(["dashboard", "input"]); const props = defineProps({ prefix: {type: String, required: true}, include: {type: Array, required: true}, @@ -196,6 +196,14 @@ } }; + const handleInputChange = (key) => { + if (key === "Enter") return; + + if (current.value.at(-1)?.label === "user") { + emits("input", select.value.states.inputValue); + } + }; + const handleClear = () => { current.value = []; triggerSearch(); @@ -228,10 +236,11 @@ }; const comparatorCallback = (value) => { current.value[dropdowns.value.second.index].comparator = value; - emptyLabel.value = - current.value[dropdowns.value.second.index].label === "labels" - ? t("filters.labels.placeholder") - : t("filters.empty"); + emptyLabel.value = ["labels", "details"].includes( + current.value[dropdowns.value.second.index].label, + ) + ? t("filters.key_value_type") + : t("filters.empty"); dropdowns.value.first = {shown: false, value: {}}; dropdowns.value.second = {shown: false, index: -1}; @@ -339,6 +348,12 @@ case "scope": return VALUES.SCOPE; + case "permission": + return VALUES.PERMISSIONS; + + case "action": + return VALUES.ACTIONS; + case "child": return VALUES.CHILD; @@ -354,6 +369,9 @@ case "metric": return props.values?.metric || []; + case "user": + return props.values?.user || []; + case "aggregation": return VALUES.AGGREGATION; @@ -388,7 +406,7 @@ if (!Array.isArray(v) || !v.length) return; if (typeof v.at(-1) === "string") { - if (v.at(-2)?.label === "labels") { + if (["labels", "details"].includes(v.at(-2)?.label)) { // Adding labels to proper filter v.at(-2).value?.push(v.at(-1)); closeDropdown(); diff --git a/ui/src/components/filter/useFilters.js b/ui/src/components/filter/useFilters.js index a0ad8841357..be3f408a0ed 100644 --- a/ui/src/components/filter/useFilters.js +++ b/ui/src/components/filter/useFilters.js @@ -2,6 +2,8 @@ import {useI18n} from "vue-i18n"; import DotsSquare from "vue-material-design-icons/DotsSquare.vue"; import TagOutline from "vue-material-design-icons/TagOutline.vue"; +import AccountCheck from "vue-material-design-icons/AccountCheck.vue"; +import AccountOutline from "vue-material-design-icons/AccountOutline.vue"; import MathLog from "vue-material-design-icons/MathLog.vue"; import Sigma from "vue-material-design-icons/Sigma.vue"; import TimelineTextOutline from "vue-material-design-icons/TimelineTextOutline.vue"; @@ -11,6 +13,7 @@ import CalendarEndOutline from "vue-material-design-icons/CalendarEndOutline.vue import FilterVariantMinus from "vue-material-design-icons/FilterVariantMinus.vue"; import StateMachine from "vue-material-design-icons/StateMachine.vue"; import FilterSettingsOutline from "vue-material-design-icons/FilterSettingsOutline.vue"; +import GestureTapButton from "vue-material-design-icons/GestureTapButton.vue"; const getItem = (key) => { return JSON.parse(localStorage.getItem(key) || "[]"); @@ -135,6 +138,34 @@ export function useFilters(prefix) { value: {label: "metric", comparator: undefined, value: []}, comparators: [COMPARATORS.IS], }, + { + key: "user", + icon: AccountOutline, + label: t("filters.options.user"), + value: {label: "user", comparator: undefined, value: []}, + comparators: [COMPARATORS.IS], + }, + { + key: "permission", + icon: AccountCheck, + label: t("filters.options.permission"), + value: {label: "permission", comparator: undefined, value: []}, + comparators: [COMPARATORS.IS], + }, + { + key: "action", + icon: GestureTapButton, + label: t("filters.options.action"), + value: {label: "action", comparator: undefined, value: []}, + comparators: [COMPARATORS.IS], + }, + { + key: "details", + icon: TagOutline, + label: t("filters.options.details"), + value: {label: "details", comparator: undefined, value: []}, + comparators: [COMPARATORS.CONTAINS], + }, { key: "aggregation", icon: Sigma, @@ -184,6 +215,15 @@ export function useFilters(prefix) { let key = match ? match.key : filter.label === "text" ? "q" : null; if (key) { + if (key === "details") { + match.value.value.forEach((item) => { + const value = item.split(":"); + if (value.length === 2) { + console.log(value); + query[`details.${value[0]}`] = value[1]; + } + }); + } if (key !== "date") query[key] = encode(filter.value, key); else { const {startDate, endDate} = filter.value[0]; @@ -193,6 +233,8 @@ export function useFilters(prefix) { } } + delete query.details; + return query; }, {}); }; @@ -207,6 +249,12 @@ export function useFilters(prefix) { ), ) .map(([key, value]) => { + if (key.startsWith("details.")) { + // Handle details.* keys + const detailKey = key.replace("details.", ""); // Extract key after 'details.' + return {label: "details", value: `${detailKey}:${value}`}; + } + const label = key === "q" ? "text" @@ -220,6 +268,17 @@ export function useFilters(prefix) { return {label, value: decodedValue}; }); + // Group all details into a single entry + const details = params + .filter((p) => p.label === "details") + .map((p) => p.value); // Collect all `details` values + + if (details.length > 0) { + // Replace multiple details with a single object + params = params.filter((p) => p.label !== "details"); // Remove individual details + params.push({label: "details", value: details}); + } + // Handle the date functionality by grouping startDate and endDate if they exist if (query.startDate && query.endDate) { params.push({ diff --git a/ui/src/components/filter/useValues.ts b/ui/src/components/filter/useValues.ts index 50d23c4c3a6..92bc41a9f3d 100644 --- a/ui/src/components/filter/useValues.ts +++ b/ui/src/components/filter/useValues.ts @@ -1,6 +1,8 @@ import {useI18n} from "vue-i18n"; import State from "../../utils/state.js"; +import permission from "../../models/permission"; +import action from "../../models/action"; export function useValues(label?: string) { const {t} = useI18n({useScope: "global"}); @@ -44,6 +46,14 @@ export function useValues(label?: string) { label: `${value.charAt(0).toUpperCase()}${value.slice(1)}`, value, })), + PERMISSIONS: Object.entries(permission).map(([key, value]) => ({ + label: key, + value, + })), + ACTIONS: Object.entries(action).map(([key, value]) => ({ + label: key, + value, + })), }; return {VALUES}; diff --git a/ui/src/translations/en.json b/ui/src/translations/en.json index 802e51c4de6..53d20d280cf 100644 --- a/ui/src/translations/en.json +++ b/ui/src/translations/en.json @@ -965,6 +965,7 @@ "label": "Choose filters", "empty": "No data.", "options": { + "user": "User", "namespace": "Namespace", "state": "State", "trigger_state": "State", @@ -973,6 +974,9 @@ "level": "Log level", "task": "Task", "metric": "Metric", + "permission": "Permission", + "action": "Action", + "details": "Details", "aggregation": "Aggregation", "relative_date": "Relative date", "absolute_date": "Absolute date", @@ -1007,9 +1011,7 @@ "settings": { "show_chart": "Show main chart" }, - "labels": { - "placeholder": "Type label as 'key:value'" - } + "key_value_type": "Type parameter as 'key:value'" }, "item": "item", "items": "items",