diff --git a/packages/web/src/components/ExecutionStep/index.tsx b/packages/web/src/components/ExecutionStep/index.tsx
index 106d03db..3c011fa2 100644
--- a/packages/web/src/components/ExecutionStep/index.tsx
+++ b/packages/web/src/components/ExecutionStep/index.tsx
@@ -10,7 +10,7 @@ import Box from '@mui/material/Box';
import type { IApp, IExecutionStep, IStep } from '@automatisch/types';
import TabPanel from 'components/TabPanel';
-import JSONViewer from 'components/JSONViewer';
+import SearchableJSONViewer from 'components/SearchableJSONViewer';
import AppIcon from 'components/AppIcon';
import { GET_APPS } from 'graphql/queries/get-apps';
import useFormatMessage from 'hooks/useFormatMessage';
@@ -92,16 +92,16 @@ export default function ExecutionStep(
-
+
-
+
{hasError && (
-
+
)}
diff --git a/packages/web/src/components/SearchableJSONViewer/index.tsx b/packages/web/src/components/SearchableJSONViewer/index.tsx
new file mode 100644
index 00000000..acfcdd79
--- /dev/null
+++ b/packages/web/src/components/SearchableJSONViewer/index.tsx
@@ -0,0 +1,78 @@
+import * as React from 'react';
+import get from 'lodash/get';
+import set from 'lodash/set';
+import throttle from 'lodash/throttle';
+import { Box } from '@mui/material';
+
+import { IJSONObject, IJSONValue } from '@automatisch/types';
+import JSONViewer from 'components/JSONViewer';
+import SearchInput from 'components/SearchInput';
+
+type JSONViewerProps = {
+ data: IJSONObject;
+};
+
+type Entry = [string, IJSONValue];
+
+const SearchableJSONViewer = ({ data }: JSONViewerProps) => {
+ const [filteredData, setFilteredData] = React.useState(data);
+
+ const allEntries = React.useMemo(() => {
+ const entries: Entry[] = [];
+ const collectEntries = (obj: IJSONObject, prefix?: string) => {
+ for (const key in obj) {
+ if (typeof obj[key] === 'object' && obj[key] !== null) {
+ entries.push([[prefix, key].filter(Boolean).join('.'), obj[key]]);
+
+ collectEntries(
+ obj[key] as IJSONObject,
+ [prefix, key].filter(Boolean).join('.')
+ );
+ } else {
+ entries.push([[prefix, key].filter(Boolean).join('.'), obj[key]]);
+ }
+ }
+ };
+
+ collectEntries(data);
+ return entries;
+ }, [data]);
+
+ const onSearchChange = React.useMemo(
+ () =>
+ throttle((event: React.ChangeEvent) => {
+ const search = (event.target as HTMLInputElement).value.toLowerCase();
+ const newFilteredData: IJSONObject = {};
+
+ if (!search) {
+ setFilteredData(data);
+ return;
+ }
+
+ allEntries.forEach(([key, value]) => {
+ if (
+ key.toLowerCase().includes(search) ||
+ (typeof value !== 'object' &&
+ value.toString().toLowerCase().includes(search))
+ ) {
+ const value = get(filteredData, key);
+ set(newFilteredData, key, value);
+ }
+ });
+
+ setFilteredData(newFilteredData);
+ }, 400),
+ [allEntries]
+ );
+
+ return (
+ <>
+
+
+
+
+ >
+ );
+};
+
+export default SearchableJSONViewer;