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" +}