Skip to content

Commit 2c57ad4

Browse files
committed
Refactor editor UI
1 parent 72acabb commit 2c57ad4

File tree

2 files changed

+75
-170
lines changed

2 files changed

+75
-170
lines changed

src/components/QueryEditor.tsx

Lines changed: 70 additions & 139 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,21 @@
11
import React from 'react';
2-
import { ChangeEvent, FormEventHandler, useState } from 'react';
2+
import { ChangeEvent, useState } from 'react';
33
import {
4-
Field,
54
Button,
65
InlineField,
76
InlineFieldRow,
87
Input,
98
ControlledCollapse,
109
InlineSwitch,
11-
Stack,
12-
FeatureBadge,
13-
Switch,
1410
Modal,
1511
useTheme2,
1612
Tooltip,
1713
} from '@grafana/ui';
1814
import { EditorHeader, InlineSelect, FlexItem } from '@grafana/plugin-ui';
19-
import { CoreApp, FeatureState, QueryEditorProps, SelectableValue } from '@grafana/data';
15+
import { CoreApp, QueryEditorProps, SelectableValue } from '@grafana/data';
2016
import { DataSource } from '../datasource';
2117
import { MongoDataSourceOptions, MongoQuery, QueryLanguage, QueryType, DEFAULT_QUERY } from '../types';
22-
import { parseJsQuery, parseJsQueryLegacy, validateJsonQueryText, validatePositiveNumber } from '../utils';
18+
import { parseJsQuery, parseJsQueryLegacy } from '../utils';
2319
import { QueryEditorRaw } from './QueryEditorRaw';
2420
import { QueryToolbox } from './QueryToolbox';
2521
import validator from 'validator';
@@ -47,24 +43,14 @@ const languageOptions: Array<SelectableValue<string>> = [
4743
];
4844

4945
export function QueryEditor(props: Props) {
50-
const { query, onChange, app, onRunQuery } = props;
46+
const { query, app, onRunQuery } = props;
5147

5248
const theme = useTheme2();
5349

54-
const [validationErrors, setValidationErrors] = useState<{ [key: string]: string }>({});
50+
const [queryTextError, setQueryTextError] = useState<string | null>(null);
5551
const [isAggregateOptionExpanded, setIsAggregateOptionExpanded] = useState(false);
5652
const [isEditorExpanded, setIsEditorExpanded] = useState(false);
5753

58-
const addValidationError = (field: string, message: string) => {
59-
setValidationErrors((prev) => ({ ...prev, [field]: message }));
60-
};
61-
62-
const removeValidationError = (field: string) => {
63-
setValidationErrors((prev) => {
64-
const { [field]: _, ...rest } = prev;
65-
return rest;
66-
});
67-
};
6854

6955
const renderRunButton = (isQueryRunnable: boolean) => {
7056
if (isQueryRunnable) {
@@ -93,7 +79,7 @@ export function QueryEditor(props: Props) {
9379
value={query.queryType}
9480
placeholder="Select format"
9581
menuShouldPortal
96-
onChange={(val) => onChange({ ...query, queryType: val.value })}
82+
onChange={(val) => props.onChange({ ...query, queryType: val.value })}
9783
options={queryTypes}
9884
/>
9985
<InlineSelect
@@ -102,7 +88,7 @@ export function QueryEditor(props: Props) {
10288
placeholder="Select query language"
10389
options={languageOptions}
10490
value={query.queryLanguage}
105-
onChange={(val) => props.onChange({ ...query, queryLanguage: val.value })}
91+
onChange={val => props.onChange({ ...query, queryLanguage: val.value })}
10692
/>
10793
<FlexItem grow={1} />
10894
{renderRunButton(true)}
@@ -115,7 +101,25 @@ export function QueryEditor(props: Props) {
115101
? 'javascript'
116102
: 'json'
117103
}
118-
onBlur={onQueryTextChange}
104+
onBlur={(queryText: string) => {
105+
if (query.queryLanguage === QueryLanguage.JAVASCRIPT || query.queryLanguage === QueryLanguage.JAVASCRIPT_SHADOW) {
106+
// parse the JavaScript query
107+
const { error, collection } =
108+
query.queryLanguage === QueryLanguage.JAVASCRIPT_SHADOW
109+
? parseJsQuery(queryText)
110+
: parseJsQueryLegacy(queryText);
111+
// let the same query text as it is
112+
props.onChange({ ...query, queryText: queryText, ...(collection ? { collection } : {}) });
113+
setQueryTextError(error);
114+
} else {
115+
props.onChange({ ...query, queryText: queryText });
116+
if (!validator.isJSON(queryText)) {
117+
setQueryTextError("Query should be a valid JSON")
118+
} else {
119+
setQueryTextError(null);
120+
}
121+
}
122+
}}
119123
width={width}
120124
height={height}
121125
fontSize={14}
@@ -127,7 +131,7 @@ export function QueryEditor(props: Props) {
127131
onExpand={setIsEditorExpanded}
128132
onFormatCode={formatQuery}
129133
showTools={showTools}
130-
error={validationErrors['query']}
134+
error={queryTextError ?? undefined}
131135
/>
132136
);
133137
}}
@@ -151,124 +155,38 @@ export function QueryEditor(props: Props) {
151155
);
152156
};
153157

154-
const onQueryTextChange = (queryText: string) => {
155-
if (query.queryLanguage === QueryLanguage.JAVASCRIPT || query.queryLanguage === QueryLanguage.JAVASCRIPT_SHADOW) {
156-
// parse the JavaScript query
157-
const { error, collection } =
158-
query.queryLanguage === QueryLanguage.JAVASCRIPT_SHADOW
159-
? parseJsQuery(queryText)
160-
: parseJsQueryLegacy(queryText);
161-
// let the same query text as it is
162-
onChange({ ...query, queryText: queryText, ...(collection ? { collection } : {}) });
163-
addValidationError('query', error || '');
164-
} else {
165-
onChange({ ...query, queryText: queryText });
166-
const error = validateJsonQueryText(queryText);
167-
addValidationError('query', error || '');
168-
}
169-
};
170-
171-
const onCollectionChange = (event: ChangeEvent<HTMLInputElement>) => {
172-
onChange({ ...query, collection: event.target.value });
173-
};
174-
175-
const onMaxTimeMSChange = (event: ChangeEvent<HTMLInputElement>) => {
176-
setMaxTimeMSText(event.target.value);
177-
178-
if (!event.target.value) {
179-
onChange({ ...query, aggregateMaxTimeMS: undefined });
180-
} else if (validatePositiveNumber(event.target.value)) {
181-
onChange({ ...query, aggregateMaxTimeMS: parseInt(event.target.value, 10) });
182-
}
183-
};
184-
185-
const onMaxAwaitTimeMSChange = (event: ChangeEvent<HTMLInputElement>) => {
186-
setMaxAwaitTimeMSText(event.target.value);
187-
if (!event.target.value) {
188-
onChange({ ...query, aggregateMaxAwaitTime: undefined });
189-
} else if (validatePositiveNumber(event.target.value)) {
190-
onChange({ ...query, aggregateMaxAwaitTime: parseInt(event.target.value, 10) });
191-
}
192-
};
193-
194-
const onAllowDiskUseChange = (event: ChangeEvent<HTMLInputElement>) => {
195-
onChange({
196-
...query,
197-
aggregateAllowDiskUse: event.target.checked,
198-
});
199-
};
200-
201-
const onBatchSizeChange = (event: ChangeEvent<HTMLInputElement>) => {
202-
setBatchSizeText(event.target.value);
203-
if (!event.target.value) {
204-
onChange({ ...query, aggregateBatchSize: undefined });
205-
} else if (validatePositiveNumber(event.target.value)) {
206-
onChange({ ...query, aggregateBatchSize: parseInt(event.target.value, 10) });
207-
}
208-
};
209-
210-
const onBypassDocumentValidationChange = (event: ChangeEvent<HTMLInputElement>) => {
211-
onChange({ ...query, aggregateBypassDocumentValidation: event.target.checked });
212-
};
213-
214-
const onCommentChange = (event: ChangeEvent<HTMLInputElement>) => {
215-
onChange({ ...query, aggregateComment: event.target.value });
216-
};
217-
218-
const onIsStreamingChange: FormEventHandler<HTMLInputElement> = (e) => {
219-
onChange({ ...query, isStreaming: e.currentTarget.checked });
220-
};
221-
222158
if (!query.queryLanguage) {
223159
query.queryLanguage = DEFAULT_QUERY.queryLanguage;
224160
}
225161

226162
return (
227163
<>
228-
{app !== CoreApp.Explore && (
229-
<div className="query-editor-collection-streaming-container">
230-
<Field
231-
className="query-editor-collection-streaming-field"
232-
label={
233-
<>
234-
<Stack direction="row" gap={1} alignItems="center">
235-
<div className="field-label">Streaming</div>
236-
<FeatureBadge featureState={FeatureState.experimental} />
237-
</Stack>
238-
</>
239-
}
240-
horizontal={true}
241-
>
242-
<Switch
243-
id="query-editor-collection-streaming"
244-
value={query.isStreaming === true}
245-
onChange={onIsStreamingChange}
246-
/>
247-
</Field>
248-
<div className="field-description">Watch MongoDB Change Streams</div>
249-
</div>
250-
)}
251-
252164
<InlineFieldRow>
253165
<InlineField
254-
label="Collection"
255-
error="Collection is required"
166+
label="Collection" error="Collection is required"
256167
invalid={query.queryLanguage !== QueryLanguage.JAVASCRIPT && !query.collection}
257-
tooltip="Name of the MongoDB collection to query"
168+
tooltip="Name of MongoDB collection to query"
258169
>
259170
<Input
260-
width={25}
261-
id="query-editor-collection"
262-
onChange={onCollectionChange}
263-
value={query.collection}
171+
width={25} id="query-editor-collection" value={query.collection}
172+
onChange={(evt: ChangeEvent<HTMLInputElement>) => props.onChange({ ...query, collection: evt.target.value })}
264173
disabled={query.queryLanguage === QueryLanguage.JAVASCRIPT}
265174
/>
266175
</InlineField>
176+
{app !== CoreApp.Explore && <InlineField label="Streaming" tooltip="Watch MongoDB change streams">
177+
<InlineSwitch
178+
id="query-editor-collection-streaming"
179+
value={query.isStreaming === true}
180+
onChange={evt => props.onChange({ ...query, isStreaming: evt.currentTarget.checked })}
181+
/>
182+
</InlineField>
183+
}
184+
267185
</InlineFieldRow>
268186
{isEditorExpanded ? renderPlaceholder() : renderCodeEditor(true, undefined, 300)}
269187

270188
<ControlledCollapse
271-
label="Aggregate Options"
189+
label="Aggregate options"
272190
isOpen={isAggregateOptionExpanded}
273191
onToggle={() => setIsAggregateOptionExpanded(!isAggregateOptionExpanded)}
274192
>
@@ -278,38 +196,53 @@ export function QueryEditor(props: Props) {
278196
tooltip="The maximum amount of time that the query can run on the server. The default value is nil, meaning that there is no time limit for query execution."
279197
>
280198
<Input
281-
id="query-editor-max-time-ms"
199+
id="query-editor-max-time-ms" value={query.aggregateMaxTimeMS}
282200
onChange={(evt: ChangeEvent<HTMLInputElement>) => {
283201
if (!evt.target.value) {
284-
onChange({ ...query, aggregateMaxTimeMS: undefined });
285-
} else if (validator.isInt(evt.target.value, { gt: 1 })) {
286-
onChange({ ...query, aggregateMaxTimeMS: parseInt(evt.target.value, 10) });
202+
props.onChange({ ...query, aggregateMaxTimeMS: undefined });
203+
} else if (validator.isInt(evt.target.value, { gt: 0 })) {
204+
props.onChange({ ...query, aggregateMaxTimeMS: parseInt(evt.target.value, 10) });
287205
}
288206
}}
289-
value={query.aggregateMaxTimeMS}
290207
/>
291208
</InlineField>
292209
<InlineField
293210
label="Max await time(ms)"
294211
tooltip="The maximum amount of time that the server should wait for new documents to satisfy a tailable cursor query."
295212
>
296-
<Input id="query-editor-max-await-time-ms" onChange={onMaxAwaitTimeMSChange} value={maxAwaitTimeMSText} />
213+
<Input id="query-editor-max-await-time-ms" value={query.aggregateMaxAwaitTime}
214+
onChange={(evt: ChangeEvent<HTMLInputElement>) => {
215+
if (!evt.target.value) {
216+
props.onChange({ ...query, aggregateMaxAwaitTime: undefined });
217+
} else if (validator.isInt(evt.target.value, { gt: 0 })) {
218+
props.onChange({ ...query, aggregateMaxAwaitTime: parseInt(evt.target.value, 10) });
219+
}
220+
}} />
297221
</InlineField>
298222
</InlineFieldRow>
299223
<InlineFieldRow>
300224
<InlineField
301225
label="Comment"
302226
tooltip="A string that will be included in server logs, profiling logs, and currentOp queries to help trace the operation."
303227
>
304-
<Input id="query-editor-comment" onChange={onCommentChange} value={query.aggregateComment} />
228+
<Input id="query-editor-comment" value={query.aggregateComment}
229+
onChange={(evt: ChangeEvent<HTMLInputElement>) => {
230+
if (evt.target.value) {
231+
props.onChange({ ...query, aggregateComment: evt.target.value });
232+
}
233+
}} />
305234
</InlineField>
306235
<InlineField
307236
label="Batch size"
308237
tooltip="The maximum number of documents to be included in each batch returned by the server."
309-
error="Invalid batch size"
310-
invalid={batchSizeText !== '' && !validatePositiveNumber(batchSizeText)}
311238
>
312-
<Input id="query-editor-batch-size" onChange={onBatchSizeChange} value={batchSizeText} />
239+
<Input id="query-editor-batch-size" value={query.aggregateBatchSize}
240+
onChange={(evt: ChangeEvent<HTMLInputElement>) => {
241+
if (validator.isInt(evt.target.value, { gt: 0 })) {
242+
props.onChange({ ...query, aggregateBatchSize: parseInt(evt.target.value, 10) });
243+
}
244+
}}
245+
/>
313246
</InlineField>
314247
</InlineFieldRow>
315248
<InlineFieldRow>
@@ -318,19 +251,17 @@ export function QueryEditor(props: Props) {
318251
tooltip="If true, the operation can write to temporary files in the _tmp subdirectory of the database directory path on the server. The default value is false."
319252
>
320253
<InlineSwitch
321-
id="query-editor-allow-disk-use"
322-
onChange={onAllowDiskUseChange}
323-
value={query.aggregateAllowDiskUse}
254+
id="query-editor-allow-disk-use" value={query.aggregateAllowDiskUse}
255+
onChange={(evt: ChangeEvent<HTMLInputElement>) => props.onChange({ ...query, aggregateAllowDiskUse: evt.target.checked })}
324256
/>
325257
</InlineField>
326258
<InlineField
327259
label="Bypass document validation"
328260
tooltip="If true, writes executed as part of the operation will opt out of document-level validation on the server. This option is valid for MongoDB versions >= 3.2 and is ignored for previous server versions. The default value is false."
329261
>
330262
<InlineSwitch
331-
id="query-editor-bypass-document-validation"
332-
onChange={onBypassDocumentValidationChange}
333-
value={query.aggregateBypassDocumentValidation}
263+
id="query-editor-bypass-document-validation" value={query.aggregateBypassDocumentValidation}
264+
onChange={(evt: ChangeEvent<HTMLInputElement>) => props.onChange({ ...query, aggregateBypassDocumentValidation: evt.target.checked })}
334265
/>
335266
</InlineField>
336267
</InlineFieldRow>

0 commit comments

Comments
 (0)