diff --git a/web-console/package.json b/web-console/package.json
index 9d687ecde74..f64366a67af 100644
--- a/web-console/package.json
+++ b/web-console/package.json
@@ -35,7 +35,7 @@
"test-e2e": "jest --config jest.e2e.config.js e2e-tests",
"codecov": "codecov --disable=gcov -p ..",
"coverage": "jest --coverage src",
- "update-snapshots": "jest -u",
+ "update-snapshots": "jest -u --config jest.unit.config.js",
"autofix": "npm run eslint-fix && npm run sasslint-fix && npm run prettify",
"eslint": "eslint '{src,e2e-tests}/**/*.ts?(x)'",
"eslint-fix": "npm run eslint -- --fix",
diff --git a/web-console/src/components/formatted-input-group/__snapshots__/formatted-input-group.spec.tsx.snap b/web-console/src/components/formatted-input-group/__snapshots__/formatted-input-group.spec.tsx.snap
new file mode 100644
index 00000000000..672f5726649
--- /dev/null
+++ b/web-console/src/components/formatted-input-group/__snapshots__/formatted-input-group.spec.tsx.snap
@@ -0,0 +1,25 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`FormattedInputGroup matches snapshot on undefined value 1`] = `
+
+
+
+`;
+
+exports[`FormattedInputGroup matches snapshot with escaped value 1`] = `
+
+
+
+`;
diff --git a/web-console/src/components/formatted-input-group/formatted-input-group.spec.tsx b/web-console/src/components/formatted-input-group/formatted-input-group.spec.tsx
new file mode 100644
index 00000000000..dfa6739ee8d
--- /dev/null
+++ b/web-console/src/components/formatted-input-group/formatted-input-group.spec.tsx
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { render } from '@testing-library/react';
+import React from 'react';
+
+import { JSON_STRING_FORMATTER } from '../../utils';
+
+import { FormattedInputGroup } from './formatted-input-group';
+
+describe('FormattedInputGroup', () => {
+ it('matches snapshot on undefined value', () => {
+ const suggestibleInput = (
+ {}} formatter={JSON_STRING_FORMATTER} />
+ );
+
+ const { container } = render(suggestibleInput);
+ expect(container.firstChild).toMatchSnapshot();
+ });
+
+ it('matches snapshot with escaped value', () => {
+ const suggestibleInput = (
+ {}}
+ formatter={JSON_STRING_FORMATTER}
+ />
+ );
+
+ const { container } = render(suggestibleInput);
+ expect(container.firstChild).toMatchSnapshot();
+ });
+});
diff --git a/web-console/src/components/formatted-input-group/formatted-input-group.tsx b/web-console/src/components/formatted-input-group/formatted-input-group.tsx
new file mode 100644
index 00000000000..5622906bfb3
--- /dev/null
+++ b/web-console/src/components/formatted-input-group/formatted-input-group.tsx
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { InputGroup, InputGroupProps2 } from '@blueprintjs/core';
+import classNames from 'classnames';
+import React, { useState } from 'react';
+
+import { Formatter } from '../../utils';
+
+export interface FormattedInputGroupProps extends InputGroupProps2 {
+ formatter: Formatter;
+ onValueChange: (newValue: undefined | string) => void;
+}
+
+export const FormattedInputGroup = React.memo(function FormattedInputGroup(
+ props: FormattedInputGroupProps,
+) {
+ const { className, formatter, value, defaultValue, onValueChange, onBlur, ...rest } = props;
+
+ const [intermediateValue, setIntermediateValue] = useState();
+
+ return (
+ {
+ const rawValue = e.target.value;
+ setIntermediateValue(rawValue);
+
+ let parsedValue: string | undefined;
+ try {
+ parsedValue = formatter.parse(rawValue);
+ } catch {
+ return;
+ }
+ onValueChange(parsedValue);
+ }}
+ onBlur={e => {
+ setIntermediateValue(undefined);
+ onBlur?.(e);
+ }}
+ {...rest}
+ />
+ );
+});
diff --git a/web-console/src/components/index.ts b/web-console/src/components/index.ts
index 0ee151bcd64..7b412960c5b 100644
--- a/web-console/src/components/index.ts
+++ b/web-console/src/components/index.ts
@@ -25,6 +25,7 @@ export * from './center-message/center-message';
export * from './clearable-input/clearable-input';
export * from './external-link/external-link';
export * from './form-json-selector/form-json-selector';
+export * from './formatted-input-group/formatted-input-group';
export * from './header-bar/header-bar';
export * from './highlight-text/highlight-text';
export * from './json-collapse/json-collapse';
diff --git a/web-console/src/components/suggestible-input/__snapshots__/suggestible-input.spec.tsx.snap b/web-console/src/components/suggestible-input/__snapshots__/suggestible-input.spec.tsx.snap
index 1c18aeff841..5686c8a7203 100644
--- a/web-console/src/components/suggestible-input/__snapshots__/suggestible-input.spec.tsx.snap
+++ b/web-console/src/components/suggestible-input/__snapshots__/suggestible-input.spec.tsx.snap
@@ -1,8 +1,8 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`suggestible input matches snapshot 1`] = `
+exports[`SuggestibleInput matches snapshot 1`] = `
`;
+
+exports[`SuggestibleInput matches snapshot with escaped value 1`] = `
+