diff --git a/public/locales/de-DE/translation.json b/public/locales/de-DE/translation.json
index 4435f35..13b0ad5 100644
--- a/public/locales/de-DE/translation.json
+++ b/public/locales/de-DE/translation.json
@@ -232,6 +232,7 @@
},
"processes": {
"title": "Prozessdefinitionen",
+ "filter-placeholder": "Name / Schlüssel suchen",
"version": "Version",
"change-definition": "Definition wechseln",
"definition-id": "Definitions-ID",
diff --git a/public/locales/en-US/translation.json b/public/locales/en-US/translation.json
index 71587ff..d6d69ae 100644
--- a/public/locales/en-US/translation.json
+++ b/public/locales/en-US/translation.json
@@ -232,6 +232,7 @@
},
"processes": {
"title": "Process Definitions",
+ "filter-placeholder": "Search Name / Key",
"version": "Version",
"change-definition": "Change Definition",
"definition-id": "Definition ID",
diff --git a/public/locales/es-ES/translation.json b/public/locales/es-ES/translation.json
index 0c0bfcc..eeb4021 100644
--- a/public/locales/es-ES/translation.json
+++ b/public/locales/es-ES/translation.json
@@ -232,6 +232,7 @@
},
"processes": {
"title": "Definiciones de procesos",
+ "filter-placeholder": "Buscar nombre / clave",
"version": "Versión",
"change-definition": "Cambiar definición",
"definition-id": "ID de definición",
diff --git a/public/locales/fr-FR/translation.json b/public/locales/fr-FR/translation.json
index c799a07..adf17d2 100644
--- a/public/locales/fr-FR/translation.json
+++ b/public/locales/fr-FR/translation.json
@@ -232,6 +232,7 @@
},
"processes": {
"title": "Définitions de processus",
+ "filter-placeholder": "Chercher nom / clé",
"version": "Version",
"change-definition": "Changer de définition",
"definition-id": "ID de définition",
diff --git a/public/locales/nl-NL/translation.json b/public/locales/nl-NL/translation.json
index de89c41..bfa8d43 100644
--- a/public/locales/nl-NL/translation.json
+++ b/public/locales/nl-NL/translation.json
@@ -232,6 +232,7 @@
},
"processes": {
"title": "Procesdefinities",
+ "filter-placeholder": "Naam / sleutel zoeken",
"version": "Versie",
"change-definition": "Definitie wijzigen",
"definition-id": "Definitie-ID",
diff --git a/src/api/resources/process_definition.js b/src/api/resources/process_definition.js
index 71214e2..1cc6804 100644
--- a/src/api/resources/process_definition.js
+++ b/src/api/resources/process_definition.js
@@ -1,7 +1,38 @@
-import { GET, GET_TEXT, POST } from '../helper.jsx'
-
-export const get_process_definitions = (state) =>
- GET('/process-definition/statistics', state, state.api.process.definition.list)
+import { GET, GET_TEXT, POST, RESPONSE_STATE } from '../helper.jsx';
+
+export const get_process_definitions = async (state, queryString = '') => {
+ if (!queryString) {
+ return GET('/process-definition/statistics', state, state.api.process.definition.list);
+ }
+ const defsSignal = { value: null };
+ const statsSignal = { value: null };
+
+ await Promise.all([
+ GET(`/process-definition?${queryString}`, state, defsSignal),
+ GET('/process-definition/statistics', state, statsSignal)
+ ]);
+ if (defsSignal.value.status === RESPONSE_STATE.ERROR || statsSignal.value.status === RESPONSE_STATE.ERROR) {
+ state.api.process.definition.list.value = {
+ status: RESPONSE_STATE.ERROR,
+ error: defsSignal.value.error || statsSignal.value.error
+ };
+ return;
+ }
+
+ const filteredDefs = defsSignal.value.data;
+ const allStats = statsSignal.value.data;
+
+ const validDefinitionIds = new Set(filteredDefs.map(def => def.id));
+
+ const finalFilteredStats = allStats.filter(stat =>
+ validDefinitionIds.has(stat.id || stat.definition?.id)
+ );
+
+ state.api.process.definition.list.value = {
+ status: RESPONSE_STATE.SUCCESS,
+ data: finalFilteredStats
+ };
+};
export const get_process_definition_statistics_with_incidents = (state, id) =>
GET(`/process-definition/${id}/statistics?incidents=true`, state, state.api.process.definition.statistics)
diff --git a/src/assets/icons.jsx b/src/assets/icons.jsx
index 7e3779b..e16421f 100644
--- a/src/assets/icons.jsx
+++ b/src/assets/icons.jsx
@@ -169,3 +169,18 @@ export const arrows_pointing_out = () =>
()
+
+export const save = () => (
+
+);
+
+export const x_mark = () => (
+
+);
\ No newline at end of file
diff --git a/src/components/AdvancedFilter.jsx b/src/components/AdvancedFilter.jsx
new file mode 100644
index 0000000..2b3ce2c
--- /dev/null
+++ b/src/components/AdvancedFilter.jsx
@@ -0,0 +1,263 @@
+import { h } from 'preact';
+import { useState, useEffect, useRef, useContext } from 'preact/hooks';
+import { AppState } from '../state.js'; // 👈 Import your context
+import '../css/components.css';
+import engine_rest from "../api/engine_rest.jsx";
+import * as Icons from '../assets/icons.jsx';
+import { chevron_down, link_out } from "../assets/icons.jsx";
+
+const AVAILABLE_FIELDS = ['Name', 'Key', 'State', 'Tenant ID'];
+const AVAILABLE_OPERATORS = ['like', '=', '!='];
+
+export function AdvancedFilter() {
+ const state = useContext(AppState);
+
+ const [activeFilters, setActiveFilters] = useState([]);
+ const [currentStep, setCurrentStep] = useState('FIELD');
+ const [draftFilter, setDraftFilter] = useState({ field: null, operator: null });
+ const [inputValue, setInputValue] = useState('');
+ const [isDropdownOpen, setIsDropdownOpen] = useState(false);
+
+ // New state for the Saved Queries dropdown
+ const [isSavedQueriesMenuOpen, setIsSavedQueriesMenuOpen] = useState(false);
+ const [savedQueriesList, setSavedQueriesList] = useState([]);
+
+ const inputRef = useRef(null);
+
+ // Trigger backend request whenever activeFilters change
+ useEffect(() => {
+ if (!state) return;
+
+ const params = new URLSearchParams();
+
+ params.append('firstResult', '0');
+ params.append('maxResults', '50');
+ params.append('sortBy', 'name');
+ params.append('sortOrder', 'asc');
+
+ activeFilters.forEach(filter => {
+ const { field, operator, value } = filter;
+ if (!value) return;
+
+ let apiKey = '';
+ let apiValue = value;
+
+ if (field === 'Name') {
+ apiKey = operator === 'like' ? 'nameLike' : 'name';
+ apiValue = operator === 'like' ? `%${value}%` : value;
+ } else if (field === 'Key') {
+ apiKey = operator === 'like' ? 'keyLike' : 'key';
+ apiValue = operator === 'like' ? `%${value}%` : value;
+ }
+
+ if (apiKey) params.append(apiKey, apiValue);
+ });
+
+ const queryString = params.toString();
+
+ console.log("Sending Request to Backend with URL query:", queryString);
+
+ void engine_rest.process_definition.list(state, queryString);
+
+ }, [activeFilters, state]);
+
+ const saveToLocalStorage = () => {
+ if (activeFilters.length === 0) {
+ alert("Cannot save an empty query!");
+ return;
+ }
+
+ let existingSaved = JSON.parse(localStorage.getItem('savedQueries') || '[]');
+
+ // Migration: If the old format was just a single array of objects, wrap it
+ if (existingSaved.length > 0 && !Array.isArray(existingSaved[0])) {
+ existingSaved = [existingSaved];
+ }
+
+ // Prevent saving exact duplicates
+ const newQueryStr = JSON.stringify(activeFilters);
+ const isDuplicate = existingSaved.some(q => JSON.stringify(q) === newQueryStr);
+
+ if (!isDuplicate) {
+ existingSaved.push(activeFilters);
+ localStorage.setItem('savedQueries', JSON.stringify(existingSaved));
+ alert('Query saved to local storage!');
+ } else {
+ alert('This query is already saved!');
+ }
+ };
+
+ const toggleSavedQueriesMenu = () => {
+ if (!isSavedQueriesMenuOpen) {
+ let saved = JSON.parse(localStorage.getItem('savedQueries') || '[]');
+ if (saved.length > 0 && !Array.isArray(saved[0])) {
+ saved = [saved]; // Migration catch for rendering
+ }
+ setSavedQueriesList(saved);
+ }
+ setIsSavedQueriesMenuOpen(!isSavedQueriesMenuOpen);
+ };
+
+ const applySavedQuery = (query) => {
+ setActiveFilters(query);
+ setIsSavedQueriesMenuOpen(false);
+ };
+
+ const resetInputState = () => {
+ setCurrentStep('FIELD');
+ setDraftFilter({ field: null, operator: null });
+ setInputValue('');
+ setIsDropdownOpen(false);
+ };
+
+ const handleInputFocus = () => {
+ if (currentStep !== 'VALUE') {
+ setIsDropdownOpen(true);
+ }
+ };
+
+ const handleInputChange = (e) => {
+ const val = e.target.value;
+ setInputValue(val);
+
+ if (val.trim() === '') {
+ resetInputState();
+ setIsDropdownOpen(true);
+ }
+ };
+
+ const handleKeyDown = (e) => {
+ if (e.key === 'Enter' && currentStep === 'VALUE') {
+ const prefix = `${draftFilter.field} ${draftFilter.operator} `;
+ if (inputValue.startsWith(prefix)) {
+ const extractedValue = inputValue.substring(prefix.length).trim();
+
+ if (extractedValue) {
+ setActiveFilters([...activeFilters, { ...draftFilter, value: extractedValue }]);
+ resetInputState();
+ }
+ }
+ }
+
+ if (e.key === 'Backspace' && inputValue === '') {
+ if (activeFilters.length > 0 && currentStep === 'FIELD') {
+ const newFilters = [...activeFilters];
+ newFilters.pop();
+ setActiveFilters(newFilters);
+ }
+ }
+ };
+
+ const handleOptionSelect = (option) => {
+ if (currentStep === 'FIELD') {
+ setDraftFilter({ field: option, operator: null });
+ setCurrentStep('OPERATOR');
+ setInputValue(`${option} `);
+ } else if (currentStep === 'OPERATOR') {
+ setDraftFilter(prev => ({ ...prev, operator: option }));
+ setCurrentStep('VALUE');
+ setInputValue(`${draftFilter.field} ${option} `);
+ setIsDropdownOpen(false);
+ }
+
+ setTimeout(() => inputRef.current?.focus(), 0);
+ };
+
+ const removeFilter = (indexToRemove) => {
+ setActiveFilters(activeFilters.filter((_, index) => index !== indexToRemove));
+ };
+
+ const dropdownOptions = currentStep === 'FIELD' ? AVAILABLE_FIELDS : AVAILABLE_OPERATORS;
+
+ return (
+
+
+ {activeFilters.map((filter, index) => (
+
+ removeFilter(index)}>×
+ {filter.field} {filter.operator} {filter.value}
+
+ ))}
+
+
+
+
setTimeout(() => setIsDropdownOpen(false), 150)}
+ onInput={handleInputChange}
+ onKeyDown={handleKeyDown}
+ autocomplete="off"
+ />
+
+ {isDropdownOpen && (
+
+ {dropdownOptions.map(option => (
+ - {
+ e.preventDefault();
+ handleOptionSelect(option);
+ }}
+ >
+ {option}
+
+ ))}
+
+ )}
+
+
+
+
{activeFilters.length}
+
+
+
+
+
+
+
+
+ {isSavedQueriesMenuOpen && (
+
+ {savedQueriesList.length === 0 ? (
+ - No saved queries yet.
+ ) : (
+ savedQueriesList.map((query, index) => (
+ - {
+ e.preventDefault();
+ applySavedQuery(query);
+ }}
+ style={{ borderBottom: '1px solid #eee' }}
+ >
+ {/* Render the saved query beautifully as a text string */}
+ {query.map(f => `${f.field} ${f.operator} "${f.value}"`).join(' AND ')}
+
+ ))
+ )}
+
+ )}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/css/components.css b/src/css/components.css
index 79ca847..b6294c2 100644
--- a/src/css/components.css
+++ b/src/css/components.css
@@ -207,6 +207,15 @@ main#processes {
z-index: 999;
}
}
+
+ .space-between {
+ justify-content: space-between;
+ align-items: center;
+ }
+
+ .search-input::placeholder {
+ font-size: var(--small-font-size);
+ }
}
/* tabs */
@@ -789,3 +798,107 @@ dialog#global-search {
}
/* migrations page */
+
+
+/* search filter */
+.filter-bar {
+ display: flex;
+ align-items: center;
+ border: 1px solid #102a5c;
+ background: #fff;
+ border-radius: 3px;
+ padding: 4px 8px;
+ box-shadow: inset 0 1px 3px rgba(0,0,0,0.05);
+ position: relative;
+ min-height: 32px;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
+}
+
+.pills-container {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+}
+
+.filter-pill {
+ display: flex;
+ align-items: center;
+ border: 1px solid #dcdfe4;
+ background-color: #f8f9fa;
+ color: #0052cc;
+ border-radius: 4px;
+ padding: 2px 6px;
+ font-size: 14px;
+ white-space: nowrap;
+}
+
+.filter-pill .remove-btn {
+ color: #0052cc;
+ cursor: pointer;
+ margin-right: 6px;
+ font-weight: bold;
+ font-size: 12px;
+}
+
+.input-wrapper {
+ flex-grow: 1;
+ position: relative;
+ margin-left: 8px;
+}
+
+.input-wrapper input {
+ border: none;
+ outline: none;
+ width: 100%;
+ font-size: 14px;
+ color: #333;
+ font-style: italic;
+ background: transparent;
+}
+
+.input-wrapper input::placeholder {
+ color: #999;
+}
+
+.input-wrapper input.active-typing {
+ font-style: normal;
+ color: #0052cc;
+}
+
+.dropdown-menu {
+ position: absolute;
+ top: 100%;
+ left: 0;
+ background: white;
+ border: 1px solid #ccc;
+ box-shadow: 0 4px 8px rgba(0,0,0,0.1);
+ list-style: none;
+ padding: 0;
+ margin: 8px 0 0 0;
+ width: 200px;
+ z-index: 1000;
+}
+
+.dropdown-menu li {
+ padding: 8px 12px;
+ cursor: pointer;
+ font-size: 14px;
+ color: #333;
+}
+
+.dropdown-menu li:hover {
+ background-color: #f4f5f7;
+}
+
+.actions {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ margin-left: 10px;
+ color: #333;
+}
+
+.action-btn {
+ cursor: pointer;
+ font-size: 16px;
+}
diff --git a/src/pages/Processes.jsx b/src/pages/Processes.jsx
index c40d298..a2c5cf8 100644
--- a/src/pages/Processes.jsx
+++ b/src/pages/Processes.jsx
@@ -1,4 +1,4 @@
-import { signal, useSignalEffect } from '@preact/signals'
+import { signal, useSignal, useSignalEffect } from '@preact/signals'
import { useContext, useEffect } from 'preact/hooks'
import { useLocation, useRoute } from 'preact-iso'
import { useTranslation } from 'react-i18next'
@@ -7,6 +7,7 @@ import * as Icons from '../assets/icons.jsx'
import { AppState } from '../state.js'
import { Accordion } from '../components/Accordion.jsx'
import { BPMNViewer } from '../components/BPMNViewer.jsx'
+import { AdvancedFilter } from '../components/AdvancedFilter'
/**
* Save custom split view width to localstorage
@@ -146,46 +147,66 @@ const ProcessDiagram = () => {
const ProcessDefinitionSelection = () => {
- const
- { api: { process: { definition } } } = useContext(AppState),
- [t] = useTranslation()
+ // 1. Grab the FULL state object from context so we can pass it to our API function
+ const state = useContext(AppState);
+ const { api: { process: { definition } } } = state;
- return
-
- {t("processes.title")}
-
-
-
-
- | {t("common.name")} |
- {t("processes.version")} |
- {t("common.key")} |
- {t("dashboard.instances")} |
- {t("processes.tabs.incidents")} |
- {t("common.state")} |
-
-
- {
-
- const grouped_definitions = Object.groupBy(definition.list.value?.data, ({ definition }) => definition.key)
- console.log(grouped_definitions)
- const grouped_definitions_values = Object.entries(grouped_definitions)
- console.log(grouped_definitions_values)
-
- return <>
- {grouped_definitions_values.map(([key, definition_group]) =>
-
- {definition_group.map(definition => )}
-
- )}
- >
- }
- } />
-
-
-}
+ const [t] = useTranslation();
+
+ const filterFields = ['Name', 'Key', 'State', 'Tenant ID'];
+ const filterOperators = ['like', '=', '!='];
+
+ return (
+
+
+
{t("processes.title")}
+
+
+
+
+
+
+
+ | {t("common.name")} |
+ {t("processes.version")} |
+ {t("common.key")} |
+ {t("dashboard.instances")} |
+ {t("processes.tabs.incidents")} |
+ {t("common.state")} |
+
+
+
+ {
+ const data = definition.list.value?.data ?? [];
+
+ const grouped_definitions = Object.groupBy(data, ({ definition }) => definition.key);
+ const grouped_definitions_values = Object.entries(grouped_definitions);
+
+ return (
+ <>
+ {grouped_definitions_values.map(([key, definition_group]) => (
+
+ {definition_group.map(def => (
+
+ ))}
+
+ ))}
+ >
+ );
+ }}
+ />
+
+
+ );
+};
const ProcessDefinitionDetails = () => {
const