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..13195849
--- /dev/null
+++ b/packages/web/src/components/SearchableJSONViewer/index.tsx
@@ -0,0 +1,108 @@
+import * as React from 'react';
+import get from 'lodash/get';
+import set from 'lodash/set';
+import throttle from 'lodash/throttle';
+import isEmpty from 'lodash/isEmpty';
+import forIn from 'lodash/forIn';
+import isPlainObject from 'lodash/isPlainObject';
+import { Box, Typography } from '@mui/material';
+
+import { IJSONObject } from '@automatisch/types';
+import JSONViewer from 'components/JSONViewer';
+import SearchInput from 'components/SearchInput';
+import useFormatMessage from 'hooks/useFormatMessage';
+
+type JSONViewerProps = {
+ data: IJSONObject;
+};
+
+function aggregate(
+ data: any,
+ searchTerm: string,
+ result = {},
+ prefix: string[] = [],
+ withinArray = false
+) {
+ if (withinArray) {
+ const containerValue = get(result, prefix, []);
+
+ result = aggregate(
+ data,
+ searchTerm,
+ result,
+ prefix.concat(containerValue.length.toString())
+ );
+
+ return result;
+ }
+
+ if (isPlainObject(data)) {
+ forIn(data, (value, key) => {
+ const fullKey = [...prefix, key];
+
+ if (key.toLowerCase().includes(searchTerm)) {
+ set(result, fullKey, value);
+ return;
+ }
+
+ result = aggregate(value, searchTerm, result, fullKey);
+ });
+ }
+
+ if (Array.isArray(data)) {
+ forIn(data, (value) => {
+ result = aggregate(value, searchTerm, result, prefix, true);
+ });
+ }
+
+ if (
+ ['string', 'number'].includes(typeof data) &&
+ String(data).toLowerCase().includes(searchTerm)
+ ) {
+ set(result, prefix, data);
+ }
+
+ return result;
+}
+
+const SearchableJSONViewer = ({ data }: JSONViewerProps) => {
+ const [filteredData, setFilteredData] = React.useState(
+ data
+ );
+ const formatMessage = useFormatMessage();
+
+ const onSearchChange = React.useMemo(
+ () =>
+ throttle((event: React.ChangeEvent) => {
+ const search = (event.target as HTMLInputElement).value.toLowerCase();
+
+ if (!search) {
+ setFilteredData(data);
+ return;
+ }
+
+ const newFilteredData = aggregate(data, search);
+
+ if (isEmpty(newFilteredData)) {
+ setFilteredData(null);
+ } else {
+ setFilteredData(newFilteredData);
+ }
+ }, 400),
+ [data]
+ );
+
+ return (
+ <>
+
+
+
+ {filteredData && }
+ {!filteredData && (
+ {formatMessage('jsonViewer.noDataFound')}
+ )}
+ >
+ );
+};
+
+export default SearchableJSONViewer;
diff --git a/packages/web/src/locales/en.json b/packages/web/src/locales/en.json
index dee7e9cd..bb127ad5 100644
--- a/packages/web/src/locales/en.json
+++ b/packages/web/src/locales/en.json
@@ -138,5 +138,6 @@
"resetPasswordForm.confirmPasswordFieldLabel": "Confirm password",
"resetPasswordForm.passwordUpdated": "The password has been updated. Now, you can login.",
"usageAlert.informationText": "Tasks: {consumedTaskCount}/{allowedTaskCount} (Resets {relativeResetDate})",
- "usageAlert.viewPlans": "View plans"
-}
\ No newline at end of file
+ "usageAlert.viewPlans": "View plans",
+ "jsonViewer.noDataFound": "We couldn't find anything matching your search"
+}