Update load query detail archive dialog for file input support (#15632)

* Update execution-submit-dialog for file input support

Modified the execution-submit-dialog to support file inputs instead of text inputs for better usability. Users can now submit their queries by selecting a JSON file directly or dragging the file into the dialog. Made appropriate UI adjustments to accommodate this change in execution-submit-dialog styles file.

* Update web-console/src/views/workbench-view/execution-submit-dialog/execution-submit-dialog.tsx

Co-authored-by: Charles Smith <techdocsmith@gmail.com>

* Update web-console/src/views/workbench-view/execution-submit-dialog/execution-submit-dialog.tsx

Co-authored-by: Charles Smith <techdocsmith@gmail.com>

* Update web-console/src/views/workbench-view/execution-submit-dialog/execution-submit-dialog.tsx

Co-authored-by: Charles Smith <techdocsmith@gmail.com>

* Update drag-and-drop instructions in execution-submit-dialog

* Add snapshot tests for ExecutionSubmitDialog

* prettify

---------

Co-authored-by: Charles Smith <techdocsmith@gmail.com>
This commit is contained in:
Vadim Ogievetsky 2024-01-10 20:48:46 -08:00 committed by GitHub
parent f445ba4d6b
commit 5b769a7d32
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 200 additions and 38 deletions

View File

@ -0,0 +1,70 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ExecutionSubmitDialog matches snapshot 1`] = `
<Blueprint4.Dialog
backdropProps={
Object {
"onDragLeave": [Function],
"onDragOver": [Function],
"onDrop": [Function],
}
}
canOutsideClickClose={false}
className="execution-submit-dialog"
isOpen={true}
onClose={[Function]}
title="Load query detail archive"
>
<div
className="bp4-dialog-body"
>
<p>
You can load query detail archive files from other Druid clusters to render the query detail here.
</p>
<p>
To download the query detail archive for a query, click on the query in the
<Unknown>
Recent query tasks
</Unknown>
panel in the query view.
</p>
<Blueprint4.FormGroup
label="Select query detail archive file"
>
<Blueprint4.FileInput
fill={true}
hasSelection={false}
inputProps={
Object {
"accept": ".json",
}
}
onInputChange={[Function]}
text="Choose file..."
/>
</Blueprint4.FormGroup>
<p>
Alternatively, drag a file directly onto this dialog.
</p>
</div>
<div
className="bp4-dialog-footer"
>
<div
className="bp4-dialog-footer-actions"
>
<Blueprint4.Button
onClick={[Function]}
text="Close"
/>
<Blueprint4.Button
disabled={true}
intent="primary"
onClick={[Function]}
text="Submit"
/>
</div>
</div>
</Blueprint4.Dialog>
`;

View File

@ -21,11 +21,21 @@
.execution-submit-dialog { .execution-submit-dialog {
&.#{$bp-ns}-dialog { &.#{$bp-ns}-dialog {
top: 5%; top: 5%;
width: 90%; width: 500px;
} }
}
.execution-submit-dialog-textarea {
margin-bottom: 10px; .#{$bp-ns}-overlay-backdrop.dragging-file {
border-bottom: 1px solid rgba(0, 0, 0, 0.25); &::after {
position: absolute;
top: 20px;
left: 20px;
right: 20px;
bottom: 20px;
border: 3px dashed cyan;
border-radius: 20px;
pointer-events: none;
content: '';
} }
} }

View File

@ -0,0 +1,31 @@
/*
* 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 React from 'react';
import { shallow } from '../../../utils/shallow-renderer';
import { ExecutionSubmitDialog } from './execution-submit-dialog';
describe('ExecutionSubmitDialog', () => {
it('matches snapshot', () => {
const comp = shallow(<ExecutionSubmitDialog onSubmit={() => {}} onClose={() => {}} />);
expect(comp).toMatchSnapshot();
});
});

View File

@ -16,10 +16,10 @@
* limitations under the License. * limitations under the License.
*/ */
import { Button, Classes, Dialog, Intent } from '@blueprintjs/core'; import { Button, Classes, Code, Dialog, FileInput, FormGroup, Intent } from '@blueprintjs/core';
import * as JSONBig from 'json-bigint-native'; import * as JSONBig from 'json-bigint-native';
import type { DragEvent } from 'react';
import React, { useState } from 'react'; import React, { useState } from 'react';
import AceEditor from 'react-ace';
import { Execution } from '../../../druid-models'; import { Execution } from '../../../druid-models';
import { AppToaster } from '../../../singletons'; import { AppToaster } from '../../../singletons';
@ -28,6 +28,22 @@ import { offsetToRowColumn } from '../../../utils';
import './execution-submit-dialog.scss'; import './execution-submit-dialog.scss';
function getDraggedFile(ev: DragEvent<HTMLDivElement>): File | undefined {
if (!ev.dataTransfer) return;
if (ev.dataTransfer.items) {
// Use DataTransferItemList interface to access the file(s)
const item = ev.dataTransfer.items[0];
if (item.kind === 'file') {
return item.getAsFile() || undefined;
}
} else {
return ev.dataTransfer.files[0];
}
return;
}
export interface ExecutionSubmitDialogProps { export interface ExecutionSubmitDialogProps {
onSubmit(execution: Execution): void; onSubmit(execution: Execution): void;
onClose(): void; onClose(): void;
@ -37,14 +53,19 @@ export const ExecutionSubmitDialog = React.memo(function ExecutionSubmitDialog(
props: ExecutionSubmitDialogProps, props: ExecutionSubmitDialogProps,
) { ) {
const { onClose, onSubmit } = props; const { onClose, onSubmit } = props;
const [archive, setArchive] = useState(''); const [selectedFile, setSelectedFile] = useState<File | undefined>();
const [dragging, setDragging] = useState(false);
async function handleSubmit(): Promise<void> {
if (!selectedFile) return;
const text = await selectedFile.text();
function handleSubmit(): void {
let parsed: QueryDetailArchive; let parsed: QueryDetailArchive;
try { try {
parsed = JSONBig.parse(archive); parsed = JSONBig.parse(text);
} catch (e) { } catch (e) {
const rowColumn = typeof e.at === 'number' ? offsetToRowColumn(archive, e.at) : undefined; const rowColumn = typeof e.at === 'number' ? offsetToRowColumn(text, e.at) : undefined;
AppToaster.show({ AppToaster.show({
intent: Intent.DANGER, intent: Intent.DANGER,
message: `Could not parse JSON: ${e.message}${ message: `Could not parse JSON: ${e.message}${
@ -56,7 +77,8 @@ export const ExecutionSubmitDialog = React.memo(function ExecutionSubmitDialog(
} }
let execution: Execution | undefined; let execution: Execution | undefined;
const detailArchiveVersion = parsed.detailArchiveVersion ?? (parsed as any).profileVersion; const detailArchiveVersion: unknown =
parsed.detailArchiveVersion ?? (parsed as any).profileVersion;
if (typeof detailArchiveVersion === 'number') { if (typeof detailArchiveVersion === 'number') {
try { try {
if (detailArchiveVersion === 2) { if (detailArchiveVersion === 2) {
@ -102,42 +124,71 @@ export const ExecutionSubmitDialog = React.memo(function ExecutionSubmitDialog(
return ( return (
<Dialog <Dialog
className="execution-submit-dialog" className="execution-submit-dialog"
backdropClassName={dragging ? `dragging-file` : undefined}
style={dragging ? { pointerEvents: 'none' } : undefined}
isOpen isOpen
onClose={onClose} onClose={onClose}
title="Load query detail archive" title="Load query detail archive"
backdropProps={{
onDrop(ev: DragEvent<HTMLDivElement>) {
// Prevent default behavior (Prevent file from being opened)
ev.preventDefault();
if (dragging) setDragging(false);
const droppedFile = getDraggedFile(ev);
if (droppedFile) {
if (!droppedFile.name.endsWith('.json')) {
AppToaster.show({
intent: Intent.DANGER,
message: `The Query Detail Archive must be a .json file`,
timeout: 5000,
});
return;
}
setSelectedFile(droppedFile);
}
},
onDragOver(ev: DragEvent<HTMLDivElement>) {
ev.preventDefault(); // Prevent default behavior (Prevent file from being opened)
if (!dragging) setDragging(true);
},
onDragLeave(ev: DragEvent<HTMLDivElement>) {
ev.preventDefault(); // Prevent default behavior (Prevent file from being opened)
if (dragging) setDragging(false);
},
}}
canOutsideClickClose={false} canOutsideClickClose={false}
> >
<AceEditor <div className={Classes.DIALOG_BODY}>
mode="hjson" <p>
theme="solarized_dark" You can load query detail archive files from other Druid clusters to render the query
className="execution-submit-dialog-textarea placeholder-padding" detail here.
onChange={setArchive} </p>
fontSize={12} <p>
showPrintMargin={false} To download the query detail archive for a query, click on the query in the{' '}
showGutter <Code>Recent query tasks</Code> panel in the query view.
highlightActiveLine </p>
value={archive} <FormGroup label="Select query detail archive file">
width="100%" <FileInput
setOptions={{ hasSelection={Boolean(selectedFile)}
showLineNumbers: true, text={selectedFile?.name ?? 'Choose file...'}
tabSize: 2, onInputChange={e => setSelectedFile((e.target as any).files[0])}
newLineMode: 'unix' as any, // newLineMode is incorrectly assumed to be boolean in the typings inputProps={{ accept: '.json' }}
}} fill
style={{}} />
placeholder="{ Query detail archive or query report... }" </FormGroup>
onLoad={editor => { <p>Alternatively, drag a file directly onto this dialog.</p>
editor.renderer.setPadding(10); </div>
editor.renderer.setScrollMargin(10, 10, 0, 0);
}}
/>
<div className={Classes.DIALOG_FOOTER}> <div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}> <div className={Classes.DIALOG_FOOTER_ACTIONS}>
<Button text="Close" onClick={onClose} /> <Button text="Close" onClick={onClose} />
<Button <Button
text="Submit" text="Submit"
intent={Intent.PRIMARY} intent={Intent.PRIMARY}
onClick={handleSubmit} onClick={() => void handleSubmit()}
disabled={!archive} disabled={!selectedFile}
/> />
</div> </div>
</div> </div>