You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@druid.apache.org by vo...@apache.org on 2021/06/27 01:55:05 UTC
[druid] branch master updated: Web console: allow encoding of ASCII
control chars (#10795)
This is an automated email from the ASF dual-hosted git repository.
vogievetsky pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/druid.git
The following commit(s) were added to refs/heads/master by this push:
new 561cc71 Web console: allow encoding of ASCII control chars (#10795)
561cc71 is described below
commit 561cc718389e6556cdd8246e140f59c06f2d7d29
Author: Vadim Ogievetsky <va...@ogievetsky.com>
AuthorDate: Sat Jun 26 18:54:41 2021 -0700
Web console: allow encoding of ASCII control chars (#10795)
* allow encoding of ascii control chars
* change to JSON
* make json escpaes work
* update snapshot
* break out component
* fix test
* update test script
* update formatter to be more chill
---
web-console/package.json | 2 +-
.../formatted-input-group.spec.tsx.snap | 25 +++++++
.../formatted-input-group.spec.tsx} | 23 +++++-
.../formatted-input-group.tsx | 69 ++++++++++++++++++
web-console/src/components/index.ts | 1 +
.../__snapshots__/suggestible-input.spec.tsx.snap | 49 ++++++++++++-
.../suggestible-input/suggestible-input.spec.tsx | 15 +++-
.../suggestible-input/suggestible-input.tsx | 44 +++++------
.../__snapshots__/retention-dialog.spec.tsx.snap | 2 +-
web-console/src/druid-models/input-format.tsx | 4 +-
web-console/src/utils/formatter.spec.ts | 85 ++++++++++++++++++++++
web-console/src/utils/formatter.ts | 68 +++++++++++++++++
web-console/src/utils/index.tsx | 1 +
13 files changed, 353 insertions(+), 35 deletions(-)
diff --git a/web-console/package.json b/web-console/package.json
index 9d687ec..f64366a 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 0000000..672f572
--- /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`] = `
+<div
+ class="bp3-input-group formatted-input-group"
+>
+ <input
+ class="bp3-input"
+ type="text"
+ value=""
+ />
+</div>
+`;
+
+exports[`FormattedInputGroup matches snapshot with escaped value 1`] = `
+<div
+ class="bp3-input-group formatted-input-group"
+>
+ <input
+ class="bp3-input"
+ type="text"
+ value="Here are some chars \\\\t\\\\r\\\\n lol"
+ />
+</div>
+`;
diff --git a/web-console/src/components/suggestible-input/suggestible-input.spec.tsx b/web-console/src/components/formatted-input-group/formatted-input-group.spec.tsx
similarity index 61%
copy from web-console/src/components/suggestible-input/suggestible-input.spec.tsx
copy to web-console/src/components/formatted-input-group/formatted-input-group.spec.tsx
index 13f86cf..dfa6739 100644
--- a/web-console/src/components/suggestible-input/suggestible-input.spec.tsx
+++ b/web-console/src/components/formatted-input-group/formatted-input-group.spec.tsx
@@ -19,12 +19,27 @@
import { render } from '@testing-library/react';
import React from 'react';
-import { SuggestibleInput } from './suggestible-input';
+import { JSON_STRING_FORMATTER } from '../../utils';
-describe('suggestible input', () => {
- it('matches snapshot', () => {
+import { FormattedInputGroup } from './formatted-input-group';
+
+describe('FormattedInputGroup', () => {
+ it('matches snapshot on undefined value', () => {
+ const suggestibleInput = (
+ <FormattedInputGroup onValueChange={() => {}} formatter={JSON_STRING_FORMATTER} />
+ );
+
+ const { container } = render(suggestibleInput);
+ expect(container.firstChild).toMatchSnapshot();
+ });
+
+ it('matches snapshot with escaped value', () => {
const suggestibleInput = (
- <SuggestibleInput onValueChange={() => {}} suggestions={['a', 'b', 'c']} />
+ <FormattedInputGroup
+ value={`Here are some chars \t\r\n lol`}
+ onValueChange={() => {}}
+ formatter={JSON_STRING_FORMATTER}
+ />
);
const { container } = render(suggestibleInput);
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 0000000..5622906
--- /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<any>;
+ 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<string | undefined>();
+
+ return (
+ <InputGroup
+ className={classNames('formatted-input-group', className)}
+ value={
+ typeof intermediateValue !== 'undefined'
+ ? intermediateValue
+ : typeof value !== 'undefined'
+ ? formatter.stringify(value)
+ : undefined
+ }
+ defaultValue={
+ typeof defaultValue !== 'undefined' ? formatter.stringify(defaultValue) : undefined
+ }
+ onChange={e => {
+ 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 0ee151b..7b41296 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 1c18aef..5686c8a 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`] = `
<div
- class="bp3-input-group suggestible-input"
+ class="bp3-input-group formatted-input-group suggestible-input"
>
<input
class="bp3-input"
@@ -44,3 +44,48 @@ exports[`suggestible input matches snapshot 1`] = `
</span>
</div>
`;
+
+exports[`SuggestibleInput matches snapshot with escaped value 1`] = `
+<div
+ class="bp3-input-group formatted-input-group suggestible-input"
+>
+ <input
+ class="bp3-input"
+ style="padding-right: 0px;"
+ type="text"
+ value="Here are some chars \\\\t\\\\r\\\\n lol"
+ />
+ <span
+ class="bp3-input-action"
+ >
+ <span
+ class="bp3-popover2-target"
+ >
+ <button
+ class="bp3-button bp3-minimal"
+ type="button"
+ >
+ <span
+ class="bp3-icon bp3-icon-caret-down"
+ icon="caret-down"
+ >
+ <svg
+ data-icon="caret-down"
+ height="16"
+ viewBox="0 0 16 16"
+ width="16"
+ >
+ <desc>
+ caret-down
+ </desc>
+ <path
+ d="M12 6.5c0-.28-.22-.5-.5-.5h-7a.495.495 0 00-.37.83l3.5 4c.09.1.22.17.37.17s.28-.07.37-.17l3.5-4c.08-.09.13-.2.13-.33z"
+ fill-rule="evenodd"
+ />
+ </svg>
+ </span>
+ </button>
+ </span>
+ </span>
+</div>
+`;
diff --git a/web-console/src/components/suggestible-input/suggestible-input.spec.tsx b/web-console/src/components/suggestible-input/suggestible-input.spec.tsx
index 13f86cf..41461c9 100644
--- a/web-console/src/components/suggestible-input/suggestible-input.spec.tsx
+++ b/web-console/src/components/suggestible-input/suggestible-input.spec.tsx
@@ -21,7 +21,7 @@ import React from 'react';
import { SuggestibleInput } from './suggestible-input';
-describe('suggestible input', () => {
+describe('SuggestibleInput', () => {
it('matches snapshot', () => {
const suggestibleInput = (
<SuggestibleInput onValueChange={() => {}} suggestions={['a', 'b', 'c']} />
@@ -30,4 +30,17 @@ describe('suggestible input', () => {
const { container } = render(suggestibleInput);
expect(container.firstChild).toMatchSnapshot();
});
+
+ it('matches snapshot with escaped value', () => {
+ const suggestibleInput = (
+ <SuggestibleInput
+ value={`Here are some chars \t\r\n lol`}
+ onValueChange={() => {}}
+ suggestions={['a', 'b', 'c']}
+ />
+ );
+
+ const { container } = render(suggestibleInput);
+ expect(container.firstChild).toMatchSnapshot();
+ });
});
diff --git a/web-console/src/components/suggestible-input/suggestible-input.tsx b/web-console/src/components/suggestible-input/suggestible-input.tsx
index dc34f07..710bc13 100644
--- a/web-console/src/components/suggestible-input/suggestible-input.tsx
+++ b/web-console/src/components/suggestible-input/suggestible-input.tsx
@@ -16,20 +16,18 @@
* limitations under the License.
*/
-import {
- Button,
- HTMLInputProps,
- InputGroup,
- Intent,
- Menu,
- MenuItem,
- Position,
-} from '@blueprintjs/core';
+import { Button, Menu, MenuItem, Position } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import { Popover2 } from '@blueprintjs/popover2';
import classNames from 'classnames';
import React, { useRef } from 'react';
+import { JSON_STRING_FORMATTER } from '../../utils';
+import {
+ FormattedInputGroup,
+ FormattedInputGroupProps,
+} from '../formatted-input-group/formatted-input-group';
+
export interface SuggestionGroup {
group: string;
suggestions: string[];
@@ -37,22 +35,19 @@ export interface SuggestionGroup {
export type Suggestion = undefined | string | SuggestionGroup;
-export interface SuggestibleInputProps extends HTMLInputProps {
- onValueChange: (newValue: undefined | string) => void;
+export interface SuggestibleInputProps extends Omit<FormattedInputGroupProps, 'formatter'> {
onFinalize?: () => void;
suggestions?: Suggestion[];
- large?: boolean;
- intent?: Intent;
}
export const SuggestibleInput = React.memo(function SuggestibleInput(props: SuggestibleInputProps) {
const {
className,
value,
- defaultValue,
onValueChange,
onFinalize,
onBlur,
+ onFocus,
suggestions,
...rest
} = props;
@@ -65,20 +60,19 @@ export const SuggestibleInput = React.memo(function SuggestibleInput(props: Sugg
}
return (
- <InputGroup
+ <FormattedInputGroup
className={classNames('suggestible-input', className)}
- value={value as string}
- defaultValue={defaultValue as string}
- onChange={(e: any) => {
- onValueChange(e.target.value);
- }}
- onFocus={(e: any) => {
+ formatter={JSON_STRING_FORMATTER}
+ value={value}
+ onValueChange={onValueChange}
+ onFocus={e => {
lastFocusValue.current = e.target.value;
+ onFocus?.(e);
}}
- onBlur={(e: any) => {
- if (onBlur) onBlur(e);
+ onBlur={e => {
+ onBlur?.(e);
if (lastFocusValue.current === e.target.value) return;
- if (onFinalize) onFinalize();
+ onFinalize?.();
}}
rightElement={
suggestions && (
@@ -98,7 +92,7 @@ export const SuggestibleInput = React.memo(function SuggestibleInput(props: Sugg
return (
<MenuItem
key={suggestion}
- text={suggestion}
+ text={JSON_STRING_FORMATTER.stringify(suggestion)}
onClick={() => handleSuggestionSelect(suggestion)}
/>
);
diff --git a/web-console/src/dialogs/retention-dialog/__snapshots__/retention-dialog.spec.tsx.snap b/web-console/src/dialogs/retention-dialog/__snapshots__/retention-dialog.spec.tsx.snap
index 487091c..9884096 100644
--- a/web-console/src/dialogs/retention-dialog/__snapshots__/retention-dialog.spec.tsx.snap
+++ b/web-console/src/dialogs/retention-dialog/__snapshots__/retention-dialog.spec.tsx.snap
@@ -232,7 +232,7 @@ exports[`retention dialog matches snapshot 1`] = `
</span>
</div>
<div
- class="bp3-input-group suggestible-input"
+ class="bp3-input-group formatted-input-group suggestible-input"
>
<input
class="bp3-input"
diff --git a/web-console/src/druid-models/input-format.tsx b/web-console/src/druid-models/input-format.tsx
index 4bd9702..7c2f324 100644
--- a/web-console/src/druid-models/input-format.tsx
+++ b/web-console/src/druid-models/input-format.tsx
@@ -113,14 +113,16 @@ export const INPUT_FORMAT_FIELDS: Field<InputFormat>[] = [
name: 'delimiter',
type: 'string',
defaultValue: '\t',
+ suggestions: ['\t', '|', '#'],
defined: (p: InputFormat) => p.type === 'tsv',
info: <>A custom delimiter for data values.</>,
},
{
name: 'listDelimiter',
type: 'string',
+ defaultValue: '\x01',
+ suggestions: ['\x01', '\x00'],
defined: (p: InputFormat) => oneOf(p.type, 'csv', 'tsv', 'regex'),
- placeholder: '(optional, default = ctrl+A)',
info: <>A custom delimiter for multi-value dimensions.</>,
},
{
diff --git a/web-console/src/utils/formatter.spec.ts b/web-console/src/utils/formatter.spec.ts
new file mode 100644
index 0000000..1031263
--- /dev/null
+++ b/web-console/src/utils/formatter.spec.ts
@@ -0,0 +1,85 @@
+/*
+ * 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 { JSON_STRING_FORMATTER } from './formatter';
+
+describe('Formatter', () => {
+ describe('JSON_STRING_FORMATTER', () => {
+ it('has a working stringify', () => {
+ expect(
+ new Array(38).fill(0).map((_, i) => {
+ return JSON_STRING_FORMATTER.stringify(
+ i + ' : `' + String.fromCharCode(i) + '` : `' + String.fromCharCode(i) + '`',
+ );
+ }),
+ ).toEqual([
+ '0 : `\\u0000` : `\\u0000`',
+ '1 : `\\u0001` : `\\u0001`',
+ '2 : `\\u0002` : `\\u0002`',
+ '3 : `\\u0003` : `\\u0003`',
+ '4 : `\\u0004` : `\\u0004`',
+ '5 : `\\u0005` : `\\u0005`',
+ '6 : `\\u0006` : `\\u0006`',
+ '7 : `\\u0007` : `\\u0007`',
+ '8 : `\\b` : `\\b`',
+ '9 : `\\t` : `\\t`',
+ '10 : `\\n` : `\\n`',
+ '11 : `\\u000b` : `\\u000b`',
+ '12 : `\\f` : `\\f`',
+ '13 : `\\r` : `\\r`',
+ '14 : `\\u000e` : `\\u000e`',
+ '15 : `\\u000f` : `\\u000f`',
+ '16 : `\\u0010` : `\\u0010`',
+ '17 : `\\u0011` : `\\u0011`',
+ '18 : `\\u0012` : `\\u0012`',
+ '19 : `\\u0013` : `\\u0013`',
+ '20 : `\\u0014` : `\\u0014`',
+ '21 : `\\u0015` : `\\u0015`',
+ '22 : `\\u0016` : `\\u0016`',
+ '23 : `\\u0017` : `\\u0017`',
+ '24 : `\\u0018` : `\\u0018`',
+ '25 : `\\u0019` : `\\u0019`',
+ '26 : `\\u001a` : `\\u001a`',
+ '27 : `\\u001b` : `\\u001b`',
+ '28 : `\\u001c` : `\\u001c`',
+ '29 : `\\u001d` : `\\u001d`',
+ '30 : `\\u001e` : `\\u001e`',
+ '31 : `\\u001f` : `\\u001f`',
+ '32 : ` ` : ` `',
+ '33 : `!` : `!`',
+ '34 : `\\"` : `\\"`',
+ '35 : `#` : `#`',
+ '36 : `$` : `$`',
+ '37 : `%` : `%`',
+ ]);
+
+ expect(JSON_STRING_FORMATTER.stringify(`hello "world"`)).toEqual(`hello \\"world\\"`);
+ });
+
+ it('has a working parse', () => {
+ expect(JSON_STRING_FORMATTER.parse(`h\u0065llo\t"world"\\`)).toEqual(`hello\t"world"\\`);
+ });
+
+ it('parses back and forth', () => {
+ new Array(38).fill(0).forEach((_, i) => {
+ const str = i + ' : `' + String.fromCharCode(i) + '` : `' + String.fromCharCode(i) + '`';
+ expect(JSON_STRING_FORMATTER.parse(JSON_STRING_FORMATTER.stringify(str))).toEqual(str);
+ });
+ });
+ });
+});
diff --git a/web-console/src/utils/formatter.ts b/web-console/src/utils/formatter.ts
new file mode 100644
index 0000000..2efbb00
--- /dev/null
+++ b/web-console/src/utils/formatter.ts
@@ -0,0 +1,68 @@
+/*
+ * 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.
+ */
+
+export interface Formatter<T> {
+ stringify: (thing: T) => string;
+ parse: (str: string) => T;
+}
+
+const JSON_ESCAPES: Record<string, string> = {
+ '"': '"',
+ '\\': '\\',
+ '/': '/',
+ 'b': '\b',
+ 'f': '\f',
+ 'n': '\n',
+ 'r': '\r',
+ 't': '\t',
+};
+
+// The stringifier is just JSON minus the double quotes, the parser is much more forgiving
+export const JSON_STRING_FORMATTER: Formatter<string> = {
+ stringify: (str: string) => {
+ if (typeof str !== 'string') throw new TypeError(`must be a string`);
+
+ const json = JSON.stringify(str);
+ return json.substr(1, json.length - 2);
+ },
+ parse: (str: string) => {
+ const n = str.length;
+ let i = 0;
+ let parsed = '';
+ while (i < n) {
+ const ch = str[i];
+ if (ch === '\\') {
+ const nextCh = str[i + 1];
+ if (nextCh === 'u' && /^[0-9a-f]{4}$/i.test(str.substr(i + 2, 4))) {
+ parsed += String.fromCharCode(parseInt(str.substr(i + 2, 4), 16));
+ i += 6;
+ } else if (JSON_ESCAPES[nextCh]) {
+ parsed += JSON_ESCAPES[nextCh];
+ i += 2;
+ } else {
+ parsed += ch;
+ i++;
+ }
+ } else {
+ parsed += ch;
+ i++;
+ }
+ }
+ return parsed;
+ },
+};
diff --git a/web-console/src/utils/index.tsx b/web-console/src/utils/index.tsx
index 0b40e73..0ac487b 100644
--- a/web-console/src/utils/index.tsx
+++ b/web-console/src/utils/index.tsx
@@ -21,6 +21,7 @@ export * from './column-metadata';
export * from './date';
export * from './druid-lookup';
export * from './druid-query';
+export * from './formatter';
export * from './general';
export * from './local-storage-keys';
export * from './object-change';
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org